From 0a1ae9af3ab2a82c2a8191b131809a72d866e2b2 Mon Sep 17 00:00:00 2001 From: Kamal Kishor Joshi Date: Wed, 10 Apr 2024 23:30:10 +0530 Subject: [PATCH] Add menu options to update/modify palette before saving (#208) * 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 --- components/SingleColorView.js | 184 ++++++++++++++++++++++++++++------ libs/Helpers.js | 22 +++- screens/ColorListScreen.js | 81 +++++++++++---- screens/ColorPickerScreen.js | 3 +- 4 files changed, 235 insertions(+), 55 deletions(-) diff --git a/components/SingleColorView.js b/components/SingleColorView.js index 6149a656..a819326f 100644 --- a/components/SingleColorView.js +++ b/components/SingleColorView.js @@ -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'; @@ -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 ( - - - {color.color.toUpperCase() + (color.name ? ' (' + color.name + ')' : '')} - - - { - onColorChange({ ...color, color: color.color, locked: !color.locked }); - }}> - - - - - - - + + + + {color.color.toUpperCase() + (color.name ? ' (' + color.name + ')' : '')} + + + { + onColorChange({ ...color, color: color.color, locked: !color.locked }); + }}> + + + + + + + + + + + {}}> + + {color.color.toUpperCase()} + {menuItems.map((item, index) => ( + <> + + {item.label} + + + + ))} + + + + + + + ); }; @@ -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' } }); diff --git a/libs/Helpers.js b/libs/Helpers.js index edefe528..60155b99 100644 --- a/libs/Helpers.js +++ b/libs/Helpers.js @@ -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'; @@ -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) => { @@ -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 ( diff --git a/screens/ColorListScreen.js b/screens/ColorListScreen.js index cbdc68bc..24010c46 100644 --- a/screens/ColorListScreen.js +++ b/screens/ColorListScreen.js @@ -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({ @@ -21,19 +26,59 @@ export default function ColorListScreen({ navigation }) { : t('Colors') }); }, []); - const renderItem = ({ item, drag }) => ( - { - 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 ( + { + 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'); @@ -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) => { @@ -68,7 +113,7 @@ export default function ColorListScreen({ navigation }) { autoscrollThreshold={100} /> - Generate new colors for unlocked colors + {helpMessage} { diff --git a/screens/ColorPickerScreen.js b/screens/ColorPickerScreen.js index 9bb2e026..5e535251 100644 --- a/screens/ColorPickerScreen.js +++ b/screens/ColorPickerScreen.js @@ -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'; @@ -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');