Skip to content

Commit

Permalink
Add menu options to update/modify palette before saving (#208)
Browse files Browse the repository at this point in the history
* basic working model on clicking on SingleColorView

* add color and remove color option in the menu

* cleanup and minor improvements

* restore pro in case init fails to restore
  • Loading branch information
kamalkishor1991 authored Apr 10, 2024
1 parent 70cffe0 commit 0a1ae9a
Show file tree
Hide file tree
Showing 4 changed files with 235 additions and 55 deletions.
184 changes: 153 additions & 31 deletions components/SingleColorView.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import * as React from 'react';
import { Platform, StyleSheet, Text, Clipboard, TouchableOpacity, View } from 'react-native';
import React, { useState } from 'react';
import {
Platform,
StyleSheet,
Text,
Clipboard,
TouchableOpacity,
View,
Modal,
TouchableWithoutFeedback,
Animated
} from 'react-native';
import { notifyMessage } from '../libs/Helpers';
import FontAwesome5 from 'react-native-vector-icons/FontAwesome5';
import MaterialIcons from 'react-native-vector-icons/MaterialIcons';
Expand All @@ -20,41 +30,99 @@ function getContrastColor(bgColor) {
return L > 0.179 ? 'black' : 'white';
}

export const SingleColorView = ({ color, onColorChange, drag }) => {
const handlePress = () => {
export const SingleColorView = ({ color, onColorChange, drag, onRemove, onAdd }) => {
const [modalVisible, setModalVisible] = useState(false);

const openModal = () => {
setModalVisible(true);
};

const closeModal = () => {
setModalVisible(false);
};

const handleCopyColor = () => {
if (Platform?.OS === 'android' || Platform.OS === 'ios') {
notifyMessage(color.color + ' copied to clipboard!');
}
Clipboard.setString(color.color);
closeModal();
};

const textColor = getContrastColor(color.color);
const handleRmoveColor = () => {
onRemove();
closeModal();
};
const handleAddColor = () => {
onAdd();
closeModal();
};

const textColor = getContrastColor(color.color);
const menuItems = [
{ label: 'Copy Color', onPress: handleCopyColor },
//{ label: 'Edit Color', onPress: handleEditColor },
{ label: 'Add Color', onPress: handleAddColor },
{ label: 'Remove Color', onPress: handleRmoveColor }
];
return (
<TouchableOpacity
onPress={handlePress}
//onPressIn={drag}
onLongPress={drag}
style={[styles.container, { backgroundColor: color.color }]}>
<Text style={[styles.colorText, { color: textColor }]}>
{color.color.toUpperCase() + (color.name ? ' (' + color.name + ')' : '')}
</Text>
<View style={styles.actionArea}>
<TouchableOpacity
style={styles.actionAreaItem}
onPress={() => {
onColorChange({ ...color, color: color.color, locked: !color.locked });
}}>
<FontAwesome5
style={[styles.icon, { color: textColor }]}
name={color.locked ? 'lock' : 'unlock'}
/>
</TouchableOpacity>
<TouchableOpacity style={styles.actionAreaItem} onPressIn={drag}>
<MaterialIcons style={[styles.icon, { color: textColor }]} name="drag-indicator" />
</TouchableOpacity>
</View>
</TouchableOpacity>
<Animated.View style={[styles.container, { opacity: color.opacity }]}>
<TouchableOpacity
onPress={openModal}
onLongPress={drag}
style={[styles.container, { backgroundColor: color.color }]}>
<Text style={[styles.colorText, { color: textColor }]}>
{color.color.toUpperCase() + (color.name ? ' (' + color.name + ')' : '')}
</Text>
<View style={styles.actionArea}>
<TouchableOpacity
style={[styles.actionAreaItem, styles.lockActionAreaItem]}
onPress={() => {
onColorChange({ ...color, color: color.color, locked: !color.locked });
}}>
<FontAwesome5
style={[styles.icon, styles.lockIcon, { color: textColor }]}
name={color.locked ? 'lock' : 'unlock'}
/>
</TouchableOpacity>
<TouchableOpacity
style={[styles.actionAreaItem, styles.dragActionAreaItem]}
onPressIn={drag}>
<MaterialIcons
style={[styles.icon, styles.dragIcon, { color: textColor }]}
name="drag-indicator"
/>
</TouchableOpacity>
</View>

<Modal
animationType="slide"
transparent={true}
visible={modalVisible}
onRequestClose={closeModal}>
<TouchableWithoutFeedback onPress={closeModal}>
<View style={styles.modalContainer}>
<TouchableWithoutFeedback onPress={() => {}}>
<View style={styles.modalContent}>
<Text style={styles.modalText}>{color.color.toUpperCase()}</Text>
{menuItems.map((item, index) => (
<>
<TouchableOpacity
key={index}
style={[styles.menuButton]}
onPress={item.onPress}>
<Text style={styles.menuButtonText}>{item.label}</Text>
</TouchableOpacity>
<View style={styles.lineseperator}></View>
</>
))}
</View>
</TouchableWithoutFeedback>
</View>
</TouchableWithoutFeedback>
</Modal>
</TouchableOpacity>
</Animated.View>
);
};

Expand All @@ -74,15 +142,69 @@ const styles = StyleSheet.create({
actionArea: {
position: 'absolute',
right: 0,
padding: 8,
flex: 1,
flexDirection: 'row'
},
icon: {
paddingHorizontal: 8
},
lockIcon: {
fontSize: 16,
opacity: 0.6
},
dragIcon: {
fontSize: 24
},
actionAreaItem: {
marginRight: 8,
marginLeft: 8
},
dragActionAreaItem: {
padding: 8
},
lockActionAreaItem: {
padding: 8,
marginVertical: 4
},
modalContainer: {
flex: 1,
justifyContent: 'flex-end',
backgroundColor: 'rgba(0, 0, 0, 0.5)'
},
modalContent: {
backgroundColor: 'white',
paddingVertical: 20,
borderTopLeftRadius: 20,
borderTopRightRadius: 20,
alignItems: 'center',
height: '40%'
},
modalText: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 20
},
copyButton: {
backgroundColor: 'blue',
paddingVertical: 10,
paddingHorizontal: 20,
borderRadius: 4,
marginBottom: 10
},
menuButton: {
paddingHorizontal: 12,
borderRadius: 8,
paddingVertical: 10,
width: '100%',
alignItems: 'center'
},
menuButtonText: {
color: 'black',
fontSize: 16,
fontWeight: 'bold'
},
lineseperator: {
height: 1,
width: '100%',
backgroundColor: 'gray'
}
});
22 changes: 18 additions & 4 deletions libs/Helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import * as RNIap from 'react-native-iap';
import { requestPurchase, getProducts } from 'react-native-iap';
import { sendClientErrorAsync } from '../network/errors';

const isProduction = () => {
// eslint-disable-next-line no-undef
return __DEV__ === false;
};

const readRemoteConfig = async (key) => {
// Native module always returns string. So, we need to convert it to boolean.
return (await NativeModules.CromaModule.getConfigString(key)) == 'true';
Expand All @@ -13,7 +18,11 @@ const productSku = function () {
return Platform.OS === 'android' ? 'croma_pro' : 'app_croma';
};
const sendClientError = (event, errorMessage, stacktrace) => {
sendClientErrorAsync(event + ' - ' + errorMessage, stacktrace || new Error().stack);
if (isProduction) {
sendClientErrorAsync(event + ' - ' + errorMessage, stacktrace || new Error().stack);
} else {
console.log('Client error', event, errorMessage, stacktrace);
}
};

const logEvent = (eventName, value) => {
Expand Down Expand Up @@ -45,10 +54,15 @@ const purchase = async function (setPurchase, productSKU) {
logEvent('purchase_successful');
notifyMessage('Congrats, You are now a pro user!');
} catch (err) {
console.warn(err.code, err.message);
notifyMessage(`Purchase unsuccessful ${err}`);
sendClientError('purchase_failed', err.message, err.stack);
if (err.code == 'E_ALREADY_OWNED') {
setPurchase('Already owned');
notifyMessage('Purchase restored successfully!');
} else {
console.warn(err.code, err.message);
notifyMessage(`Purchase unsuccessful ${err.message}`);
}
logEvent('purchase_failed', err.message);
sendClientError('purchase_failed', err.message + 'Error code: ' + err.code, err.stack);
}
};
const initPurchase = async function (
Expand Down
81 changes: 63 additions & 18 deletions screens/ColorListScreen.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import React, { useLayoutEffect } from 'react';
import { SingleColorView } from '../components/SingleColorView';
import { StyleSheet, View, Text, Platform } from 'react-native';
import { StyleSheet, View, Text, Platform, Animated } from 'react-native';
import CromaButton from '../components/CromaButton';
import { logEvent, notifyMessage } from '../libs/Helpers';
import { logEvent } from '../libs/Helpers';
import { CromaContext } from '../store/store';
import { useTranslation } from 'react-i18next';
import DraggableFlatList from 'react-native-draggable-flatlist';
import { Color } from 'pigment/full';

export default function ColorListScreen({ navigation }) {
const { t } = useTranslation();
const [helpMessage, setHelpMessage] = React.useState('Generate new colors for unlocked colors');

const { colorList, setColorList } = React.useContext(CromaContext);
const colors = uniqueColors(colorList);
const colors = uniqueColors(colorList).map((color) => ({
...color,
opacity: color.opacity || new Animated.Value(1)
}));

useLayoutEffect(() => {
navigation.setOptions({
Expand All @@ -21,19 +26,59 @@ export default function ColorListScreen({ navigation }) {
: t('Colors')
});
}, []);
const renderItem = ({ item, drag }) => (
<SingleColorView
onColorChange={(updatedColor) => {
const index = colors.findIndex((color) => color.color === updatedColor.color);
const updatedColors = [...colors];
updatedColors[index] = updatedColor;
setColorList(updatedColors);
}}
key={item.color + '-' + item.locked}
color={item}
drag={drag}
/>
);
const renderItem = ({ item, drag }) => {
const opecity = item.opacity;
return (
<SingleColorView
onColorChange={(updatedColor) => {
const index = colors.findIndex((color) => color.color === updatedColor.color);
const updatedColors = [...colors];
updatedColors[index] = updatedColor;
setColorList(updatedColors);
}}
opacity={opecity}
key={item.color + '-' + item.locked}
color={item}
drag={drag}
onAdd={() => {
logEvent('add_color_to_palette');
const index = colors.findIndex((color) => color.color === item.color);
const currentColor = new Color(colors[index].color);
const newColor = {
color: currentColor.darken(0.1).tohex(),
locked: false,
opacity: new Animated.Value(0)
};
const updatedColors = [
...colors.slice(0, index + 1),
newColor,
...colors.slice(index + 1)
];

setColorList(updatedColors);

// Find the opacity value of the newly added color
const newColorOpacity = updatedColors[index + 1].opacity;
newColorOpacity.setValue(0);
Animated.timing(newColorOpacity, {
toValue: 1,
duration: 1000,
useNativeDriver: true
}).start();
}}
onRemove={() => {
Animated.timing(opecity, {
toValue: 0,
duration: 600,
useNativeDriver: true
}).start(() => {
logEvent('remove_color_from_palette');
setColorList(colors.filter((color) => color.color !== item.color));
});
}}
/>
);
};

const onDragEnd = ({ data }) => {
logEvent('drag_end_event_color_list');
Expand All @@ -42,7 +87,7 @@ export default function ColorListScreen({ navigation }) {
const regenerateUnlockedColors = () => {
logEvent('regenerate_unlocked_colors', colors.filter((color) => !color.locked).length);
if (colors.filter((color) => !color.locked).length == 0) {
notifyMessage(t('Please unlock at least one color'));
setHelpMessage('Please unlock some colors or add colors to generate new colors');
} else {
// TODO: improve this algorithm.
const newColors = colors.map((color) => {
Expand All @@ -68,7 +113,7 @@ export default function ColorListScreen({ navigation }) {
autoscrollThreshold={100}
/>
</View>
<Text style={styles.hintText}>Generate new colors for unlocked colors</Text>
<Text style={styles.hintText}>{helpMessage}</Text>
<CromaButton
style={styles.button}
onPress={() => {
Expand Down
3 changes: 1 addition & 2 deletions screens/ColorPickerScreen.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import { ScrollView, View, StyleSheet, TouchableOpacity, Text } from 'react-native';
import CromaButton from '../components/CromaButton';
import { CromaColorPicker as ColorPicker } from 'croma-color-picker';
Expand All @@ -7,7 +7,6 @@ import { CromaContext } from '../store/store';
import SliderColorPicker from '../components/SliderColorPicker';
import AIColorPicker from '../components/AIColorPicker';
import Colors from '../constants/Colors';
import { useEffect } from 'react/cjs/react.production.min';

export default function ColorPickerScreen({ navigation }) {
const [color, setColor] = useState('#db0a5b');
Expand Down

0 comments on commit 0a1ae9a

Please sign in to comment.