mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
mobile: allow custom colors
This commit is contained in:
committed by
Abdullah Atta
parent
ceb948bd1f
commit
f1f4cd67f7
@@ -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
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
302
apps/mobile/app/components/dialogs/color-picker/index.tsx
Normal file
302
apps/mobile/app/components/dialogs/color-picker/index.tsx
Normal 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;
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -218,8 +218,7 @@ export const Items = ({ item, buttons, close }) => {
|
||||
<ScrollView
|
||||
horizontal
|
||||
style={{
|
||||
paddingHorizontal: 12,
|
||||
paddingVertical: 12
|
||||
paddingHorizontal: 12
|
||||
}}
|
||||
>
|
||||
{topBarItems.map(renderTopBarItem)}
|
||||
|
||||
@@ -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 || "",
|
||||
|
||||
Reference in New Issue
Block a user