mirror of
				https://github.com/usememos/memos.git
				synced 2025-11-01 01:06:04 +08:00 
			
		
		
		
	feat: customize system profile (#828)
This commit is contained in:
		
							parent
							
								
									72daa4e1d6
								
							
						
					
					
						commit
						7efa749c66
					
				
					 14 changed files with 171 additions and 141 deletions
				
			
		|  | @ -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") | ||||
| 	} | ||||
|  |  | |||
|  | @ -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 { | ||||
|  |  | |||
							
								
								
									
										7
									
								
								go.mod
									
										
									
									
									
								
							
							
						
						
									
										7
									
								
								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 | ||||
| ) | ||||
|  |  | |||
							
								
								
									
										6
									
								
								go.sum
									
										
									
									
									
								
							
							
						
						
									
										6
									
								
								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= | ||||
|  |  | |||
|  | @ -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()) | ||||
|  |  | |||
|  | @ -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), | ||||
| 				} | ||||
| 			} | ||||
|  |  | |||
|  | @ -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: 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 ( | ||||
|     <Select | ||||
|       className="!min-w-[10rem] w-auto text-sm" | ||||
|       value={appearance} | ||||
|       className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`} | ||||
|       value={value} | ||||
|       onChange={(_, appearance) => { | ||||
|         if (appearance) { | ||||
|           handleSelectChange(appearance); | ||||
|         } | ||||
|       }} | ||||
|       startDecorator={getPrefixIcon(appearance)} | ||||
|       startDecorator={getPrefixIcon(value)} | ||||
|     > | ||||
|       {appearanceList.map((item) => ( | ||||
|         <Option key={item} value={item} className="whitespace-nowrap"> | ||||
|  |  | |||
							
								
								
									
										37
									
								
								web/src/components/LocaleSelect.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								web/src/components/LocaleSelect.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,37 @@ | |||
| import { Option, Select } from "@mui/joy"; | ||||
| import { FC } from "react"; | ||||
| import Icon from "./Icon"; | ||||
| 
 | ||||
| interface Props { | ||||
|   value: Locale; | ||||
|   onChange: (locale: Locale) => void; | ||||
|   className?: string; | ||||
| } | ||||
| 
 | ||||
| const LocaleSelect: FC<Props> = (props: Props) => { | ||||
|   const { onChange, value, className } = props; | ||||
| 
 | ||||
|   const handleSelectChange = async (locale: Locale) => { | ||||
|     onChange(locale); | ||||
|   }; | ||||
| 
 | ||||
|   return ( | ||||
|     <Select | ||||
|       className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`} | ||||
|       startDecorator={<Icon.Globe className="w-4 h-auto" />} | ||||
|       value={value} | ||||
|       onChange={(_, value) => handleSelectChange(value as Locale)} | ||||
|     > | ||||
|       <Option value="en">English</Option> | ||||
|       <Option value="zh">中文</Option> | ||||
|       <Option value="vi">Tiếng Việt</Option> | ||||
|       <Option value="fr">French</Option> | ||||
|       <Option value="nl">Nederlands</Option> | ||||
|       <Option value="sv">Svenska</Option> | ||||
|       <Option value="de">German</Option> | ||||
|       <Option value="es">Español</Option> | ||||
|     </Select> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| export default LocaleSelect; | ||||
|  | @ -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 = () => { | |||
|       <p className="title-text">{t("common.basic")}</p> | ||||
|       <div className="form-label selector"> | ||||
|         <span className="normal-text">{t("common.language")}</span> | ||||
|         <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> | ||||
|         <LocaleSelect value={locale} onChange={handleLocaleSelectChange} /> | ||||
|       </div> | ||||
|       <div className="form-label selector"> | ||||
|         <span className="normal-text">{t("setting.preference-section.theme")}</span> | ||||
|         <AppearanceSelect /> | ||||
|         <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} /> | ||||
|       </div> | ||||
|       <p className="title-text">{t("setting.preference")}</p> | ||||
|       <div className="form-label selector"> | ||||
|  |  | |||
|  | @ -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<Props> = ({ destroy }: Props) => { | |||
|     }); | ||||
|   }; | ||||
| 
 | ||||
