web: add support for custom colors

This commit is contained in:
Abdullah Atta
2023-12-23 15:12:17 +05:00
parent f1f4cd67f7
commit cc07cedd4a
8 changed files with 184 additions and 83 deletions

View File

@@ -17,16 +17,6 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export const COLORS = [
{ key: "red", title: "Red" },
{ key: "orange", title: "Orange" },
{ key: "yellow", title: "Yellow" },
{ key: "green", title: "Green" },
{ key: "blue", title: "Blue" },
{ key: "purple", title: "Purple" },
{ key: "gray", title: "Gray" }
] as const;
export const SUBSCRIPTION_STATUS = {
BASIC: 0,
TRIAL: 1,

View File

@@ -88,6 +88,15 @@ export function showAddTagsDialog(noteIds: string[]) {
));
}
export function showCreateColorDialog() {
return showDialog<"CreateColorDialog", string | undefined>(
"CreateColorDialog",
(Dialog, perform) => (
<Dialog onClose={() => perform(undefined)} onDone={(id) => perform(id)} />
)
);
}
export function showAddNotebookDialog(parentId?: string) {
return showDialog("AddNotebookDialog", (Dialog, perform) => (
<Dialog

View File

@@ -208,7 +208,7 @@ function NavigationMenu(props: NavigationMenuProps) {
title={color.title}
icon={Circle}
selected={location === `/colors/${color.id}`}
color={color.title.toLowerCase()}
color={color.colorCode}
onClick={() => {
_navigate(`/colors/${color.id}`);
}}

View File

@@ -57,6 +57,7 @@ import {
confirm,
showAddReminderDialog,
showAddTagsDialog,
showCreateColorDialog,
showMoveNoteDialog
} from "../../common/dialog-controller";
import { store, useStore } from "../../stores/note-store";
@@ -68,7 +69,6 @@ import { showToast } from "../../utils/toast";
import { hashNavigate, navigate } from "../../navigation";
import { showPublishView } from "../publish-view";
import IconTag from "../icon-tag";
import { COLORS } from "../../common/constants";
import { exportNote, exportNotes, exportToPDF } from "../../common/export";
import { Multiselect } from "../../common/multi-select";
import { store as selectionStore } from "../../stores/selection-store";
@@ -81,13 +81,7 @@ import {
getFormattedReminderTime,
pluralize
} from "@notesnook/common";
import {
Color,
Note,
Notebook as NotebookItem,
Tag,
DefaultColors
} from "@notesnook/core";
import { Color, Note, Notebook as NotebookItem, Tag } from "@notesnook/core";
import { MenuItem } from "@notesnook/ui";
import { Context } from "../list-container/types";
import { SchemeColors } from "@notesnook/theme";
@@ -554,18 +548,38 @@ function colorsToMenuItems(
noteColor: Color | undefined,
ids: string[]
): MenuItem[] {
return COLORS.map((color) => {
const isChecked = !!noteColor && noteColor.title === color.title;
return {
return [
{
key: "new-color",
type: "button",
key: color.key,
title: color.title,
icon: Circle.path,
styles: { icon: { color: DefaultColors[color.key] } },
isChecked,
onClick: () => store.setColor(color, isChecked, ...ids)
} satisfies MenuItem;
});
title: "Add new color",
icon: Plus.path,
onClick: async () => {
const id = await showCreateColorDialog();
if (!id) return;
await store.get().setColor(id, noteColor?.id === id, ...ids);
}
},
{
key: "colors",
type: "lazy-loader",
async items() {
const colors = await db.colors.all.items();
return colors.map((color) => {
const isChecked = !!noteColor && noteColor.id === color.id;
return {
type: "button",
key: color.id,
title: color.title,
icon: Circle.path,
styles: { icon: { color: color.colorCode } },
isChecked,
onClick: () => store.setColor(color.id, isChecked, ...ids)
} satisfies MenuItem;
});
}
}
];
}
function notebooksMenuItems(ids: string[]): MenuItem[] {

View File

@@ -42,8 +42,6 @@ import { PreviewSession } from "../editor/types";
import { ListItemWrapper } from "../list-container/list-profiles";
import { VirtualizedList } from "../virtualized-list";
import { SessionItem } from "../session-item";
import { COLORS } from "../../common/constants";
import { DefaultColors } from "@notesnook/core";
import { VirtualizedTable } from "../virtualized-table";
const tools = [
@@ -204,14 +202,7 @@ export default React.memo(EditorProperties);
function Colors({ noteId }: { noteId: string }) {
const color = useStore((store) => store.color);
const result = usePromise(
async () =>
(
await db.relations.to({ id: noteId, type: "note" }, "color").resolve(1)
).at(0),
[color]
);
console.log(result);
const result = usePromise(() => db.colors.all.items(), [color]);
return (
<Flex
py={2}
@@ -221,37 +212,36 @@ function Colors({ noteId }: { noteId: string }) {
justifyContent: "center"
}}
>
{COLORS.map((label) => {
const isChecked =
result.status === "fulfilled" &&
DefaultColors[label.key] === result.value?.colorCode;
return (
<Flex
key={label.key}
onClick={() => noteStore.get().setColor(label, isChecked, noteId)}
sx={{
cursor: "pointer",
position: "relative",
alignItems: "center",
justifyContent: "space-between"
}}
data-test-id={`properties-${label.key}`}
>
<Circle
size={35}
color={DefaultColors[label.key]}
data-test-id={`toggle-state-${isChecked ? "on" : "off"}`}
/>
{isChecked && (
<Checkmark
color="white"
size={18}
sx={{ position: "absolute", left: "8px" }}
{result.status === "fulfilled" &&
result.value.map((c) => {
const isChecked = c.id === color;
return (
<Flex
key={c.id}
onClick={() => noteStore.get().setColor(c.id, isChecked, noteId)}
sx={{
cursor: "pointer",
position: "relative",
alignItems: "center",
justifyContent: "space-between"
}}
data-test-id={`properties-${c.title}`}
>
<Circle
size={35}
color={c.colorCode}
data-test-id={`toggle-state-${isChecked ? "on" : "off"}`}
/>
)}
</Flex>
);
})}
{isChecked && (
<Checkmark
color="white"
size={18}
sx={{ position: "absolute", left: "8px" }}
/>
)}
</Flex>
);
})}
</Flex>
);
}

View File

@@ -0,0 +1,105 @@
/*
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 { Box, Flex } from "@theme-ui/components";
import Dialog from "../components/dialog";
import Field from "../components/field";
import { Perform } from "../common/dialog-controller";
import { useRef } from "react";
import tinycolor from "tinycolor2";
import { db } from "../common/db";
type CreateColorDialogProps = {
onClose: Perform;
onDone: Perform<string>;
};
function CreateColorDialog(props: CreateColorDialogProps) {
const colorRef = useRef<HTMLInputElement>(null);
const colorPickerRef = useRef<HTMLInputElement>(null);
return (
<Dialog
testId="new-color-dialog"
isOpen={true}
title={"Create a new color"}
onClose={() => props.onClose(false)}
positiveButton={{
form: "colorForm",
type: "submit",
text: "Create"
}}
negativeButton={{ text: "Cancel", onClick: () => props.onClose(false) }}
>
<Box
as="form"
id="colorForm"
onSubmit={async (e) => {
e.preventDefault();
const form = Object.fromEntries(
new FormData(e.target as HTMLFormElement).entries()
) as { color: string; title: string };
const colorId = await db.colors.add({
colorCode: form.color,
title: form.title
});
props.onDone(colorId);
}}
>
<Field
required
label="Title"
id="title"
name="title"
autoFocus
data-test-id="title-input"
/>
<Flex sx={{ alignItems: "end" }}>
<Field
inputRef={colorRef}
required
label="Color"
id="color"
name="color"
data-test-id="color-input"
sx={{ flex: 1 }}
onChange={(e) => {
const color = tinycolor(e.target.value);
if (colorPickerRef.current && color.isValid())
colorPickerRef.current.value = color.toHexString();
}}
/>
<input
ref={colorPickerRef}
type="color"
style={{
height: 41,
backgroundColor: "transparent",
border: "1px solid var(--border)",
borderRadius: 5
}}
onChange={(e) => {
if (colorRef.current) colorRef.current.value = e.target.value;
}}
/>
</Flex>
</Box>
</Dialog>
);
}
export default CreateColorDialog;

View File

@@ -55,6 +55,7 @@ const ThemeDetailsDialog = React.lazy(() => import("./theme-details-dialog"));
const BackupPasswordDialog = React.lazy(
() => import("./backup-password-dialog")
);
const CreateColorDialog = React.lazy(() => import("./create-color-dialog"));
export const Dialogs = {
AddNotebookDialog,
@@ -83,5 +84,6 @@ export const Dialogs = {
AddTagsDialog,
SettingsDialog,
ThemeDetailsDialog,
BackupPasswordDialog
BackupPasswordDialog,
CreateColorDialog
};

View File

@@ -25,7 +25,7 @@ import { store as selectionStore } from "./selection-store";
import Vault from "../common/vault";
import BaseStore from ".";
import Config from "../utils/config";
import { DefaultColors, Note, VirtualizedGrouping } from "@notesnook/core";
import { Note, VirtualizedGrouping } from "@notesnook/core";
import { Context } from "../components/list-container/types";
type ViewMode = "detailed" | "compact";
@@ -129,18 +129,9 @@ class NoteStore extends BaseStore<NoteStore> {
await this.refresh();
};
setColor = async (
color: { key: string; title: string },
isChecked: boolean,
...ids: string[]
) => {
setColor = async (colorId: string, isChecked: boolean, ...ids: string[]) => {
await db.relations.to({ type: "note", ids }, "color").unlink();
if (!isChecked) {
const colorId = await db.colors.add({
title: color.title,
colorCode: DefaultColors[color.key]
});
for (const id of ids) {
await db.relations.add(
{ type: "color", id: colorId },
@@ -149,7 +140,7 @@ class NoteStore extends BaseStore<NoteStore> {
}
}
await appStore.refreshNavItems();
this.syncNoteWithEditor(ids, "color", color.key);
this.syncNoteWithEditor(ids, "color", colorId);
await this.refresh();
};