diff --git a/api/system.go b/api/system.go index ed6086c5..9d67cbfa 100644 --- a/api/system.go +++ b/api/system.go @@ -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"` } diff --git a/api/system_setting.go b/api/system_setting.go index ce35bb8a..197ea22f 100644 --- a/api/system_setting.go +++ b/api/system_setting.go @@ -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") } diff --git a/server/system.go b/server/system.go index 5bf8e78d..c485e930 100644 --- a/server/system.go +++ b/server/system.go @@ -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), + } } } diff --git a/web/src/App.tsx b/web/src/App.tsx index 5a6b5305..e00b47ab 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -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(() => { diff --git a/web/src/components/Dialog/BaseDialog.tsx b/web/src/components/Dialog/BaseDialog.tsx index 85a76ccf..b41e818c 100644 --- a/web/src/components/Dialog/BaseDialog.tsx +++ b/web/src/components/Dialog/BaseDialog.tsx @@ -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) => { const { children, className, clickSpaceDestroy, dialogName, destroy } = props; const dialogStore = useDialogStore(); + const dialogContainerRef = useRef(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) => { 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) => { return (
-
e.stopPropagation()}> +
e.stopPropagation()}> {children}
diff --git a/web/src/components/SettingDialog.tsx b/web/src/components/SettingDialog.tsx index da2df9a0..961c3fdd 100644 --- a/web/src/components/SettingDialog.tsx +++ b/web/src/components/SettingDialog.tsx @@ -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) => { onClick={() => handleSectionSelectorItemClick("system")} className={`section-item ${state.selectedSection === "system" ? "selected" : ""}`} > - 🧑‍🔧 {t("setting.system")} + 🛠️ {t("setting.system")}
diff --git a/web/src/components/Settings/SystemSection.tsx b/web/src/components/Settings/SystemSection.tsx index 98d8a215..345a6ad7 100644 --- a/web/src/components/Settings/SystemSection.tsx +++ b/web/src/components/Settings/SystemSection.tsx @@ -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({ - 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 (

{t("common.basic")}

-