mirror of
https://github.com/usememos/memos.git
synced 2025-03-03 16:53:30 +08:00
feat: customize system profile (#774)
* feat: system setting for customized profile * chore: update * feat: update frontend * chore: update
This commit is contained in:
parent
55695f2189
commit
b67ed1ee13
20 changed files with 259 additions and 56 deletions
|
@ -14,4 +14,6 @@ type SystemStatus struct {
|
|||
AdditionalStyle string `json:"additionalStyle"`
|
||||
// Additional script.
|
||||
AdditionalScript string `json:"additionalScript"`
|
||||
// Customized server profile, including server name and external url.
|
||||
CustomizedProfile CustomizedProfile `json:"customizedProfile"`
|
||||
}
|
||||
|
|
|
@ -14,8 +14,20 @@ const (
|
|||
SystemSettingAdditionalStyleName SystemSettingName = "additionalStyle"
|
||||
// SystemSettingAdditionalScriptName is the key type of additional script.
|
||||
SystemSettingAdditionalScriptName SystemSettingName = "additionalScript"
|
||||
// SystemSettingCustomizedProfileName is the key type of customized server profile.
|
||||
SystemSettingCustomizedProfileName SystemSettingName = "customizedProfile"
|
||||
)
|
||||
|
||||
// CustomizedProfile is the struct definition for SystemSettingCustomizedProfileName system setting item.
|
||||
type CustomizedProfile struct {
|
||||
// Name is the server name, default is `memos`
|
||||
Name string `json:"name"`
|
||||
// IconURL is the url of icon image.
|
||||
IconURL string `json:"iconUrl"`
|
||||
// ExternalURL is the external url of server. e.g. https://usermemos.com
|
||||
ExternalURL string `json:"externalUrl"`
|
||||
}
|
||||
|
||||
func (key SystemSettingName) String() string {
|
||||
switch key {
|
||||
case SystemSettingAllowSignUpName:
|
||||
|
@ -24,6 +36,8 @@ func (key SystemSettingName) String() string {
|
|||
return "additionalStyle"
|
||||
case SystemSettingAdditionalScriptName:
|
||||
return "additionalScript"
|
||||
case SystemSettingCustomizedProfileName:
|
||||
return "customizedProfile"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
@ -75,6 +89,16 @@ func (upsert SystemSettingUpsert) Validate() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal system setting additional script value")
|
||||
}
|
||||
} else if upsert.Name == SystemSettingCustomizedProfileName {
|
||||
value := CustomizedProfile{
|
||||
Name: "memos",
|
||||
IconURL: "",
|
||||
ExternalURL: "",
|
||||
}
|
||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal system setting customized profile value")
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid system setting name")
|
||||
}
|
||||
|
|
|
@ -46,6 +46,9 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||
AllowSignUp: false,
|
||||
AdditionalStyle: "",
|
||||
AdditionalScript: "",
|
||||
CustomizedProfile: api.CustomizedProfile{
|
||||
Name: "memos",
|
||||
},
|
||||
}
|
||||
|
||||
systemSettingList, err := s.Store.FindSystemSettingList(ctx, &api.SystemSettingFind{})
|
||||
|
@ -54,7 +57,7 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||
}
|
||||
for _, systemSetting := range systemSettingList {
|
||||
var value interface{}
|
||||
err = json.Unmarshal([]byte(systemSetting.Value), &value)
|
||||
err := json.Unmarshal([]byte(systemSetting.Value), &value)
|
||||
if err != nil {
|
||||
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to unmarshal system setting").SetInternal(err)
|
||||
}
|
||||
|
@ -65,6 +68,13 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||
systemStatus.AdditionalStyle = value.(string)
|
||||
} else if systemSetting.Name == api.SystemSettingAdditionalScriptName {
|
||||
systemStatus.AdditionalScript = value.(string)
|
||||
} else if systemSetting.Name == api.SystemSettingCustomizedProfileName {
|
||||
valueMap := value.(map[string]interface{})
|
||||
systemStatus.CustomizedProfile = api.CustomizedProfile{
|
||||
Name: valueMap["name"].(string),
|
||||
IconURL: valueMap["iconUrl"].(string),
|
||||
ExternalURL: valueMap["externalUrl"].(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -55,6 +55,11 @@ const App = () => {
|
|||
scriptEl.innerHTML = systemStatus.additionalScript;
|
||||
document.head.appendChild(scriptEl);
|
||||
}
|
||||
|
||||
// dynamic update metadata with customized profile.
|
||||
document.title = systemStatus.customizedProfile.name;
|
||||
const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement;
|
||||
link.href = systemStatus.customizedProfile.iconUrl || "/logo.webp";
|
||||
}, [systemStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { useEffect } from "react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { Provider } from "react-redux";
|
||||
import { ANIMATION_DURATION } from "../../helpers/consts";
|
||||
|
@ -10,8 +10,8 @@ import "../../less/base-dialog.less";
|
|||
|
||||
interface DialogConfig {
|
||||
className: string;
|
||||
clickSpaceDestroy?: boolean;
|
||||
dialogName: string;
|
||||
clickSpaceDestroy?: boolean;
|
||||
}
|
||||
|
||||
interface Props extends DialogConfig, DialogProps {
|
||||
|
@ -21,13 +21,14 @@ interface Props extends DialogConfig, DialogProps {
|
|||
const BaseDialog: React.FC<Props> = (props: Props) => {
|
||||
const { children, className, clickSpaceDestroy, dialogName, destroy } = props;
|
||||
const dialogStore = useDialogStore();
|
||||
const dialogContainerRef = useRef<HTMLDivElement>(null);
|
||||
const dialogIndex = dialogStore.state.dialogStack.findIndex((item) => item === dialogName);
|
||||
|
||||
useEffect(() => {
|
||||
dialogStore.pushDialogStack(dialogName);
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.code === "Escape") {
|
||||
if (dialogName === dialogStore.topDialogStack()) {
|
||||
dialogStore.popDialogStack();
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
|
@ -37,9 +38,16 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
|||
|
||||
return () => {
|
||||
document.body.removeEventListener("keydown", handleKeyDown);
|
||||
dialogStore.removeDialog(dialogName);
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (dialogIndex > 0 && dialogContainerRef.current) {
|
||||
dialogContainerRef.current.style.marginTop = `${dialogIndex * 16}px`;
|
||||
}
|
||||
}, [dialogIndex]);
|
||||
|
||||
const handleSpaceClicked = () => {
|
||||
if (clickSpaceDestroy) {
|
||||
destroy();
|
||||
|
@ -48,7 +56,7 @@ const BaseDialog: React.FC<Props> = (props: Props) => {
|
|||
|
||||
return (
|
||||
<div className={`dialog-wrapper ${className}`} onMouseDown={handleSpaceClicked}>
|
||||
<div className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
||||
<div ref={dialogContainerRef} className="dialog-container" onMouseDown={(e) => e.stopPropagation()}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useUserStore } from "../store/module";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import MyAccountSection from "./Settings/MyAccountSection";
|
||||
|
@ -7,7 +8,6 @@ import PreferencesSection from "./Settings/PreferencesSection";
|
|||
import MemberSection from "./Settings/MemberSection";
|
||||
import SystemSection from "./Settings/SystemSection";
|
||||
import "../less/setting-dialog.less";
|
||||
import { useUserStore } from "../store/module";
|
||||
|
||||
type Props = DialogProps;
|
||||
|
||||
|
@ -67,7 +67,7 @@ const SettingDialog: React.FC<Props> = (props: Props) => {
|
|||
onClick={() => handleSectionSelectorItemClick("system")}
|
||||
className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`}
|
||||
>
|
||||
<span className="icon-text">🧑🔧</span> {t("setting.system")}
|
||||
<span className="icon-text">🛠️</span> {t("setting.system")}
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, Switch, Textarea } from "@mui/joy";
|
||||
import { useGlobalStore } from "../../store/module";
|
||||
import * as api from "../../helpers/api";
|
||||
import toastHelper from "../Toast";
|
||||
import "../../less/settings/system-section.less";
|
||||
import showUpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||
import "@/less/settings/system-section.less";
|
||||
|
||||
interface State {
|
||||
dbSize: number;
|
||||
|
@ -23,25 +25,28 @@ const formatBytes = (bytes: number) => {
|
|||
|
||||
const SystemSection = () => {
|
||||
const { t } = useTranslation();
|
||||
const globalStore = useGlobalStore();
|
||||
const systemStatus = globalStore.state.systemStatus;
|
||||
const [state, setState] = useState<State>({
|
||||
dbSize: 0,
|
||||
allowSignUp: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
dbSize: systemStatus.dbSize,
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
api.getSystemStatus().then(({ data }) => {
|
||||
const { data: status } = data;
|
||||
setState({
|
||||
dbSize: status.dbSize,
|
||||
allowSignUp: status.allowSignUp,
|
||||
additionalStyle: status.additionalStyle,
|
||||
additionalScript: status.additionalScript,
|
||||
});
|
||||
});
|
||||
globalStore.fetchSystemStatus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setState({
|
||||
dbSize: systemStatus.dbSize,
|
||||
allowSignUp: systemStatus.allowSignUp,
|
||||
additionalStyle: systemStatus.additionalStyle,
|
||||
additionalScript: systemStatus.additionalScript,
|
||||
});
|
||||
}, [systemStatus]);
|
||||
|
||||
const handleAllowSignUpChanged = async (value: boolean) => {
|
||||
setState({
|
||||
...state,
|
||||
|
@ -60,16 +65,14 @@ const SystemSection = () => {
|
|||
});
|
||||
};
|
||||
|
||||
const handleUpdateCustomizedProfileButtonClick = () => {
|
||||
showUpdateCustomizedProfileDialog();
|
||||
};
|
||||
|
||||
const handleVacuumBtnClick = async () => {
|
||||
try {
|
||||
await api.vacuumDatabase();
|
||||
const { data: status } = (await api.getSystemStatus()).data;
|
||||
setState({
|
||||
dbSize: status.dbSize,
|
||||
allowSignUp: status.allowSignUp,
|
||||
additionalStyle: status.additionalStyle,
|
||||
additionalScript: status.additionalScript,
|
||||
});
|
||||
await globalStore.fetchSystemStatus();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
|
@ -113,17 +116,23 @@ const SystemSection = () => {
|
|||
return (
|
||||
<div className="section-container system-section-container">
|
||||
<p className="title-text">{t("common.basic")}</p>
|
||||
<label className="form-label">
|
||||
<div className="form-label">
|
||||
<div className="normal-text">
|
||||
Server name: <span className="font-mono font-bold">{systemStatus.customizedProfile.name}</span>
|
||||
</div>
|
||||
<Button onClick={handleUpdateCustomizedProfileButtonClick}>Edit</Button>
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">
|
||||
{t("setting.system-section.database-file-size")}: <span className="font-mono font-medium">{formatBytes(state.dbSize)}</span>
|
||||
{t("setting.system-section.database-file-size")}: <span className="font-mono font-bold">{formatBytes(state.dbSize)}</span>
|
||||
</span>
|
||||
<Button onClick={handleVacuumBtnClick}>{t("common.vacuum")}</Button>
|
||||
</label>
|
||||
</div>
|
||||
<p className="title-text">{t("sidebar.setting")}</p>
|
||||
<label className="form-label">
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.allow-user-signup")}</span>
|
||||
<Switch checked={state.allowSignUp} onChange={(event) => handleAllowSignUpChanged(event.target.checked)} />
|
||||
</label>
|
||||
</div>
|
||||
<div className="form-label">
|
||||
<span className="normal-text">{t("setting.system-section.additional-style")}</span>
|
||||
<Button onClick={handleSaveAdditionalStyle}>{t("common.save")}</Button>
|
||||
|
|
100
web/src/components/UpdateCustomizedProfileDialog.tsx
Normal file
100
web/src/components/UpdateCustomizedProfileDialog.tsx
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useGlobalStore } from "../store/module";
|
||||
import * as api from "../helpers/api";
|
||||
import Icon from "./Icon";
|
||||
import { generateDialog } from "./Dialog";
|
||||
import toastHelper from "./Toast";
|
||||
|
||||
type Props = DialogProps;
|
||||
|
||||
const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const globalStore = useGlobalStore();
|
||||
const [state, setState] = useState<CustomizedProfile>(globalStore.state.systemStatus.customizedProfile);
|
||||
|
||||
useEffect(() => {
|
||||
// do nth
|
||||
}, []);
|
||||
|
||||
const handleCloseBtnClick = () => {
|
||||
destroy();
|
||||
};
|
||||
|
||||
const handleNameChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState((state) => {
|
||||
return {
|
||||
...state,
|
||||
name: e.target.value as string,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleIconUrlChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setState((state) => {
|
||||
return {
|
||||
...state,
|
||||
iconUrl: e.target.value as string,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
const handleSaveBtnClick = async () => {
|
||||
if (state.name === "" || state.iconUrl === "") {
|
||||
toastHelper.error(t("message.fill-all"));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.upsertSystemSetting({
|
||||
name: "customizedProfile",
|
||||
value: JSON.stringify(state),
|
||||
});
|
||||
await globalStore.fetchSystemStatus();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return;
|
||||
}
|
||||
toastHelper.success("Succeed to update customized profile");
|
||||
destroy();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dialog-header-container !w-64">
|
||||
<p className="title-text">Customize server</p>
|
||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||
<Icon.X />
|
||||
</button>
|
||||
</div>
|
||||
<div className="dialog-content-container">
|
||||
<p className="text-sm mb-1">
|
||||
Server Name<span className="text-sm text-gray-400 ml-1">(Default is memos)</span>
|
||||
</p>
|
||||
<input type="text" className="input-text" value={state.name} onChange={handleNameChanged} />
|
||||
<p className="text-sm mb-1 mt-2">Icon URL</p>
|
||||
<input type="text" className="input-text" value={state.iconUrl} onChange={handleIconUrlChanged} />
|
||||
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||
{t("common.cancel")}
|
||||
</span>
|
||||
<span className="btn-primary" onClick={handleSaveBtnClick}>
|
||||
{t("common.save")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function showUpdateCustomizedProfileDialog() {
|
||||
generateDialog(
|
||||
{
|
||||
className: "update-customized-profile-dialog",
|
||||
dialogName: "update-customized-profile-dialog",
|
||||
},
|
||||
UpdateCustomizedProfileDialog
|
||||
);
|
||||
}
|
||||
|
||||
export default showUpdateCustomizedProfileDialog;
|
|
@ -14,11 +14,11 @@
|
|||
@apply w-full flex flex-row justify-start items-center;
|
||||
|
||||
> .logo-img {
|
||||
@apply h-20 w-auto;
|
||||
@apply h-16 w-auto mr-2;
|
||||
}
|
||||
|
||||
> .logo-text {
|
||||
@apply text-6xl tracking-wide text-black dark:text-gray-200;
|
||||
@apply text-5xl tracking-wide text-black opacity-80 dark:text-gray-200;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .logo-img {
|
||||
@apply h-12 sm:h-14 w-auto mr-1;
|
||||
@apply h-12 sm:h-14 w-auto mr-2;
|
||||
}
|
||||
|
||||
> .title-text {
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
@apply flex flex-row justify-start items-center;
|
||||
|
||||
> .logo-img {
|
||||
@apply h-12 sm:h-14 w-auto mr-1;
|
||||
@apply h-12 sm:h-14 w-auto mr-2;
|
||||
}
|
||||
|
||||
> .logo-text {
|
||||
|
|
|
@ -23,8 +23,8 @@ const Auth = () => {
|
|||
const navigate = useNavigate();
|
||||
const globalStore = useGlobalStore();
|
||||
const userStore = useUserStore();
|
||||
const systemStatus = globalStore.state.systemStatus;
|
||||
const actionBtnLoadingState = useLoading(false);
|
||||
const systemStatus = globalStore.state.systemStatus;
|
||||
const mode = systemStatus.profile.mode;
|
||||
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
||||
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
||||
|
@ -119,8 +119,8 @@ const Auth = () => {
|
|||
<div className="auth-form-wrapper">
|
||||
<div className="page-header-container">
|
||||
<div className="title-container">
|
||||
<img className="logo-img" src="/logo.webp" alt="" />
|
||||
<p className="logo-text">memos</p>
|
||||
<img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
|
||||
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
||||
</div>
|
||||
<p className="slogan-text">{t("slogan")}</p>
|
||||
</div>
|
||||
|
|
|
@ -2,7 +2,7 @@ import dayjs from "dayjs";
|
|||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import { DEFAULT_MEMO_LIMIT } from "../helpers/consts";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import toastHelper from "../components/Toast";
|
||||
|
@ -16,16 +16,18 @@ interface State {
|
|||
|
||||
const Explore = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const globalStore = useGlobalStore();
|
||||
const locationStore = useLocationStore();
|
||||
const userStore = useUserStore();
|
||||
const memoStore = useMemoStore();
|
||||
const user = userStore.state.user;
|
||||
const location = locationStore.state;
|
||||
const [state, setState] = useState<State>({
|
||||
memos: [],
|
||||
});
|
||||
const [isComplete, setIsComplete] = useState<boolean>(false);
|
||||
const loadingState = useLoading();
|
||||
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
|
||||
const user = userStore.state.user;
|
||||
const location = locationStore.state;
|
||||
|
||||
useEffect(() => {
|
||||
memoStore.fetchAllMemos(DEFAULT_MEMO_LIMIT, state.memos.length).then((memos) => {
|
||||
|
@ -61,8 +63,8 @@ const Explore = () => {
|
|||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<div className="title-container">
|
||||
<img className="logo-img" src="/logo.webp" alt="" />
|
||||
<span className="title-text">memos</span>
|
||||
<img className="logo-img" src={customizedProfile.iconUrl} alt="" />
|
||||
<span className="title-text">{customizedProfile.name}</span>
|
||||
</div>
|
||||
<div className="action-button-container">
|
||||
{!loadingState.isLoading && user ? (
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
|
|||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import { UNKNOWN_ID } from "../helpers/consts";
|
||||
import { useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import { useGlobalStore, useLocationStore, useMemoStore, useUserStore } from "../store/module";
|
||||
import useLoading from "../hooks/useLoading";
|
||||
import toastHelper from "../components/Toast";
|
||||
import MemoContent from "../components/MemoContent";
|
||||
|
@ -17,17 +17,19 @@ interface State {
|
|||
const MemoDetail = () => {
|
||||
const { t, i18n } = useTranslation();
|
||||
const params = useParams();
|
||||
const globalStore = useGlobalStore();
|
||||
const locationStore = useLocationStore();
|
||||
const memoStore = useMemoStore();
|
||||
const userStore = useUserStore();
|
||||
const user = userStore.state.user;
|
||||
const location = locationStore.state;
|
||||
const [state, setState] = useState<State>({
|
||||
memo: {
|
||||
id: UNKNOWN_ID,
|
||||
} as Memo,
|
||||
});
|
||||
const loadingState = useLoading();
|
||||
const customizedProfile = globalStore.state.systemStatus.customizedProfile;
|
||||
const user = userStore.state.user;
|
||||
const location = locationStore.state;
|
||||
|
||||
useEffect(() => {
|
||||
const memoId = Number(params.memoId);
|
||||
|
@ -52,8 +54,8 @@ const MemoDetail = () => {
|
|||
<div className="page-container">
|
||||
<div className="page-header">
|
||||
<div className="title-container">
|
||||
<img className="logo-img" src="/logo.webp" alt="" />
|
||||
<p className="logo-text">memos</p>
|
||||
<img className="logo-img" src={customizedProfile.iconUrl} alt="" />
|
||||
<p className="logo-text">{customizedProfile.name}</p>
|
||||
</div>
|
||||
<div className="action-button-container">
|
||||
{!loadingState.isLoading && (
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import store, { useAppSelector } from "..";
|
||||
import { pushDialogStack, popDialogStack } from "../reducer/dialog";
|
||||
import { last } from "lodash";
|
||||
import store, { useAppSelector } from "..";
|
||||
import { pushDialogStack, popDialogStack, removeDialog } from "../reducer/dialog";
|
||||
|
||||
export const useDialogStore = () => {
|
||||
const state = useAppSelector((state) => state.editor);
|
||||
const state = useAppSelector((state) => state.dialog);
|
||||
|
||||
return {
|
||||
state,
|
||||
|
@ -16,6 +16,9 @@ export const useDialogStore = () => {
|
|||
popDialogStack: () => {
|
||||
store.dispatch(popDialogStack());
|
||||
},
|
||||
removeDialog: (dialogName: string) => {
|
||||
store.dispatch(removeDialog(dialogName));
|
||||
},
|
||||
topDialogStack: () => {
|
||||
return last(store.getState().dialog.dialogStack);
|
||||
},
|
||||
|
|
|
@ -11,6 +11,11 @@ export const initialGlobalState = async () => {
|
|||
allowSignUp: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
customizedProfile: {
|
||||
name: "memos",
|
||||
iconUrl: "/logo.webp",
|
||||
externalUrl: "",
|
||||
},
|
||||
} as SystemStatus,
|
||||
};
|
||||
|
||||
|
@ -42,6 +47,11 @@ export const useGlobalStore = () => {
|
|||
getState: () => {
|
||||
return store.getState().global;
|
||||
},
|
||||
fetchSystemStatus: async () => {
|
||||
const { data: systemStatus } = (await api.getSystemStatus()).data;
|
||||
store.dispatch(setGlobalState({ systemStatus: systemStatus }));
|
||||
return systemStatus;
|
||||
},
|
||||
setLocale: (locale: Locale) => {
|
||||
store.dispatch(setLocale(locale));
|
||||
},
|
||||
|
|
|
@ -22,9 +22,16 @@ const dialogSlice = createSlice({
|
|||
dialogStack: state.dialogStack.slice(0, state.dialogStack.length - 1),
|
||||
};
|
||||
},
|
||||
removeDialog: (state, action: PayloadAction<string>) => {
|
||||
const filterDialogStack = state.dialogStack.filter((dialogName) => dialogName !== action.payload);
|
||||
return {
|
||||
...state,
|
||||
dialogStack: filterDialogStack,
|
||||
};
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const { pushDialogStack, popDialogStack } = dialogSlice.actions;
|
||||
export const { pushDialogStack, popDialogStack, removeDialog } = dialogSlice.actions;
|
||||
|
||||
export default dialogSlice.reducer;
|
||||
|
|
|
@ -21,11 +21,19 @@ const globalSlice = createSlice({
|
|||
allowSignUp: false,
|
||||
additionalStyle: "",
|
||||
additionalScript: "",
|
||||
customizedProfile: {
|
||||
name: "memos",
|
||||
iconUrl: "/logo.webp",
|
||||
externalUrl: "",
|
||||
},
|
||||
},
|
||||
} as State,
|
||||
reducers: {
|
||||
setGlobalState: (_, action: PayloadAction<State>) => {
|
||||
return action.payload;
|
||||
setGlobalState: (state, action: PayloadAction<Partial<State>>) => {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
};
|
||||
},
|
||||
setLocale: (state, action: PayloadAction<Locale>) => {
|
||||
return {
|
||||
|
|
7
web/src/types/modules/system.d.ts
vendored
7
web/src/types/modules/system.d.ts
vendored
|
@ -3,6 +3,12 @@ interface Profile {
|
|||
version: string;
|
||||
}
|
||||
|
||||
interface CustomizedProfile {
|
||||
name: string;
|
||||
iconUrl: string;
|
||||
externalUrl: string;
|
||||
}
|
||||
|
||||
interface SystemStatus {
|
||||
host?: User;
|
||||
profile: Profile;
|
||||
|
@ -11,6 +17,7 @@ interface SystemStatus {
|
|||
allowSignUp: boolean;
|
||||
additionalStyle: string;
|
||||
additionalScript: string;
|
||||
customizedProfile: CustomizedProfile;
|
||||
}
|
||||
|
||||
interface SystemSetting {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { resolve } from "path";
|
||||
import { defineConfig } from "vite";
|
||||
import legacy from "@vitejs/plugin-legacy";
|
||||
import react from "@vitejs/plugin-react-swc";
|
||||
|
@ -32,4 +33,9 @@ export default defineConfig({
|
|||
},
|
||||
},
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
"@/": `${resolve(__dirname, "src")}/`,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue