mirror of
https://github.com/usememos/memos.git
synced 2025-02-20 21:30:55 +01:00
chore: update user setting appearance (#654)
This commit is contained in:
parent
5451fd2d2c
commit
14f9f29348
@ -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)
|
||||
|
@ -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} />
|
||||
|
@ -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);
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,6 @@ const DailyReviewDialog: React.FC<Props> = (props: Props) => {
|
||||
toggleShowDatePicker(false);
|
||||
|
||||
toImage(memosElRef.current, {
|
||||
backgroundColor: "#ffffff",
|
||||
pixelRatio: window.devicePixelRatio * 2,
|
||||
})
|
||||
.then((url) => {
|
||||
|
@ -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>
|
||||
|
@ -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="" />
|
||||
|
@ -10,6 +10,8 @@ interface StorageData {
|
||||
editingMemoVisibilityCache: Visibility;
|
||||
// locale
|
||||
locale: Locale;
|
||||
// appearance
|
||||
appearance: Appearance;
|
||||
// local setting
|
||||
localSetting: LocalSetting;
|
||||
// skipped version
|
||||
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -10,7 +10,7 @@ const globalSlice = createSlice({
|
||||
name: "global",
|
||||
initialState: {
|
||||
locale: "en",
|
||||
appearance: "system",
|
||||
appearance: "light",
|
||||
systemStatus: {
|
||||
host: undefined,
|
||||
profile: {
|
||||
|
2
web/src/types/modules/setting.d.ts
vendored
2
web/src/types/modules/setting.d.ts
vendored
@ -1,4 +1,4 @@
|
||||
type Appearance = "light" | "dark" | "system";
|
||||
type Appearance = "light" | "dark";
|
||||
|
||||
interface Setting {
|
||||
locale: Locale;
|
||||
|
Loading…
x
Reference in New Issue
Block a user