From 3f0d86292305ebf543c5a149b378f15c49b414b1 Mon Sep 17 00:00:00 2001 From: ahonn Date: Tue, 2 Aug 2022 20:09:58 +0800 Subject: [PATCH] feat: add plugin settings to customize theme colors, #18 includes dark/light mode background color and task section title color --- .eslintrc.json | 1 + package.json | 1 + pnpm-lock.yaml | 20 +++++++++- src/App.tsx | 16 ++++++-- src/components/TaskInput.tsx | 18 +++++++-- src/components/TaskItem.tsx | 33 +++++++++++----- src/components/TaskSection.tsx | 16 +++++--- src/hooks/useAppState.tsx | 71 ++++++++++++++++++++++++++-------- src/hooks/useThemeMode.ts | 35 +++++++++++++++++ src/hooks/useThemeStyle.ts | 31 +++++++++++++++ src/main.tsx | 9 ++++- src/settings.ts | 46 ++++++++++++++++++++++ windi.config.ts | 1 + 13 files changed, 256 insertions(+), 42 deletions(-) create mode 100644 src/hooks/useThemeMode.ts create mode 100644 src/hooks/useThemeStyle.ts create mode 100644 src/settings.ts diff --git a/.eslintrc.json b/.eslintrc.json index 4147e3e..d6356dc 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,4 +1,5 @@ { + "root": true, "extends": [ "eslint:recommended", "plugin:react/recommended", diff --git a/package.json b/package.json index f4e7c02..3fae4a2 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "conventional-changelog-conventionalcommits": "4.6.3", "eslint": "8.13.0", "eslint-plugin-react": "7.29.4", + "eslint-plugin-react-hooks": "^4.6.0", "node-fetch": "3.2.3", "semantic-release": "19.0.2", "typescript": "4.6.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a03c04c..14f4186 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,7 @@ specifiers: dayjs: ^1.11.1 eslint: 8.13.0 eslint-plugin-react: 7.29.4 + eslint-plugin-react-hooks: ^4.6.0 node-fetch: 3.2.3 rc-checkbox: ^2.3.2 react: ^18.0.0 @@ -62,6 +63,7 @@ devDependencies: conventional-changelog-conventionalcommits: 4.6.3 eslint: 8.13.0 eslint-plugin-react: 7.29.4_eslint@8.13.0 + eslint-plugin-react-hooks: 4.6.0_eslint@8.13.0 node-fetch: 3.2.3 semantic-release: 19.0.2 typescript: 4.6.3 @@ -308,6 +310,13 @@ packages: regenerator-runtime: 0.13.9 dev: false + /@babel/runtime/7.18.9: + resolution: {integrity: sha512-lkqXDcvlFT5rvEjiu6+QYO+1GXrEHRo2LOtS7E4GtX5ESIZOgepqsZBVIj6Pv+a6zqsya9VCgiK1KAK4BvJDAw==} + engines: {node: '>=6.9.0'} + dependencies: + regenerator-runtime: 0.13.9 + dev: false + /@babel/template/7.16.7: resolution: {integrity: sha512-I8j/x8kHUrbYRTUxXrrMbfCa7jxkE7tZre39x3kjr9hvI82cK1FfqLygotcWN5kdPGWcLdWMHpSBavse5tWw3w==} engines: {node: '>=6.9.0'} @@ -1759,6 +1768,15 @@ packages: engines: {node: '>=10'} dev: true + /eslint-plugin-react-hooks/4.6.0_eslint@8.13.0: + resolution: {integrity: sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==} + engines: {node: '>=10'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 + dependencies: + eslint: 8.13.0 + dev: true + /eslint-plugin-react/7.29.4_eslint@8.13.0: resolution: {integrity: sha512-CVCXajliVh509PcZYRFyu/BoUEz452+jtQJq2b3Bae4v3xBUWPLCmtmBM+ZinG4MzwmxJgJ2M5rMqhqLVn7MtQ==} engines: {node: '>=4'} @@ -3568,7 +3586,7 @@ packages: /rtl-css-js/1.15.0: resolution: {integrity: sha512-99Cu4wNNIhrI10xxUaABHsdDqzalrSRTie4GeCmbGVuehm4oj+fIy8fTzB+16pmKe8Bv9rl+hxIBez6KxExTew==} dependencies: - '@babel/runtime': 7.17.7 + '@babel/runtime': 7.18.9 dev: false /run-parallel/1.2.0: diff --git a/src/App.tsx b/src/App.tsx index a93de43..301710b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import 'virtual:windi.css'; -import React, { useCallback, useEffect, useRef } from 'react'; +import React, { useEffect, useRef } from 'react'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import dayjs from 'dayjs'; import advancedFormat from 'dayjs/plugin/advancedFormat'; @@ -9,13 +9,14 @@ import TaskSection from './components/TaskSection'; import { logseq as plugin } from '../package.json'; import useAppState, { withAppState } from './hooks/useAppState'; import './style.css'; +import useThemeStyle from './hooks/useThemeStyle'; dayjs.extend(advancedFormat); function ErrorFallback({ error }: FallbackProps) { useEffect(() => { window.logseq.App.showMsg(`[${plugin.id}]: ${error.message}`, 'error'); - }, []); + }, [error.message]); return (
@@ -30,6 +31,7 @@ function App() { const inputRef = useRef(null); const visible = useAppVisible(); const { userConfigs, refresh, tasks } = useAppState(); + const themeStyle = useThemeStyle(); useEffect(() => { if (visible) { @@ -46,7 +48,7 @@ function App() { document.removeEventListener('keydown', keydownHandler); }; } - }, [visible]); + }, [refresh, visible]); const handleClickOutside = (e: React.MouseEvent) => { if (!innerRef.current?.contains(e.target as any)) { @@ -78,7 +80,13 @@ function App() { onClick={handleClickOutside} >
-
+
diff --git a/src/components/TaskInput.tsx b/src/components/TaskInput.tsx index 265e577..ef5eb3b 100644 --- a/src/components/TaskInput.tsx +++ b/src/components/TaskInput.tsx @@ -1,5 +1,6 @@ import React, { useImperativeHandle, useRef } from 'react'; import { CirclePlus } from 'tabler-icons-react'; +import useThemeStyle from '../hooks/useThemeStyle'; export interface ITaskInputRef { focus: () => void; @@ -9,9 +10,13 @@ export interface ITaskInputProps { onCreateTask(content: string): void; } -const TaskInput: React.ForwardRefRenderFunction = (props, ref) => { +const TaskInput: React.ForwardRefRenderFunction< + ITaskInputRef, + ITaskInputProps +> = (props, ref) => { const [content, setContent] = React.useState(''); const inputRef = useRef(null); + const themeStyle = useThemeStyle(); const focus = () => { if (inputRef.current) { @@ -25,12 +30,17 @@ const TaskInput: React.ForwardRefRenderFunction return (
-
- +
+ setContent(e.target.value)} placeholder="Type a task and hit enter" diff --git a/src/components/TaskItem.tsx b/src/components/TaskItem.tsx index 4aaa0a2..7f28ab8 100644 --- a/src/components/TaskItem.tsx +++ b/src/components/TaskItem.tsx @@ -7,15 +7,19 @@ import useTaskManager from '../hooks/useTaskManager'; import { TaskEntityObject, TaskMarker } from '../models/TaskEntity'; import useAppState from '../hooks/useAppState'; import 'rc-checkbox/assets/index.css'; +import useThemeStyle from '../hooks/useThemeStyle'; export interface ITaskItemProps { item: TaskEntityObject; } const TaskItem: React.FC = (props) => { - const { userConfigs: { preferredDateFormat, preferredWorkflow } } = useAppState(); + const { + userConfigs: { preferredDateFormat, preferredWorkflow }, + } = useAppState(); const task = useTaskManager(props.item); const [checked, setChecked] = React.useState(task.completed); + const themeStyle = useThemeStyle(); const isExpiredTask = useMemo(() => { if (!task.scheduled) { @@ -37,11 +41,15 @@ const TaskItem: React.FC = (props) => { const toggleMarker = () => { if (preferredWorkflow === 'now') { - task.setMarker(task.marker === TaskMarker.NOW ? TaskMarker.LATER : TaskMarker.NOW); + task.setMarker( + task.marker === TaskMarker.NOW ? TaskMarker.LATER : TaskMarker.NOW, + ); return; } - task.setMarker(task.marker === TaskMarker.TODO ? TaskMarker.DOING : TaskMarker.TODO); - } + task.setMarker( + task.marker === TaskMarker.TODO ? TaskMarker.DOING : TaskMarker.TODO, + ); + }; const contentClassName = classnames('mb-1 line-clamp-3 cursor-pointer', { 'line-through': checked, @@ -49,7 +57,10 @@ const TaskItem: React.FC = (props) => { }); return ( -
+
= (props) => { className="pt-1 mr-1" />
-
+
- + {task.marker} - - {task.content} - + {task.content}

{task.scheduled && ( diff --git a/src/components/TaskSection.tsx b/src/components/TaskSection.tsx index 44971e1..f747e8e 100644 --- a/src/components/TaskSection.tsx +++ b/src/components/TaskSection.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import useThemeStyle from '../hooks/useThemeStyle'; import { TaskEntityObject } from '../models/TaskEntity'; import TaskItem from './TaskItem'; @@ -9,6 +10,7 @@ export interface ITaskSectionProps { const TaskSection: React.FC = (props) => { const { title, tasks } = props; + const themeStyle = useThemeStyle(); if (tasks.length === 0) { return null; @@ -16,13 +18,17 @@ const TaskSection: React.FC = (props) => { return (

-

{title}

+

+ {title} +

{tasks.map((task) => ( - + ))}
diff --git a/src/hooks/useAppState.tsx b/src/hooks/useAppState.tsx index e377f8c..07f7f2c 100644 --- a/src/hooks/useAppState.tsx +++ b/src/hooks/useAppState.tsx @@ -1,5 +1,5 @@ import { AppUserConfigs } from '@logseq/libs/dist/LSPlugin.user'; -import React, { useCallback, useContext, useMemo } from 'react'; +import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'; import { TaskEntityObject } from '../models/TaskEntity'; import getAnytimeTaskQuery from '../querys/anytime'; import getScheduledTaskQuery from '../querys/scheduled'; @@ -8,22 +8,39 @@ import useTaskQuery from './useTaskQuery'; import useUserConfigs, { DEFAULT_USER_CONFIGS } from './useUserConfigs'; export interface IAppState { - userConfigs: Partial, + userConfigs: Partial; + settings: { + lightPrimaryBackgroundColor: string; + lightSecondaryBackgroundColor: string; + darkPrimaryBackgroundColor: string; + darkSecondaryBackgroundColor: string; + sectionTitleColor: string; + }; tasks: { - today: TaskEntityObject[], - scheduled: TaskEntityObject[], - anytime: TaskEntityObject[], - }, - refresh(): void, + today: TaskEntityObject[]; + scheduled: TaskEntityObject[]; + anytime: TaskEntityObject[]; + }; + refresh(): void; } +const DEFAULT_SETTINGS = { + sectionTitleColor: '#0a0a0a', + lightPrimaryBackgroundColor: '#ffffff', + lightSecondaryBackgroundColor: '#f7f7f7', + darkPrimaryBackgroundColor: '#002B37', + darkSecondaryBackgroundColor: '#106ba3', +}; + const AppStateContext = React.createContext({ userConfigs: DEFAULT_USER_CONFIGS, + settings: DEFAULT_SETTINGS, tasks: { today: [], scheduled: [], anytime: [], }, + // eslint-disable-next-line @typescript-eslint/no-empty-function refresh: () => {}, }); @@ -32,10 +49,14 @@ const useAppState = () => { return appState; }; +// eslint-disable-next-line @typescript-eslint/ban-types export const withAppState =

( WrapComponent: React.ComponentType

, ) => { const WithAppState: typeof WrapComponent = (props) => { + const [settings, setSettings] = useState( + (logseq.settings as unknown as IAppState['settings']) ?? DEFAULT_SETTINGS, + ); const userConfigs = useUserConfigs(); const todayTask = useTaskQuery(getTodayTaskQuery()); const scheduledTask = useTaskQuery(getScheduledTaskQuery()); @@ -47,15 +68,33 @@ export const withAppState =

( anytimeTask.mutate(); }, [todayTask, scheduledTask, anytimeTask]); - const state = useMemo(() => ({ - userConfigs, - tasks: { - today: todayTask.data || [], - scheduled: scheduledTask.data || [], - anytime: anytimeTask.data || [], - }, - refresh, - }), [userConfigs, todayTask, scheduledTask, anytimeTask, refresh]); + const state = useMemo( + () => ({ + userConfigs, + settings, + tasks: { + today: todayTask.data || [], + scheduled: scheduledTask.data || [], + anytime: anytimeTask.data || [], + }, + refresh, + }), + [ + userConfigs, + settings, + todayTask.data, + scheduledTask.data, + anytimeTask.data, + refresh, + ], + ); + + useEffect(() => { + const unlisten = logseq.onSettingsChanged((newSettings) => { + setSettings(newSettings); + }); + return () => unlisten(); + }, []); return ( diff --git a/src/hooks/useThemeMode.ts b/src/hooks/useThemeMode.ts new file mode 100644 index 0000000..09e994f --- /dev/null +++ b/src/hooks/useThemeMode.ts @@ -0,0 +1,35 @@ +import { useEffect, useState } from "react"; +import useUserConfigs from "./useUserConfigs"; + +const useThemeMode = () => { + const userConfigs = useUserConfigs(); + const [themeMode, setThemeMode] = useState(userConfigs.preferredThemeMode); + const isLightMode = themeMode === "light"; + const isDarkMode = themeMode === "dark"; + + useEffect(() => { + setThemeMode(userConfigs.preferredThemeMode); + }, [userConfigs.preferredThemeMode]); + + useEffect(() => { + logseq.App.onThemeModeChanged((evt) => { + setThemeMode(evt.mode); + }); + }, []); + + useEffect(() => { + if (isDarkMode) { + document.documentElement.classList.add('dark'); + } else { + document.documentElement.classList.add('light'); + } + }, [isDarkMode]); + + return { + themeMode, + isLightMode, + isDarkMode, + }; +}; + +export default useThemeMode; diff --git a/src/hooks/useThemeStyle.ts b/src/hooks/useThemeStyle.ts new file mode 100644 index 0000000..eb40fd9 --- /dev/null +++ b/src/hooks/useThemeStyle.ts @@ -0,0 +1,31 @@ +import { useMemo } from 'react'; +import useAppState from './useAppState'; +import useThemeMode from './useThemeMode'; + +const useThemeStyle = () => { + const { settings } = useAppState(); + const { isLightMode } = useThemeMode(); + + const primaryBackgroundColor = useMemo( + () => + isLightMode + ? settings.lightPrimaryBackgroundColor + : settings.darkPrimaryBackgroundColor, + [isLightMode, settings], + ); + const secondaryBackgroundColor = useMemo( + () => + isLightMode + ? settings.lightSecondaryBackgroundColor + : settings.darkSecondaryBackgroundColor, + [isLightMode, settings], + ); + + return { + primaryBackgroundColor, + secondaryBackgroundColor, + sectionTitleColor: settings.sectionTitleColor, + }; +}; + +export default useThemeStyle; diff --git a/src/main.tsx b/src/main.tsx index cd3f2eb..25747c2 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -3,6 +3,7 @@ import React from 'react'; import * as ReactDOM from 'react-dom/client'; import { logseq as plugin } from '../package.json'; import App from './App'; +import settings from './settings'; async function openTaskPanel() { const rect = await logseq.App.queryElementRect('#' + plugin.id); @@ -45,7 +46,7 @@ function main() { key: 'logseq-plugin-todo', label: 'Quick open task panel', keybinding: { - binding: "mod+shift+t", + binding: 'mod+shift+t', }, }, () => { @@ -68,4 +69,8 @@ function main() { } } -logseq.ready(createModel()).then(main).catch(console.error); +logseq + .useSettingsSchema(settings) + .ready(createModel()) + .then(main) + .catch(console.error); diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 0000000..76091a6 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,46 @@ +import { SettingSchemaDesc } from "@logseq/libs/dist/LSPlugin"; + +const settings: SettingSchemaDesc[] = [ + { + key: 'sectionTitleColor', + type: 'string', + title: 'The title color', + description: 'color of task section title!', + default: '#106ba3', + inputAs: 'color' + }, + { + key: 'lightPrimaryBackgroundColor', + type: 'string', + title: 'The primary background color (light mode)', + description: '🌝 primary color of light mode!', + default: '#ffffff', + inputAs: 'color' + }, + { + key: 'lightSecondaryBackgroundColor', + type: 'string', + title: 'The secondary background color (light mode)', + description: '🌝 secondray color of light mode!', + default: '#f7f7f7', + inputAs: 'color' + }, + { + key: 'darkPrimaryBackgroundColor', + type: 'string', + title: 'The primary background color (dark mode)', + description: '🌚 primary color of dark mode!', + default: '#023643', + inputAs: 'color' + }, + { + key: 'darkSecondaryBackgroundColor', + type: 'string', + title: 'The secondary background color (dark mode)', + description: '🌚 secondary color of dark mode!', + default: '#002B37', + inputAs: 'color' + } +]; + +export default settings; diff --git a/windi.config.ts b/windi.config.ts index 9f4af17..3ee79f0 100644 --- a/windi.config.ts +++ b/windi.config.ts @@ -1,4 +1,5 @@ export default { + darkMode: 'class', plugins: [ require('windicss/plugin/line-clamp'), ],