diff --git a/api/system_setting.go b/api/system_setting.go index 197ea22f..ff67d11c 100644 --- a/api/system_setting.go +++ b/api/system_setting.go @@ -3,6 +3,8 @@ package api import ( "encoding/json" "fmt" + + "golang.org/x/exp/slices" ) type SystemSettingName string @@ -24,6 +26,12 @@ type CustomizedProfile struct { Name string `json:"name"` // IconURL is the url of icon image. IconURL string `json:"iconUrl"` + // Description is the server description. + Description string `json:"description"` + // Locale is the server default locale. + Locale string `json:"locale"` + // Appearance is the server default appearance. + Appearance string `json:"appearance"` // ExternalURL is the external url of server. e.g. https://usermemos.com ExternalURL string `json:"externalUrl"` } @@ -90,15 +98,24 @@ func (upsert SystemSettingUpsert) Validate() error { return fmt.Errorf("failed to unmarshal system setting additional script value") } } else if upsert.Name == SystemSettingCustomizedProfileName { - value := CustomizedProfile{ + customizedProfile := CustomizedProfile{ Name: "memos", IconURL: "", + Description: "", + Locale: "en", + Appearance: "system", ExternalURL: "", } - err := json.Unmarshal([]byte(upsert.Value), &value) + err := json.Unmarshal([]byte(upsert.Value), &customizedProfile) if err != nil { return fmt.Errorf("failed to unmarshal system setting customized profile value") } + if !slices.Contains(UserSettingLocaleValue, customizedProfile.Locale) { + return fmt.Errorf("invalid locale value") + } + if !slices.Contains(UserSettingAppearanceValue, customizedProfile.Appearance) { + return fmt.Errorf("invalid appearance value") + } } else { return fmt.Errorf("invalid system setting name") } diff --git a/api/user_setting.go b/api/user_setting.go index 23001f0b..89ee852d 100644 --- a/api/user_setting.go +++ b/api/user_setting.go @@ -3,6 +3,8 @@ package api import ( "encoding/json" "fmt" + + "golang.org/x/exp/slices" ) type UserSettingKey string @@ -60,32 +62,16 @@ func (upsert UserSettingUpsert) Validate() error { if err != nil { return fmt.Errorf("failed to unmarshal user setting locale value") } - - invalid := true - for _, value := range UserSettingLocaleValue { - if localeValue == value { - invalid = false - break - } - } - if invalid { + if !slices.Contains(UserSettingLocaleValue, localeValue) { return fmt.Errorf("invalid user setting locale value") } } else if upsert.Key == UserSettingAppearanceKey { - appearanceValue := "light" + appearanceValue := "system" 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 { + if !slices.Contains(UserSettingAppearanceValue, appearanceValue) { return fmt.Errorf("invalid user setting appearance value") } } else if upsert.Key == UserSettingMemoVisibilityKey { @@ -94,15 +80,7 @@ func (upsert UserSettingUpsert) Validate() error { if err != nil { return fmt.Errorf("failed to unmarshal user setting memo visibility value") } - - invalid := true - for _, value := range UserSettingMemoVisibilityValue { - if memoVisibilityValue == value { - invalid = false - break - } - } - if invalid { + if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) { return fmt.Errorf("invalid user setting memo visibility value") } } else if upsert.Key == UserSettingMemoDisplayTsOptionKey { @@ -111,15 +89,7 @@ func (upsert UserSettingUpsert) Validate() error { if err != nil { return fmt.Errorf("failed to unmarshal user setting memo display ts option") } - - invalid := true - for _, value := range UserSettingMemoDisplayTsOptionKeyValue { - if memoDisplayTsOption == value { - invalid = false - break - } - } - if invalid { + if !slices.Contains(UserSettingMemoDisplayTsOptionKeyValue, memoDisplayTsOption) { return fmt.Errorf("invalid user setting memo display ts option value") } } else { diff --git a/go.mod b/go.mod index 5085d860..9e2b69ac 100644 --- a/go.mod +++ b/go.mod @@ -38,11 +38,14 @@ require ( github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/fasttemplate v1.2.1 // indirect github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect - golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 // indirect + golang.org/x/sys v0.1.0 // indirect golang.org/x/text v0.3.7 // indirect golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -require github.com/segmentio/analytics-go v3.1.0+incompatible +require ( + github.com/segmentio/analytics-go v3.1.0+incompatible + golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 +) diff --git a/go.sum b/go.sum index e03d5d8a..3c318af8 100644 --- a/go.sum +++ b/go.sum @@ -70,14 +70,16 @@ github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEAB github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c= golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15 h1:5oN1Pz/eDhCpbMbLstvIPa0b/BEQo6g6nwV3pLjfM6w= +golang.org/x/exp v0.0.0-20221217163422-3c43f8badb15/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= golang.org/x/net v0.0.0-20220728030405-41545e8bf201 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0= golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211103235746-7861aae1554b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ= diff --git a/server/acl.go b/server/acl.go index ccb67e81..c2e33995 100644 --- a/server/acl.go +++ b/server/acl.go @@ -27,6 +27,7 @@ func setUserSession(ctx echo.Context, user *api.User) error { Path: "/", MaxAge: 3600 * 24 * 30, HttpOnly: true, + Secure: true, } sess.Values[userIDContextKey] = user.ID err := sess.Save(ctx.Request(), ctx.Response()) diff --git a/server/system.go b/server/system.go index c485e930..e3a28116 100644 --- a/server/system.go +++ b/server/system.go @@ -47,7 +47,12 @@ func (s *Server) registerSystemRoutes(g *echo.Group) { AdditionalStyle: "", AdditionalScript: "", CustomizedProfile: api.CustomizedProfile{ - Name: "memos", + Name: "memos", + IconURL: "", + Description: "", + Locale: "en", + Appearance: "system", + ExternalURL: "", }, } @@ -73,6 +78,9 @@ func (s *Server) registerSystemRoutes(g *echo.Group) { systemStatus.CustomizedProfile = api.CustomizedProfile{ Name: valueMap["name"].(string), IconURL: valueMap["iconUrl"].(string), + Description: valueMap["description"].(string), + Locale: valueMap["locale"].(string), + Appearance: valueMap["appearance"].(string), ExternalURL: valueMap["externalUrl"].(string), } } diff --git a/web/src/components/AppearanceSelect.tsx b/web/src/components/AppearanceSelect.tsx index 69a00942..2cddf1f8 100644 --- a/web/src/components/AppearanceSelect.tsx +++ b/web/src/components/AppearanceSelect.tsx @@ -1,16 +1,19 @@ import { Option, Select } from "@mui/joy"; +import { FC } from "react"; import { useTranslation } from "react-i18next"; -import { useGlobalStore, useUserStore } from "../store/module"; import Icon from "./Icon"; +interface Props { + value: Appearance; + onChange: (appearance: Appearance) => void; + className?: string; +} + const appearanceList = ["system", "light", "dark"]; -const AppearanceSelect = () => { +const AppearanceSelect: FC = (props: Props) => { + const { onChange, value, className } = props; const { t } = useTranslation(); - const globalStore = useGlobalStore(); - const userStore = useUserStore(); - const { appearance } = globalStore.state; - const user = userStore.state.user; const getPrefixIcon = (apperance: Appearance) => { const className = "w-4 h-auto"; @@ -24,22 +27,19 @@ const AppearanceSelect = () => { }; const handleSelectChange = async (appearance: Appearance) => { - if (user) { - await userStore.upsertUserSetting("appearance", appearance); - } - globalStore.setAppearance(appearance); + onChange(appearance); }; return ( } + value={value} + onChange={(_, value) => handleSelectChange(value as Locale)} + > + + + + + + + + + + ); +}; + +export default LocaleSelect; diff --git a/web/src/components/Settings/PreferencesSection.tsx b/web/src/components/Settings/PreferencesSection.tsx index 48cfbe79..bb678209 100644 --- a/web/src/components/Settings/PreferencesSection.tsx +++ b/web/src/components/Settings/PreferencesSection.tsx @@ -2,49 +2,15 @@ import { Select, Switch, Option } from "@mui/joy"; import { useTranslation } from "react-i18next"; import { useGlobalStore, useUserStore } from "../../store/module"; import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts"; -import Icon from "../Icon"; import AppearanceSelect from "../AppearanceSelect"; +import LocaleSelect from "../LocaleSelect"; import "../../less/settings/preferences-section.less"; -const localeSelectorItems = [ - { - text: "English", - value: "en", - }, - { - text: "中文", - value: "zh", - }, - { - text: "Tiếng Việt", - value: "vi", - }, - { - text: "French", - value: "fr", - }, - { - text: "Nederlands", - value: "nl", - }, - { - text: "Svenska", - value: "sv", - }, - { - text: "German", - value: "de", - }, - { - text: "Español", - value: "es", - }, -]; - const PreferencesSection = () => { const { t } = useTranslation(); const globalStore = useGlobalStore(); const userStore = useUserStore(); + const { appearance, locale } = globalStore.state; const { setting, localSetting } = userStore.state.user as User; const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => { return { @@ -60,9 +26,14 @@ const PreferencesSection = () => { }; }); - const handleLocaleChanged = async (value: string) => { - await userStore.upsertUserSetting("locale", value); - globalStore.setLocale(value as Locale); + const handleLocaleSelectChange = async (locale: Locale) => { + await userStore.upsertUserSetting("locale", locale); + globalStore.setLocale(locale); + }; + + const handleAppearanceSelectChange = async (appearance: Appearance) => { + await userStore.upsertUserSetting("appearance", appearance); + globalStore.setAppearance(appearance); }; const handleDefaultMemoVisibilityChanged = async (value: string) => { @@ -82,26 +53,11 @@ const PreferencesSection = () => {

{t("common.basic")}

{t("common.language")} - +
{t("setting.preference-section.theme")} - +

{t("setting.preference")}

diff --git a/web/src/components/UpdateCustomizedProfileDialog.tsx b/web/src/components/UpdateCustomizedProfileDialog.tsx index b189df4c..8b30bf3b 100644 --- a/web/src/components/UpdateCustomizedProfileDialog.tsx +++ b/web/src/components/UpdateCustomizedProfileDialog.tsx @@ -5,6 +5,8 @@ import * as api from "../helpers/api"; import Icon from "./Icon"; import { generateDialog } from "./Dialog"; import toastHelper from "./Toast"; +import LocaleSelect from "./LocaleSelect"; +import AppearanceSelect from "./AppearanceSelect"; type Props = DialogProps; @@ -39,6 +41,33 @@ const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => { }); }; + const handleDescriptionChanged = (e: React.ChangeEvent) => { + setState((state) => { + return { + ...state, + description: e.target.value as string, + }; + }); + }; + + const handleLocaleSelectChange = (locale: Locale) => { + setState((state) => { + return { + ...state, + locale: locale, + }; + }); + }; + + const handleAppearanceSelectChange = (appearance: Appearance) => { + setState((state) => { + return { + ...state, + appearance: appearance, + }; + }); + }; + const handleSaveBtnClick = async () => { if (state.name === "" || state.iconUrl === "") { toastHelper.error(t("message.fill-all")); @@ -61,13 +90,13 @@ const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => { return ( <> -
+

{t("setting.system-section.customize-server.title")}

-
+

{t("setting.system-section.server-name")} ({t("setting.system-section.customize-server.default")}) @@ -75,6 +104,12 @@ const UpdateCustomizedProfileDialog: React.FC = ({ destroy }: Props) => {

{t("setting.system-section.customize-server.icon-url")}

+

Description

+ +

Server locale

+ +

Server appearance

+
{t("common.cancel")} diff --git a/web/src/pages/Auth.tsx b/web/src/pages/Auth.tsx index d79f9d2b..10842f11 100644 --- a/web/src/pages/Auth.tsx +++ b/web/src/pages/Auth.tsx @@ -1,4 +1,3 @@ -import { Option, Select } from "@mui/joy"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; @@ -9,6 +8,7 @@ import useLoading from "../hooks/useLoading"; import Icon from "../components/Icon"; import toastHelper from "../components/Toast"; import AppearanceSelect from "../components/AppearanceSelect"; +import LocaleSelect from "../components/LocaleSelect"; import "../less/auth.less"; const validateConfig: ValidatorConfig = { @@ -19,12 +19,12 @@ const validateConfig: ValidatorConfig = { }; const Auth = () => { - const { t, i18n } = useTranslation(); + const { t } = useTranslation(); const navigate = useNavigate(); const globalStore = useGlobalStore(); const userStore = useUserStore(); const actionBtnLoadingState = useLoading(false); - const systemStatus = globalStore.state.systemStatus; + const { appearance, locale, systemStatus } = globalStore.state; const mode = systemStatus.profile.mode; const [username, setUsername] = useState(mode === "dev" ? "demohero" : ""); const [password, setPassword] = useState(mode === "dev" ? "secret" : ""); @@ -43,6 +43,14 @@ const Auth = () => { setPassword(text); }; + const handleLocaleSelectChange = (locale: Locale) => { + globalStore.setLocale(locale); + }; + + const handleAppearanceSelectChange = (appearance: Appearance) => { + globalStore.setAppearance(appearance); + }; + const handleSigninBtnsClick = async () => { if (actionBtnLoadingState.isLoading) { return; @@ -109,10 +117,6 @@ const Auth = () => { actionBtnLoadingState.setFinish(); }; - const handleLocaleItemClick = (locale: Locale) => { - globalStore.setLocale(locale); - }; - return (
@@ -122,7 +126,7 @@ const Auth = () => {

{systemStatus.customizedProfile.name}

-

{t("slogan")}

+

{systemStatus.customizedProfile.description || t("slogan")}

@@ -167,22 +171,8 @@ const Auth = () => { {!systemStatus?.host &&

{t("auth.host-tip")}

}
- - + +
diff --git a/web/src/store/module/global.ts b/web/src/store/module/global.ts index 14c3b8ff..687cc9e6 100644 --- a/web/src/store/module/global.ts +++ b/web/src/store/module/global.ts @@ -13,7 +13,10 @@ export const initialGlobalState = async () => { additionalScript: "", customizedProfile: { name: "memos", - iconUrl: "/logo.webp", + iconUrl: "https://usememos.com/logo.webp", + description: "", + locale: "en", + appearance: "system", externalUrl: "", }, } as SystemStatus, @@ -31,6 +34,8 @@ export const initialGlobalState = async () => { const { data } = (await api.getSystemStatus()).data; if (data) { defaultGlobalState.systemStatus = data; + defaultGlobalState.locale = data.customizedProfile.locale; + defaultGlobalState.appearance = data.customizedProfile.appearance; } } catch (error) { // do nth diff --git a/web/src/store/reducer/global.ts b/web/src/store/reducer/global.ts index 9de1f808..089dca26 100644 --- a/web/src/store/reducer/global.ts +++ b/web/src/store/reducer/global.ts @@ -23,7 +23,10 @@ const globalSlice = createSlice({ additionalScript: "", customizedProfile: { name: "memos", - iconUrl: "/logo.webp", + iconUrl: "https://usememos.com/logo.webp", + description: "", + locale: "en", + appearance: "system", externalUrl: "", }, }, diff --git a/web/src/types/modules/system.d.ts b/web/src/types/modules/system.d.ts index 31877772..aad5bb78 100644 --- a/web/src/types/modules/system.d.ts +++ b/web/src/types/modules/system.d.ts @@ -6,6 +6,9 @@ interface Profile { interface CustomizedProfile { name: string; iconUrl: string; + description: string; + locale: Locale; + appearance: Appearance; externalUrl: string; }