feat: update appearance selector (#645)

This commit is contained in:
boojack 2022-12-01 20:57:19 +08:00 committed by GitHub
parent eaebc6dcef
commit 7c6d7226f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 157 additions and 118 deletions

View File

@ -61,9 +61,6 @@ func NewServer(profile *profile.Profile) *Server {
rootGroup := e.Group("")
s.registerRSSRoutes(rootGroup)
webhookGroup := e.Group("/h")
s.registerResourcePublicRoutes(webhookGroup)
publicGroup := e.Group("/o")
s.registerResourcePublicRoutes(publicGroup)
s.registerGetterPublicRoutes(publicGroup)

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 useApperance from "./hooks/useApperance";
import useAppearance from "./hooks/useAppearance";
function App() {
const { i18n } = useTranslation();
const { locale, systemStatus } = useAppSelector((state) => state.global);
useApperance();
useAppearance();
useEffect(() => {
locationService.updateStateWithLocation();

View File

@ -1,14 +1,16 @@
import { Option, Select } from "@mui/joy";
import { useTranslation } from "react-i18next";
import { globalService } from "../services";
import { useAppSelector } from "../store";
import Icon from "./Icon";
import { APPERANCE_OPTIONS } from "../helpers/consts";
import useApperance, { Apperance } from "../hooks/useApperance";
const ApperanceSelect = () => {
const [apperance, setApperance] = useApperance();
const appearanceList = ["system", "light", "dark"];
const AppearanceSelect = () => {
const appearance = useAppSelector((state) => state.global.appearance);
const { t } = useTranslation();
const getPrefixIcon = (apperance: Apperance) => {
const getPrefixIcon = (apperance: Appearance) => {
const className = "w-4 h-auto";
if (apperance === "light") {
return <Icon.Sun className={className} />;
@ -19,16 +21,22 @@ const ApperanceSelect = () => {
}
};
const handleSelectChange = (appearance: Appearance) => {
globalService.setAppearance(appearance);
};
return (
<Select
className="!min-w-[10rem] w-auto text-sm"
value={apperance}
onChange={(_, value) => {
setApperance(value as Apperance);
value={appearance}
onChange={(_, appearance) => {
if (appearance) {
handleSelectChange(appearance);
}
}}
startDecorator={getPrefixIcon(apperance)}
startDecorator={getPrefixIcon(appearance)}
>
{APPERANCE_OPTIONS.map((item) => (
{appearanceList.map((item) => (
<Option key={item} value={item} className="whitespace-nowrap">
{t(`setting.apperance-option.${item}`)}
</Option>
@ -37,4 +45,4 @@ const ApperanceSelect = () => {
);
};
export default ApperanceSelect;
export default AppearanceSelect;

View File

@ -82,6 +82,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
<p className="text-sm mb-1">{t("common.new-password")}</p>
<input
type="password"
autoComplete="new-password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPassword}
@ -90,6 +91,7 @@ const ChangePasswordDialog: React.FC<Props> = ({ destroy }: Props) => {
<p className="text-sm mb-1 mt-2">{t("common.repeat-new-password")}</p>
<input
type="password"
autoComplete="new-password"
className="input-text"
placeholder={t("common.repeat-new-password")}
value={newPasswordAgain}

View File

@ -34,7 +34,14 @@ const SearchBar = () => {
<div className="search-bar-container">
<div className="search-bar-inputer">
<Icon.Search className="icon-img" />
<input className="text-input" autoComplete="off" type="text" placeholder="" value={queryText} onChange={handleTextQueryInput} />
<input
className="text-input"
autoComplete="new-password"
type="text"
placeholder=""
value={queryText}
onChange={handleTextQueryInput}
/>
</div>
<div className="quickly-action-wrapper">
<div className="quickly-action-container">

View File

@ -12,7 +12,6 @@ import "../../less/settings/member-section.less";
interface State {
createUserUsername: string;
createUserPassword: string;
repeatUserPassword: string;
}
const PreferencesSection = () => {
@ -21,7 +20,6 @@ const PreferencesSection = () => {
const [state, setState] = useState<State>({
createUserUsername: "",
createUserPassword: "",
repeatUserPassword: "",
});
const [userList, setUserList] = useState<User[]>([]);
@ -48,22 +46,11 @@ const PreferencesSection = () => {
});
};
const handleRepeatPasswordInputChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setState({
...state,
repeatUserPassword: event.target.value,
});
};
const handleCreateUserBtnClick = async () => {
if (state.createUserUsername === "" || state.createUserPassword === "") {
toastHelper.error(t("message.fill-form"));
return;
}
if (state.createUserPassword !== state.repeatUserPassword) {
toastHelper.error(t("message.password-not-match"));
return;
}
const userCreate: UserCreate = {
username: state.createUserUsername,
@ -80,7 +67,6 @@ const PreferencesSection = () => {
setState({
createUserUsername: "",
createUserPassword: "",
repeatUserPassword: "",
});
};
@ -131,19 +117,22 @@ const PreferencesSection = () => {
<div className="create-member-container">
<div className="input-form-container">
<span className="field-text">{t("common.username")}</span>
<input type="text" placeholder={t("common.username")} value={state.createUserUsername} onChange={handleUsernameInputChange} />
<input
type="text"
autoComplete="new-password"
placeholder={t("common.username")}
value={state.createUserUsername}
onChange={handleUsernameInputChange}
/>
</div>
<div className="input-form-container">
<span className="field-text">{t("common.password")}</span>
<input type="password" placeholder={t("common.password")} value={state.createUserPassword} onChange={handlePasswordInputChange} />
</div>
<div className="input-form-container">
<span className="field-text">{t("common.repeat-password-short")}</span>
<input
type="password"
placeholder={t("common.repeat-password")}
value={state.repeatUserPassword}
onChange={handleRepeatPasswordInputChange}
autoComplete="new-password"
placeholder={t("common.password")}
value={state.createUserPassword}
onChange={handlePasswordInputChange}
/>
</div>
<div className="btns-container">

View File

@ -4,6 +4,7 @@ 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 AppearanceSelect from "../AppearanceSelect";
import "../../less/settings/preferences-section.less";
const localeSelectorItems = [
@ -66,6 +67,10 @@ const PreferencesSection = () => {
<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">
<span className="normal-text">Theme</span>
<AppearanceSelect />
</label>
<p className="title-text">{t("setting.preference")}</p>
<label className="form-label selector">
<span className="normal-text">{t("setting.preference-section.default-memo-visibility")}</span>

View File

@ -14,8 +14,8 @@ 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";
import useApperance from "../hooks/useApperance";
interface Props extends DialogProps {
memo: Memo;
@ -36,7 +36,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
shortcutImgUrl: "",
memoVisibility: propsMemo.visibility,
});
const [apperance] = useApperance();
const [appearance] = useAppearance();
const loadingState = useLoading();
const memoElRef = useRef<HTMLDivElement>(null);
const memo = {
@ -72,7 +72,7 @@ const ShareMemoImageDialog: React.FC<Props> = (props: Props) => {
}
toImage(memoElRef.current, {
backgroundColor: apperance === "light" ? "#f4f4f5" : "#27272a",
backgroundColor: appearance === "light" ? "#f4f4f5" : "#27272a",
pixelRatio: window.devicePixelRatio * 2,
})
.then((url) => {

View File

@ -19,6 +19,3 @@ export const MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS = [
];
export const TAB_SPACE_WIDTH = 2;
export const APPERANCE_OPTIONS = ["auto", "light", "dark"] as const;
export const APPERANCE_OPTIONS_STORAGE_KEY = "setting_APPERANCE_OPTIONS";

View File

@ -0,0 +1,65 @@
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,30 +0,0 @@
import { useEffect } from "react";
import { useColorScheme } from "@mui/joy/styles";
import { APPERANCE_OPTIONS, APPERANCE_OPTIONS_STORAGE_KEY } from "../helpers/consts";
import useLocalStorage from "./useLocalStorage";
import useMediaQuery from "./useMediaQuery";
export type Apperance = typeof APPERANCE_OPTIONS[number];
const useApperance = () => {
const [apperance, setApperance] = useLocalStorage<Apperance>(APPERANCE_OPTIONS_STORAGE_KEY, APPERANCE_OPTIONS[0]);
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
const { setMode } = useColorScheme();
useEffect(() => {
const root = document.documentElement;
if (apperance === "dark" || (apperance === "auto" && prefersDarkMode)) {
root.classList.add("dark");
setMode("dark");
} else {
root.classList.remove("dark");
setMode("light");
}
}, [apperance, prefersDarkMode]);
return [apperance, setApperance] as const;
};
export default useApperance;

View File

@ -1,26 +0,0 @@
import { useState } from "react";
const useLocalStorage = <T>(key: string, initialValue: T) => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
const setValue = (value: T | ((val: T) => T)) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.log(error);
}
};
return [storedValue, setValue] as const;
};
export default useLocalStorage;

View File

@ -152,7 +152,7 @@
"enable-folding-memo": "Enable folding memo",
"editor-font-style": "Editor font style",
"mobile-editor-style": "Mobile editor style",
"default-memo-sort-option": "Display by created/updated time",
"default-memo-sort-option": "Memo display time",
"created_ts": "Created Time",
"updated_ts": "Updated Time"
},
@ -168,9 +168,9 @@
"additional-script-placeholder": "Additional JavaScript codes"
},
"apperance-option": {
"auto": "Follow system",
"light": "Always light",
"dark": "Always dark"
"dark": "Always dark",
"system": "Follow system"
}
},
"amount-text": {

View File

@ -152,7 +152,7 @@
"enable-folding-memo": "Activer le mémo pliable",
"editor-font-style": "Style de police de l'éditeur",
"mobile-editor-style": "Style de l'éditeur mobile",
"default-memo-sort-option": "Affichage par heure de création/mise à jour",
"default-memo-sort-option": "Memo display time",
"created_ts": "Heure de création",
"updated_ts": "Heure de mise à jour"
},

View File

@ -151,7 +151,7 @@
"enable-folding-memo": "Enable folding memo",
"editor-font-style": "Thay đổi font cho trình soạn thảo",
"mobile-editor-style": "Vị trí editor trên mobile",
"default-memo-sort-option": "Sắp xếp theo thời gian đã tạo",
"default-memo-sort-option": "Memo display time",
"created_ts": "tạo thời gian",
"updated_ts": "Thời gian cập nhật"
},

View File

@ -152,7 +152,7 @@
"enable-folding-memo": "开启折叠 Memo",
"editor-font-style": "编辑器字体样式",
"mobile-editor-style": "移动端编辑器样式",
"default-memo-sort-option": "按创建时间/更新时间显示",
"default-memo-sort-option": "Memo 显示时间",
"created_ts": "创建时间",
"updated_ts": "更新时间"
},
@ -168,9 +168,9 @@
"additional-script-placeholder": "自定义 JavaScript 代码"
},
"apperance-option": {
"auto": "跟随系统",
"light": "总是浅色",
"dark": "总是深色"
"dark": "总是深色",
"system": "跟随系统"
}
},
"amount-text": {

View File

@ -9,7 +9,7 @@ import useLoading from "../hooks/useLoading";
import { globalService, userService } from "../services";
import Icon from "../components/Icon";
import toastHelper from "../components/Toast";
import ApperanceSelect from "../components/ApperanceSelect";
import AppearanceSelect from "../components/AppearanceSelect";
import "../less/auth.less";
const validateConfig: ValidatorConfig = {
@ -177,7 +177,7 @@ const Auth = () => {
<Option value="vi">Tiếng Việt</Option>
<Option value="fr">French</Option>
</Select>
<ApperanceSelect />
<AppearanceSelect />
</div>
</div>
</div>

View File

@ -18,6 +18,7 @@ const router = createBrowserRouter([
} catch (error) {
// do nth
}
return null;
},
},
{
@ -37,6 +38,7 @@ const router = createBrowserRouter([
} else if (isNullorUndefined(user)) {
return redirect("/explore");
}
return null;
},
},
{
@ -54,6 +56,7 @@ const router = createBrowserRouter([
if (isNullorUndefined(host)) {
return redirect("/auth");
}
return null;
},
},
{
@ -71,6 +74,7 @@ const router = createBrowserRouter([
if (isNullorUndefined(host)) {
return redirect("/auth");
}
return null;
},
},
{
@ -88,6 +92,7 @@ const router = createBrowserRouter([
if (isNullorUndefined(host)) {
return redirect("/auth");
}
return null;
},
},
]);

View File

@ -1,7 +1,7 @@
import store from "../store";
import * as api from "../helpers/api";
import * as storage from "../helpers/storage";
import { setGlobalState, setLocale } from "../store/modules/global";
import { setAppearance, setGlobalState, setLocale } from "../store/modules/global";
const globalService = {
getState: () => {
@ -11,6 +11,7 @@ const globalService = {
initialState: async () => {
const defaultGlobalState = {
locale: "en" as Locale,
appearance: "system" as Appearance,
systemStatus: {
allowSignUp: false,
additionalStyle: "",
@ -38,6 +39,10 @@ const globalService = {
setLocale: (locale: Locale) => {
store.dispatch(setLocale(locale));
},
setAppearance: (appearance: Appearance) => {
store.dispatch(setAppearance(appearance));
},
};
export default globalService;

View File

@ -8,6 +8,7 @@ import { setUser, patchUser, setHost, setOwner } from "../store/modules/user";
const defaultSetting: Setting = {
locale: "en",
appearance: "system",
memoVisibility: "PRIVATE",
memoDisplayTsOption: "created_ts",
};

View File

@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit";
interface State {
locale: Locale;
appearance: Appearance;
systemStatus: SystemStatus;
}
@ -9,6 +10,7 @@ const globalSlice = createSlice({
name: "global",
initialState: {
locale: "en",
appearance: "system",
systemStatus: {
host: undefined,
profile: {
@ -31,9 +33,15 @@ const globalSlice = createSlice({
locale: action.payload,
};
},
setAppearance: (state, action: PayloadAction<Appearance>) => {
return {
...state,
appearance: action.payload,
};
},
},
});
export const { setGlobalState, setLocale } = globalSlice.actions;
export const { setGlobalState, setLocale, setAppearance } = globalSlice.actions;
export default globalSlice.reducer;

View File

@ -3,10 +3,8 @@ import { extendTheme } from "@mui/joy";
const theme = extendTheme({
components: {
JoySelect: {
styleOverrides: {
root: {
fontSize: "0.875rem",
},
defaultProps: {
size: "sm",
},
},
},

View File

@ -1,5 +1,8 @@
type Appearance = "light" | "dark" | "system";
interface Setting {
locale: Locale;
appearance: Appearance;
memoVisibility: Visibility;
memoDisplayTsOption: "created_ts" | "updated_ts";
}
@ -13,12 +16,17 @@ interface UserLocaleSetting {
value: Locale;
}
interface UserAppearanceSetting {
key: "appearance";
value: Appearance;
}
interface UserMemoVisibilitySetting {
key: "memoVisibility";
value: Visibility;
}
type UserSetting = UserLocaleSetting | UserMemoVisibilitySetting;
type UserSetting = UserLocaleSetting | UserAppearanceSetting | UserMemoVisibilitySetting;
interface UserSettingUpsert {
key: keyof Setting;