diff --git a/api/user_setting.go b/api/user_setting.go index eb0ddfc..cd02e07 100644 --- a/api/user_setting.go +++ b/api/user_setting.go @@ -10,6 +10,8 @@ type UserSettingKey string const ( // UserSettingLocaleKey is the key type for user locale. UserSettingLocaleKey UserSettingKey = "locale" + // UserSettingAppearanceKey is the key type for user appearance. + UserSettingAppearanceKey UserSettingKey = "appearance" // UserSettingMemoVisibilityKey is the key type for user preference memo default visibility. UserSettingMemoVisibilityKey UserSettingKey = "memoVisibility" // UserSettingMemoDisplayTsOptionKey is the key type for memo display ts option. @@ -21,6 +23,8 @@ func (key UserSettingKey) String() string { switch key { case UserSettingLocaleKey: return "locale" + case UserSettingAppearanceKey: + return "appearance" case UserSettingMemoVisibilityKey: return "memoVisibility" case UserSettingMemoDisplayTsOptionKey: @@ -31,8 +35,8 @@ func (key UserSettingKey) String() string { var ( UserSettingLocaleValue = []string{"en", "zh", "vi", "fr"} + UserSettingAppearanceValue = []string{"light", "dark"} UserSettingMemoVisibilityValue = []Visibility{Private, Protected, Public} - UserSettingEditorFontStyleValue = []string{"normal", "mono"} UserSettingMemoDisplayTsOptionKeyValue = []string{"created_ts", "updated_ts"} ) @@ -67,6 +71,23 @@ func (upsert UserSettingUpsert) Validate() error { if invalid { return fmt.Errorf("invalid user setting locale value") } + } else if upsert.Key == UserSettingAppearanceKey { + appearanceValue := "light" + err := json.Unmarshal([]byte(upsert.Value), &appearanceValue) + if err != nil { + return fmt.Errorf("failed to unmarshal user setting appearance value") + } + + invalid := true + for _, value := range UserSettingAppearanceValue { + if appearanceValue == value { + invalid = false + break + } + } + if invalid { + return fmt.Errorf("invalid user setting appearance value") + } } else if upsert.Key == UserSettingMemoVisibilityKey { memoVisibilityValue := Private err := json.Unmarshal([]byte(upsert.Value), &memoVisibilityValue) diff --git a/web/src/App.tsx b/web/src/App.tsx index a99b88b..ec36bf0 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -6,12 +6,12 @@ import { useAppSelector } from "./store"; import Loading from "./pages/Loading"; import router from "./router"; import * as storage from "./helpers/storage"; -import useAppearance from "./hooks/useAppearance"; +import { useColorScheme } from "@mui/joy"; function App() { const { i18n } = useTranslation(); - const { locale, systemStatus } = useAppSelector((state) => state.global); - useAppearance(); + const { appearance, locale, systemStatus } = useAppSelector((state) => state.global); + const { setMode } = useColorScheme(); useEffect(() => { locationService.updateStateWithLocation(); @@ -42,6 +42,19 @@ function App() { }); }, [locale]); + useEffect(() => { + const root = document.documentElement; + if (appearance === "light") { + root.classList.remove("dark"); + } else if (appearance === "dark") { + root.classList.add("dark"); + } + setMode(appearance); + storage.set({ + appearance: appearance, + }); + }, [appearance]); + return ( }> diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx index e6210be..371e18e 100644 --- a/web/src/components/AppearanceSelect.tsx +++ b/web/src/components/AppearanceSelect.tsx @@ -1,12 +1,13 @@ import { Option, Select } from "@mui/joy"; import { useTranslation } from "react-i18next"; -import { globalService } from "../services"; +import { globalService, userService } from "../services"; import { useAppSelector } from "../store"; import Icon from "./Icon"; -const appearanceList = ["system", "light", "dark"]; +const appearanceList = ["light", "dark"]; const AppearanceSelect = () => { + const user = useAppSelector((state) => state.user.user); const appearance = useAppSelector((state) => state.global.appearance); const { t } = useTranslation(); @@ -16,12 +17,13 @@ const AppearanceSelect = () => { return ; } else if (apperance === "dark") { return ; - } else { - return ; } }; - const handleSelectChange = (appearance: Appearance) => { + const handleSelectChange = async (appearance: Appearance) => { + if (user) { + await userService.upsertUserSetting("appearance", appearance); + } globalService.setAppearance(appearance); }; diff --git a/web/src/components/DailyReviewDialog.tsx b/web/src/components/DailyReviewDialog.tsx index d938fea..4ae4586 100644 --- a/web/src/components/DailyReviewDialog.tsx +++ b/web/src/components/DailyReviewDialog.tsx @@ -43,7 +43,6 @@ const DailyReviewDialog: React.FC = (props: Props) => { toggleShowDatePicker(false); toImage(memosElRef.current, { - backgroundColor: "#ffffff", pixelRatio: window.devicePixelRatio * 2, }) .then((url) => { diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 1f3285c..fa2cace 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -1,9 +1,9 @@ +import { Select, Switch, Option } from "@mui/joy"; import { useTranslation } from "react-i18next"; -import Switch from "@mui/joy/Switch"; import { globalService, userService } from "../../services"; import { useAppSelector } from "../../store"; import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts"; -import Selector from "../common/Selector"; +import Icon from "../Icon"; import AppearanceSelect from "../AppearanceSelect"; import "../../less/settings/preferences-section.less"; @@ -63,32 +63,65 @@ const PreferencesSection = () => { return ( {t("common.basic")} - + {t("common.language")} - - - + { + if (locale) { + handleLocaleChanged(locale); + } + }} + startDecorator={} + > + {localeSelectorItems.map((item) => ( + + {item.text} + + ))} + + + Theme - + {t("setting.preference")} - + {t("setting.preference-section.default-memo-visibility")} - - + onChange={(_, visibility) => { + if (visibility) { + handleDefaultMemoVisibilityChanged(visibility); + } + }} + > + {visibilitySelectorItems.map((item) => ( + + {item.text} + + ))} + + {t("setting.preference-section.default-memo-sort-option")} - + onChange={(_, value) => { + if (value) { + handleMemoDisplayTsOptionChanged(value); + } + }} + > + {memoDisplayTsOptionSelectorItems.map((item) => ( + + {item.text} + + ))} + {t("setting.preference-section.enable-folding-memo")} diff --git a/web/src/components/ShareMemoImageDialog.tsx b/web/src/components/ShareMemoImageDialog.tsx index 941aa78..48f7856 100644 --- a/web/src/components/ShareMemoImageDialog.tsx +++ b/web/src/components/ShareMemoImageDialog.tsx @@ -14,7 +14,6 @@ import toastHelper from "./Toast"; import MemoContent from "./MemoContent"; import MemoResources from "./MemoResources"; import Selector from "./common/Selector"; -import useAppearance from "../hooks/useAppearance"; import "../less/share-memo-image-dialog.less"; interface Props extends DialogProps { @@ -36,7 +35,6 @@ const ShareMemoImageDialog: React.FC = (props: Props) => { shortcutImgUrl: "", memoVisibility: propsMemo.visibility, }); - const [appearance] = useAppearance(); const loadingState = useLoading(); const memoElRef = useRef(null); const memo = { @@ -72,7 +70,6 @@ const ShareMemoImageDialog: React.FC = (props: Props) => { } toImage(memoElRef.current, { - backgroundColor: appearance === "light" ? "#f4f4f5" : "#27272a", pixelRatio: window.devicePixelRatio * 2, }) .then((url) => { @@ -147,7 +144,7 @@ const ShareMemoImageDialog: React.FC = (props: Props) => { {user.nickname || user.username} - {createdDays} DAYS / {state.memoAmount} MEMOS + {state.memoAmount} MEMOS / {createdDays} DAYS diff --git a/web/src/helpers/storage.ts b/web/src/helpers/storage.ts index 1b3e584..bf615d1 100644 --- a/web/src/helpers/storage.ts +++ b/web/src/helpers/storage.ts @@ -10,6 +10,8 @@ interface StorageData { editingMemoVisibilityCache: Visibility; // locale locale: Locale; + // appearance + appearance: Appearance; // local setting localSetting: LocalSetting; // skipped version diff --git a/web/src/helpers/utils.ts b/web/src/helpers/utils.ts index 90dfc57..0316d77 100644 --- a/web/src/helpers/utils.ts +++ b/web/src/helpers/utils.ts @@ -140,3 +140,11 @@ export function absolutifyLink(rel: string): string { anchor.setAttribute("href", rel); return anchor.href; } + +export function getSystemColorScheme() { + if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { + return "dark"; + } else { + return "light"; + } +} diff --git a/web/src/hooks/useAppearance.ts b/web/src/hooks/useAppearance.ts deleted file mode 100644 index 0a82753..0000000 --- a/web/src/hooks/useAppearance.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useEffect } from "react"; -import { useColorScheme } from "@mui/joy/styles"; -import { useAppSelector } from "../store"; -import { globalService } from "../services"; - -const getSystemColorScheme = () => { - if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { - return "dark"; - } else { - return "light"; - } -}; - -const useAppearance = () => { - const user = useAppSelector((state) => state.user.user); - const appearance = useAppSelector((state) => state.global.appearance); - const { mode, setMode } = useColorScheme(); - - useEffect(() => { - if (user) { - globalService.setAppearance(user.setting.appearance); - } - }, [user]); - - useEffect(() => { - let mode = appearance; - if (appearance === "system") { - mode = getSystemColorScheme(); - } - setMode(mode); - }, [appearance]); - - useEffect(() => { - const colorSchemeChangeHandler = (event: MediaQueryListEvent) => { - const newColorScheme = event.matches ? "dark" : "light"; - if (globalService.getState().appearance === "system") { - setMode(newColorScheme); - } - }; - - if (appearance !== "system") { - window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", colorSchemeChangeHandler); - return; - } - - window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", colorSchemeChangeHandler); - - return () => { - window.matchMedia("(prefers-color-scheme: dark)").removeEventListener("change", colorSchemeChangeHandler); - }; - }, [appearance]); - - useEffect(() => { - const root = document.documentElement; - if (mode === "dark") { - root.classList.add("dark"); - } else if (mode === "light") { - root.classList.remove("dark"); - } - }, [mode]); - - return [appearance, globalService.setAppearance] as const; -}; - -export default useAppearance; diff --git a/web/src/hooks/useMediaQuery.ts b/web/src/hooks/useMediaQuery.ts deleted file mode 100644 index ecc22f2..0000000 --- a/web/src/hooks/useMediaQuery.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { useState, useEffect } from "react"; - -const useMediaQuery = (query: string) => { - const [matches, setMatches] = useState(false); - - useEffect(() => { - const media = window.matchMedia(query); - if (media.matches !== matches) { - setMatches(media.matches); - } - const listener = () => { - setMatches(media.matches); - }; - media.addEventListener("change", listener); - return () => media.removeEventListener("change", listener); - }, [query, matches]); - - return matches; -}; - -export default useMediaQuery; diff --git a/web/src/hooks/useQuery.ts b/web/src/hooks/useQuery.ts deleted file mode 100644 index 70d0eda..0000000 --- a/web/src/hooks/useQuery.ts +++ /dev/null @@ -1,13 +0,0 @@ -// A custom hook that builds on useLocation to parse - -import React from "react"; -import { useLocation } from "react-router-dom"; - -// the query string for you. -const useQuery = () => { - const { search } = useLocation(); - - return React.useMemo(() => new URLSearchParams(search), [search]); -}; - -export default useQuery; diff --git a/web/src/less/daily-review-dialog.less b/web/src/less/daily-review-dialog.less index 21daef2..f2fa1ca 100644 --- a/web/src/less/daily-review-dialog.less +++ b/web/src/less/daily-review-dialog.less @@ -2,7 +2,7 @@ @apply p-0 sm:py-16; > .dialog-container { - @apply w-full sm:w-112 max-w-full grow sm:grow-0 p-0 rounded-none sm:rounded-lg; + @apply w-full sm:w-112 max-w-full grow sm:grow-0 p-0 pb-4 rounded-none sm:rounded-lg; > .dialog-header-container { @apply relative flex flex-row justify-between items-center w-full p-6 pb-0 mb-0; @@ -33,7 +33,7 @@ } > .dialog-content-container { - @apply w-full h-auto flex flex-col justify-start items-start p-6 pb-0; + @apply w-full h-auto flex flex-col justify-start items-start p-6 pb-0 bg-white dark:bg-zinc-800; > .date-card-container { @apply flex flex-col justify-center items-center m-auto pb-6 select-none; diff --git a/web/src/less/share-memo-image-dialog.less b/web/src/less/share-memo-image-dialog.less index 23c67b9..b799598 100644 --- a/web/src/less/share-memo-image-dialog.less +++ b/web/src/less/share-memo-image-dialog.less @@ -1,6 +1,6 @@ .share-memo-image-dialog { > .dialog-container { - @apply w-96 p-0 bg-zinc-100; + @apply w-96 p-0 bg-white dark:bg-zinc-800; > .dialog-header-container { @apply py-2 pt-4 px-4 pl-6 mb-0 rounded-t-lg; @@ -35,7 +35,7 @@ } > .memo-container { - @apply w-96 max-w-full h-auto select-none relative flex flex-col justify-start items-start; + @apply w-96 max-w-full h-auto select-none relative flex flex-col justify-start items-start bg-white dark:bg-zinc-800; > .memo-shortcut-img { @apply absolute top-0 left-0 w-full h-auto z-10; @@ -50,7 +50,7 @@ } > .images-container { - @apply w-full h-auto flex flex-col justify-start items-start px-6 pb-2 bg-white; + @apply w-full h-auto flex flex-col justify-start items-start px-6 pb-2; > img { @apply w-full h-auto mb-2 rounded; @@ -58,30 +58,22 @@ } > .watermark-container { - @apply flex flex-row justify-between items-center w-full dark:bg-zinc-900 py-2 px-6; - - > .normal-text { - @apply w-full flex flex-row justify-start items-center text-sm leading-6 text-gray-500; - - > .name-text { - @apply text-black; - } - } + @apply flex flex-row justify-between items-center w-full bg-gray-100 dark:bg-zinc-700 py-2 px-6; > .userinfo-container { @apply w-64 flex flex-col justify-center items-start; > .name-text { - @apply text-lg truncate font-medium text-gray-600; + @apply text-sm truncate font-bold text-gray-600 dark:text-gray-300; } > .usage-text { - @apply -mt-1 text-sm text-gray-400; + @apply text-xs text-gray-400; } } > .logo-img { - @apply h-12 w-auto; + @apply h-10 w-auto; } } } diff --git a/web/src/services/globalService.ts b/web/src/services/globalService.ts index 492f5f2..10f442b 100644 --- a/web/src/services/globalService.ts +++ b/web/src/services/globalService.ts @@ -11,7 +11,7 @@ const globalService = { initialState: async () => { const defaultGlobalState = { locale: "en" as Locale, - appearance: "system" as Appearance, + appearance: "light" as Appearance, systemStatus: { allowSignUp: false, additionalStyle: "", @@ -19,10 +19,13 @@ const globalService = { } as SystemStatus, }; - const { locale: storageLocale } = storage.get(["locale"]); + const { locale: storageLocale, appearance: storageAppearance } = storage.get(["locale", "appearance"]); if (storageLocale) { defaultGlobalState.locale = storageLocale; } + if (storageAppearance) { + defaultGlobalState.appearance = storageAppearance; + } try { const { data } = (await api.getSystemStatus()).data; diff --git a/web/src/services/userService.ts b/web/src/services/userService.ts index f61cdf6..1466ce4 100644 --- a/web/src/services/userService.ts +++ b/web/src/services/userService.ts @@ -3,12 +3,12 @@ import * as api from "../helpers/api"; import * as storage from "../helpers/storage"; import { UNKNOWN_ID } from "../helpers/consts"; import store from "../store"; -import { setLocale } from "../store/modules/global"; import { setUser, patchUser, setHost, setOwner } from "../store/modules/user"; +import { getSystemColorScheme } from "../helpers/utils"; const defaultSetting: Setting = { locale: "en", - appearance: "system", + appearance: getSystemColorScheme(), memoVisibility: "PRIVATE", memoDisplayTsOption: "created_ts", }; @@ -61,11 +61,15 @@ const userService = { } } - const { data: user } = (await api.getMyselfUser()).data; - if (user) { - store.dispatch(setUser(convertResponseModelUser(user))); + const { data } = (await api.getMyselfUser()).data; + if (data) { + const user = convertResponseModelUser(data); + store.dispatch(setUser(user)); if (user.setting.locale) { - store.dispatch(setLocale(user.setting.locale)); + globalService.setLocale(user.setting.locale); + } + if (user.setting.appearance) { + globalService.setAppearance(user.setting.appearance); } } }, diff --git a/web/src/store/modules/global.ts b/web/src/store/modules/global.ts index 492b961..7c5ecd9 100644 --- a/web/src/store/modules/global.ts +++ b/web/src/store/modules/global.ts @@ -10,7 +10,7 @@ const globalSlice = createSlice({ name: "global", initialState: { locale: "en", - appearance: "system", + appearance: "light", systemStatus: { host: undefined, profile: { diff --git a/web/src/types/modules/setting.d.ts b/web/src/types/modules/setting.d.ts index abf07b8..0468941 100644 --- a/web/src/types/modules/setting.d.ts +++ b/web/src/types/modules/setting.d.ts @@ -1,4 +1,4 @@ -type Appearance = "light" | "dark" | "system"; +type Appearance = "light" | "dark"; interface Setting { locale: Locale;
{t("common.basic")}
{t("setting.preference")}