chore: update user setting appearance (#654)

This commit is contained in:
boojack 2022-12-02 20:00:34 +08:00 committed by GitHub
parent 5451fd2d2c
commit 14f9f29348
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 134 additions and 159 deletions

View File

@ -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)

View File

@ -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 (
<Suspense fallback={<Loading />}>
<RouterProvider router={router} />

View File

@ -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 <Icon.Sun className={className} />;
} else if (apperance === "dark") {
return <Icon.Moon className={className} />;
} else {
return <Icon.Smile className={className} />;
}
};
const handleSelectChange = (appearance: Appearance) => {
const handleSelectChange = async (appearance: Appearance) => {
if (user) {
await userService.upsertUserSetting("appearance", appearance);
}
globalService.setAppearance(appearance);
};

View File

@ -43,7 +43,6 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
toggleShowDatePicker(false);
toImage(memosElRef.current, {
backgroundColor: "#ffffff",
pixelRatio: window.devicePixelRatio * 2,
})
.then((url) => {

View File

@ -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 (
<div className="section-container preferences-section-container">
<p className="title-text">{t("common.basic")}</p>
<label className="form-label selector">
<div className="form-label selector">
<span className="normal-text">{t("common.language")}</span>
<Selector className="ml-2 w-32" value={setting.locale} dataSource={localeSelectorItems} handleValueChanged={handleLocaleChanged} />
</label>
<label className="form-label selector">
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.locale}
onChange={(_, locale) => {
if (locale) {
handleLocaleChanged(locale);
}
}}
startDecorator={<Icon.Globe className="w-4 h-auto" />}
>
{localeSelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</div>
<div className="form-label selector">
<span className="normal-text">Theme</span>
<AppearanceSelect />
</label>
</div>
<p className="title-text">{t("setting.preference")}</p>
<label className="form-label selector">
<div className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>
<Selector
className="ml-2 w-32"
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.memoVisibility}
dataSource={visibilitySelectorItems}
handleValueChanged={handleDefaultMemoVisibilityChanged}
/>
</label>
onChange={(_, visibility) => {
if (visibility) {
handleDefaultMemoVisibilityChanged(visibility);
}
}}
>
{visibilitySelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</div>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-sort-option")}</span>
<Selector
className="ml-2 w-32"
<Select
className="!min-w-[10rem] w-auto text-sm"
value={setting.memoDisplayTsOption}
dataSource={memoDisplayTsOptionSelectorItems}
handleValueChanged={handleMemoDisplayTsOptionChanged}
/>
onChange={(_, value) => {
if (value) {
handleMemoDisplayTsOptionChanged(value);
}
}}
>
{memoDisplayTsOptionSelectorItems.map((item) => (
<Option key={item.value} value={item.value} className="whitespace-nowrap">
{item.text}
</Option>
))}
</Select>
</label>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.enable-folding-memo")}</span>

View File

@ -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: Props) => {
shortcutImgUrl: "",
memoVisibility: propsMemo.visibility,
});
const [appearance] = useAppearance();
const loadingState = useLoading();
const memoElRef = useRef<HTMLDivElement>(null);
const memo = {
@ -72,7 +70,6 @@ const ShareMemoImageDialog: React.FC<Props> = (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: Props) => {
<div className="userinfo-container">
<span className="name-text">{user.nickname || user.username}</span>
<span className="usage-text">
{createdDays} DAYS / {state.memoAmount} MEMOS
{state.memoAmount} MEMOS / {createdDays} DAYS
</span>
</div>
<img className="logo-img" src="/logo.webp" alt="" />

View File

@ -10,6 +10,8 @@ interface StorageData {
editingMemoVisibilityCache: Visibility;
// locale
locale: Locale;
// appearance
appearance: Appearance;
// local setting
localSetting: LocalSetting;
// skipped version

View File

@ -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";
}
}

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;
}
}
}

View File

@ -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;

View File

@ -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);
}
}
},

View File

@ -10,7 +10,7 @@ const globalSlice = createSlice({
name: "global",
initialState: {
locale: "en",
appearance: "system",
appearance: "light",
systemStatus: {
host: undefined,
profile: {

View File

@ -1,4 +1,4 @@
type Appearance = "light" | "dark" | "system";
type Appearance = "light" | "dark";
interface Setting {
locale: Locale;