mobile: allow custom colors

This commit is contained in:
Ammar Ahmed
2023-12-05 14:52:06 +05:00
committed by Abdullah Atta
parent ceb948bd1f
commit f1f4cd67f7
5 changed files with 447 additions and 103 deletions

View File

@@ -18,25 +18,36 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import React from "react";
import { View } from "react-native";
import { View, ViewProps } from "react-native";
import { DDS } from "../../services/device-detection";
import { useThemeColors } from "@notesnook/theme";
import { getElevationStyle } from "../../utils/elevation";
const DialogContainer = ({ width, height, ...restProps }) => {
const DialogContainer = ({
width,
height,
style,
...restProps
}: ViewProps & {
width?: any;
height?: any;
}) => {
const { colors } = useThemeColors();
return (
<View
{...restProps}
style={{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
}}
style={[
{
...getElevationStyle(5),
width: width || DDS.isTab ? 500 : "85%",
maxHeight: height || 450,
borderRadius: 10,
backgroundColor: colors.primary.background,
paddingTop: 12
},
style
]}
/>
);
};

View File

@@ -0,0 +1,302 @@
/*
This file is part of the Notesnook project (https://notesnook.com/)
Copyright (C) 2023 Streetwriters (Private) Limited
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { isThemeColor, useThemeColors } from "@notesnook/theme";
import React, { useRef, useState } from "react";
import { TextInput, View } from "react-native";
import { FlashList } from "react-native-actions-sheet";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { useSettingStore } from "../../../stores/use-setting-store";
import { SIZE } from "../../../utils/size";
import BaseDialog from "../../dialog/base-dialog";
import DialogContainer from "../../dialog/dialog-container";
import { Button } from "../../ui/button";
import Input from "../../ui/input";
import { PressableButton } from "../../ui/pressable";
import { ToastManager } from "../../../services/event-manager";
import { Toast } from "../../toast";
import { db } from "../../../common/database";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useMenuStore } from "../../../stores/use-menu-store";
const arrayOfColors = [
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7",
"#FF5733",
"#33FF57",
"#339DFF",
"#FF33E9",
"#E9FF33",
"#FF3395",
"#95FF33",
"#FF3369",
"#6933FF",
"#33FFC7"
];
const HEX_COLOR_REGEX_ALPHA =
/^#(?:(?:[\da-fA-F]{3}){1,2}|(?:[\da-fA-F]{4}){1,2})$/;
const convertToColorObjects = (colors: string[]) => {
const colorObjects = [];
for (let i = 0; i < colors.length; i += 3) {
colorObjects.push({
colorOne: colors[i],
colorTwo: colors[i + 1],
colorThree: colors[i + 2]
});
}
return colorObjects;
};
const ColorPicker = ({
visible,
setVisible
}: {
visible: boolean;
setVisible: (value: boolean) => void;
}) => {
const [selectedColor, setSelectedColor] = useState<string>();
const { colors } = useThemeColors();
const inputRef = useRef<TextInput>(null);
const title = useRef<string>();
const renderItem = ({
item
}: {
item: { colorOne: string; colorTwo: string; colorThree: string };
}) => (
<View>
{Object.keys(item).map((key) => (
<PressableButton
key={item[key as keyof typeof item]}
type="accent"
accentColor={item[key as keyof typeof item]}
customStyle={{
width: 40,
height: 40,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 10,
marginBottom: 10
}}
onPress={() => {
const color = item[key as keyof typeof item];
setSelectedColor(color);
inputRef.current?.setNativeProps({
placeholder: color,
text: color
});
}}
>
{selectedColor === item[key as keyof typeof item] ? (
<Icon name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
))}
</View>
);
return (
<BaseDialog
visible={visible}
onRequestClose={() => {
setVisible(false);
useSettingStore.getState().setSheetKeyboardHandler(true);
}}
statusBarTranslucent={false}
centered
>
<Toast context="color-picker" />
<DialogContainer
style={{
paddingTop: 0
}}
>
<View
style={{
padding: 20
}}
>
<FlashList
extraData={selectedColor}
horizontal
data={convertToColorObjects(arrayOfColors)}
renderItem={renderItem}
/>
<View
style={{
flexDirection: "row",
alignItems: "center",
paddingTop: 10,
columnGap: 10,
marginBottom: 10
}}
>
<Input
fwdRef={inputRef}
placeholder="#f0f0f0"
marginBottom={0}
onChangeText={(value) => {
if (HEX_COLOR_REGEX_ALPHA.test(value)) {
setSelectedColor(value);
inputRef.current?.setNativeProps({
placeholder: value,
text: value
});
}
}}
/>
<PressableButton
type="accent"
accentColor={selectedColor || colors.secondary.background}
customStyle={{
width: 45,
height: 45,
borderRadius: 100,
justifyContent: "center",
alignItems: "center"
}}
/>
</View>
<Input
marginBottom={10}
onChangeText={(value) => {
title.current = value;
}}
placeholder={title.current || "Color title"}
/>
<Button
title="Add color"
onPress={async () => {
if (!selectedColor)
return ToastManager.error(
new Error("Select a color"),
undefined,
"color-picker"
);
if (!title.current)
return ToastManager.error(
new Error("Enter a title for the color")
);
const exists = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", selectedColor)])
);
if (exists)
return ToastManager.error(
new Error(`Color #${selectedColor} already exists`)
);
await db.colors.add({
title: title.current,
colorCode: selectedColor
});
useRelationStore.getState().update();
useMenuStore.getState().setColorNotes();
setVisible(false);
}}
type={selectedColor ? "grayAccent" : "grayBg"}
width="100%"
/>
</View>
</DialogContainer>
</BaseDialog>
);
};
export default ColorPicker;

View File

@@ -17,11 +17,10 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import { DefaultColors } from "@notesnook/core/dist/collections/colors";
import { Color, ItemReference, Note } from "@notesnook/core/dist/types";
import { Color, Note } from "@notesnook/core/dist/types";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect, useState } from "react";
import { View } from "react-native";
import React, { useCallback, useEffect, useState } from "react";
import { ScrollView, View } from "react-native";
import Icon from "react-native-vector-icons/MaterialCommunityIcons";
import { notesnook } from "../../../e2e/test.ids";
import { db } from "../../common/database";
@@ -32,87 +31,29 @@ import { useRelationStore } from "../../stores/use-relation-store";
import { useSettingStore } from "../../stores/use-setting-store";
import { refreshNotesPage } from "../../utils/events";
import { SIZE } from "../../utils/size";
import ColorPicker from "../dialogs/color-picker";
import { PressableButton } from "../ui/pressable";
import { FlashList } from "react-native-actions-sheet";
import { Button } from "../ui/button";
export const ColorTags = ({ item }: { item: Note }) => {
const ColorItem = ({ item, note }: { item: Color; note: Note }) => {
const { colors } = useThemeColors();
const [isLinked, setIsLinked] = useState<boolean>();
const setColorNotes = useMenuStore((state) => state.setColorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const getColorInfo = async (colorCode: string) => {
const dbColor = await db.colors.all.find((v) =>
v.and([v(`colorCode`, "==", colorCode)])
);
let isLinked = false;
if (dbColor) {
const hasRelation = await db.relations.from(dbColor, "note").has(item.id);
if (hasRelation) {
isLinked = true;
}
}
return {
linked: isLinked,
item: dbColor
useEffect(() => {
const checkIsLinked = async (color: Color) => {
const hasRelation = await db.relations.from(color, "note").has(note.id);
return hasRelation;
};
};
const ColorItem = ({ name }: { name: keyof typeof DefaultColors }) => {
const color = DefaultColors[name];
const [colorInfo, setColorInfo] = useState<{
linked: boolean;
item: Color | undefined;
}>();
checkIsLinked(item).then((info) => setIsLinked(info));
}, [item, note.id]);
useEffect(() => {
getColorInfo(color).then((info) => setColorInfo(info));
}, [color]);
const toggleColor = async () => {
await db.relations.unlinkOfType("color", [item.id]);
return (
<PressableButton
type="accent"
accentColor={color}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(name)}
key={color}
onPress={() => changeColor(name)}
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: isTablet ? 10 : undefined
}}
>
{colorInfo?.linked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
const changeColor = async (color: string) => {
const colorInfo = await getColorInfo(DefaultColors[color]);
if (colorInfo.item) {
if (colorInfo.linked) {
await db.relations.unlink(colorInfo.item, item);
} else {
await db.relations.add(colorInfo.item, item);
}
} else {
const colorId = await db.colors.add({
title: color,
colorCode: DefaultColors[color]
});
const dbColor = await db.colors.color(colorId);
if (dbColor) {
await db.relations.add(dbColor as unknown as ItemReference, item);
}
if (!isLinked) {
await db.relations.add(item, note);
}
useRelationStore.getState().update();
@@ -122,20 +63,109 @@ export const ColorTags = ({ item }: { item: Note }) => {
};
return (
<View
style={{
flexDirection: "row",
flexWrap: "wrap",
flexGrow: isTablet ? undefined : 1,
paddingHorizontal: 12,
paddingRight: 0,
<PressableButton
type="accent"
accentColor={item.colorCode}
accentText={colors.static.white}
testID={notesnook.ids.dialogs.actionsheet.color(item.colorCode)}
key={item.id}
onPress={toggleColor}
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
justifyContent: isTablet ? "center" : "space-between"
marginRight: 5
}}
>
{Object.keys(DefaultColors).map((name: keyof typeof DefaultColors) => {
return <ColorItem key={name} name={name} />;
})}
</View>
{isLinked ? (
<Icon testID="icon-check" name="check" color="white" size={SIZE.lg} />
) : null}
</PressableButton>
);
};
export const ColorTags = ({ item }: { item: Note }) => {
const { colors } = useThemeColors();
const colorNotes = useMenuStore((state) => state.colorNotes);
const isTablet = useSettingStore((state) => state.deviceMode) !== "mobile";
const updater = useRelationStore((state) => state.updater);
const [visible, setVisible] = useState(false);
const note = item;
const renderItem = useCallback(
({ item }: { item: Color }) => (
<ColorItem note={note} key={item.id} item={item} />
),
[note]
);
return (
<>
<ColorPicker visible={visible} setVisible={setVisible} />
<View
style={{
flexGrow: isTablet ? undefined : 1,
paddingRight: 0,
flexDirection: "row"
}}
>
{!colorNotes || !colorNotes.length ? (
<Button
onPress={async () => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
buttonType={{
text: colors.primary.accent
}}
title="Add color"
type="grayBg"
icon="plus"
iconPosition="right"
height={30}
fontSize={SIZE.xs}
style={{
marginRight: 5,
borderRadius: 100,
paddingHorizontal: 8
}}
/>
) : (
<PressableButton
customStyle={{
width: 30,
height: 30,
borderRadius: 100,
justifyContent: "center",
alignItems: "center",
marginRight: 5
}}
type="grayBg"
onPress={() => {
useSettingStore.getState().setSheetKeyboardHandler(false);
setVisible(true);
}}
>
<Icon
testID="icon-plus"
name="plus"
color={colors.primary.icon}
size={SIZE.lg}
/>
</PressableButton>
)}
<FlashList
data={colorNotes}
estimatedItemSize={30}
horizontal
extraData={updater}
renderItem={renderItem}
showsHorizontalScrollIndicator={false}
/>
</View>
</>
);
};

View File

@@ -218,8 +218,7 @@ export const Items = ({ item, buttons, close }) => {
<ScrollView
horizontal
style={{
paddingHorizontal: 12,
paddingVertical: 12
paddingHorizontal: 12
}}
>
{topBarItems.map(renderTopBarItem)}

View File

@@ -64,6 +64,7 @@ export const eSubscribeEvent = <T = unknown>(
eventName: string,
action: EventHandler
) => {
if (!action) return;
return eventManager.subscribe(eventName, action);
};
@@ -71,6 +72,7 @@ export const eUnSubscribeEvent = <T = unknown>(
eventName: string,
action: EventHandler
) => {
if (!action) return;
eventManager.unsubscribe(eventName, action);
};
@@ -132,7 +134,7 @@ export function hideSheet() {
export type ToastOptions = {
heading?: string;
message?: string;
context?: "global" | "local";
context?: any;
type?: "error" | "success" | "info";
duration?: number;
func?: () => void;
@@ -160,7 +162,7 @@ export const ToastManager = {
});
},
hide: () => eSendEvent(eHideToast),
error: (e: Error, title?: string, context?: "global" | "local") => {
error: (e: Error, title?: string, context?: any) => {
ToastManager.show({
heading: title,
message: e?.message || "",