|   const handleDescriptionChanged = (e: React.ChangeEvent<HTMLInputElement>) => { | ||||
|     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<Props> = ({ destroy }: Props) => { | |||
| 
 | ||||
|   return ( | ||||
|     <> | ||||
|       <div className="dialog-header-container !w-64"> | ||||
|       <div className="dialog-header-container"> | ||||
|         <p className="title-text">{t("setting.system-section.customize-server.title")}</p> | ||||
|         <button className="btn close-btn" onClick={handleCloseBtnClick}> | ||||
|           <Icon.X /> | ||||
|         </button> | ||||
|       </div> | ||||
|       <div className="dialog-content-container"> | ||||
|       <div className="dialog-content-container !w-80"> | ||||
|         <p className="text-sm mb-1"> | ||||
|           {t("setting.system-section.server-name")} | ||||
|           <span className="text-sm text-gray-400 ml-1">({t("setting.system-section.customize-server.default")})</span> | ||||
|  | @ -75,6 +104,12 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => { | |||
|         <input type="text" className="input-text" value={state.name} onChange={handleNameChanged} /> | ||||
|         <p className="text-sm mb-1 mt-2">{t("setting.system-section.customize-server.icon-url")}</p> | ||||
|         <input type="text" className="input-text" value={state.iconUrl} onChange={handleIconUrlChanged} /> | ||||
|         <p className="text-sm mb-1 mt-2">Description</p> | ||||
|         <input type="text" className="input-text" value={state.description} onChange={handleDescriptionChanged} /> | ||||
|         <p className="text-sm mb-1 mt-2">Server locale</p> | ||||
|         <LocaleSelect className="w-full" value={state.locale} onChange={handleLocaleSelectChange} /> | ||||
|         <p className="text-sm mb-1 mt-2">Server appearance</p> | ||||
|         <AppearanceSelect className="w-full" value={state.appearance} onChange={handleAppearanceSelectChange} /> | ||||
|         <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")} | ||||
|  |  | |||
|  | @ -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 ( | ||||
|     <div className="page-wrapper auth"> | ||||
|       <div className="page-container"> | ||||
|  | @ -122,7 +126,7 @@ const Auth = () => { | |||
|               <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> | ||||
|             <p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p> | ||||
|           </div> | ||||
|           <div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}> | ||||
|             <div className="form-item-container input-form-container"> | ||||
|  | @ -167,22 +171,8 @@ const Auth = () => { | |||
|           {!systemStatus?.host && <p className="tip-text">{t("auth.host-tip")}</p>} | ||||
|         </div> | ||||
|         <div className="flex flex-row items-center justify-center w-full gap-2"> | ||||
|           <Select | ||||
|             className="!min-w-[9rem] w-auto whitespace-nowrap" | ||||
|             startDecorator={<Icon.Globe className="w-4 h-auto" />} | ||||
|             value={i18n.language} | ||||
|             onChange={(_, value) => handleLocaleItemClick(value as Locale)} | ||||
|           > | ||||
|             <Option value="en">English</Option> | ||||
|             <Option value="zh">中文</Option> | ||||
|             <Option value="vi">Tiếng Việt</Option> | ||||
|             <Option value="fr">French</Option> | ||||
|             <Option value="nl">Nederlands</Option> | ||||
|             <Option value="sv">Svenska</Option> | ||||
|             <Option value="de">German</Option> | ||||
|             <Option value="es">Español</Option> | ||||
|           </Select> | ||||
|           <AppearanceSelect /> | ||||
|           <LocaleSelect value={locale} onChange={handleLocaleSelectChange} /> | ||||
|           <AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|  |  | |||
|  | @ -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
 | ||||
|  |  | |||
|  | @ -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: "", | ||||
|       }, | ||||
|     }, | ||||
|  |  | |||
							
								
								
									
										3
									
								
								web/src/types/modules/system.d.ts
									
										
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								web/src/types/modules/system.d.ts
									
										
									
									
										vendored
									
									
								
							|  | @ -6,6 +6,9 @@ interface Profile { | |||
| interface CustomizedProfile { | ||||
|   name: string; | ||||
|   iconUrl: string; | ||||
|   description: string; | ||||
|   locale: Locale; | ||||
|   appearance: Appearance; | ||||
|   externalUrl: string; | ||||
| } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		
		Reference in a new issue