mirror of
https://github.com/usememos/memos.git
synced 2025-06-05 22:09:59 +02:00
feat: customize system profile (#828)
This commit is contained in:
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SystemSettingName string
|
type SystemSettingName string
|
||||||
@ -24,6 +26,12 @@ type CustomizedProfile struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
// IconURL is the url of icon image.
|
// IconURL is the url of icon image.
|
||||||
IconURL string `json:"iconUrl"`
|
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 is the external url of server. e.g. https://usermemos.com
|
||||||
ExternalURL string `json:"externalUrl"`
|
ExternalURL string `json:"externalUrl"`
|
||||||
}
|
}
|
||||||
@ -90,15 +98,24 @@ func (upsert SystemSettingUpsert) Validate() error {
|
|||||||
return fmt.Errorf("failed to unmarshal system setting additional script value")
|
return fmt.Errorf("failed to unmarshal system setting additional script value")
|
||||||
}
|
}
|
||||||
} else if upsert.Name == SystemSettingCustomizedProfileName {
|
} else if upsert.Name == SystemSettingCustomizedProfileName {
|
||||||
value := CustomizedProfile{
|
customizedProfile := CustomizedProfile{
|
||||||
Name: "memos",
|
Name: "memos",
|
||||||
IconURL: "",
|
IconURL: "",
|
||||||
|
Description: "",
|
||||||
|
Locale: "en",
|
||||||
|
Appearance: "system",
|
||||||
ExternalURL: "",
|
ExternalURL: "",
|
||||||
}
|
}
|
||||||
err := json.Unmarshal([]byte(upsert.Value), &value)
|
err := json.Unmarshal([]byte(upsert.Value), &customizedProfile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal system setting customized profile value")
|
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 {
|
} else {
|
||||||
return fmt.Errorf("invalid system setting name")
|
return fmt.Errorf("invalid system setting name")
|
||||||
}
|
}
|
||||||
|
@ -3,6 +3,8 @@ package api
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserSettingKey string
|
type UserSettingKey string
|
||||||
@ -60,32 +62,16 @@ func (upsert UserSettingUpsert) Validate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal user setting locale value")
|
return fmt.Errorf("failed to unmarshal user setting locale value")
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(UserSettingLocaleValue, localeValue) {
|
||||||
invalid := true
|
|
||||||
for _, value := range UserSettingLocaleValue {
|
|
||||||
if localeValue == value {
|
|
||||||
invalid = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invalid {
|
|
||||||
return fmt.Errorf("invalid user setting locale value")
|
return fmt.Errorf("invalid user setting locale value")
|
||||||
}
|
}
|
||||||
} else if upsert.Key == UserSettingAppearanceKey {
|
} else if upsert.Key == UserSettingAppearanceKey {
|
||||||
appearanceValue := "light"
|
appearanceValue := "system"
|
||||||
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
|
err := json.Unmarshal([]byte(upsert.Value), &appearanceValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal user setting appearance value")
|
return fmt.Errorf("failed to unmarshal user setting appearance value")
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(UserSettingAppearanceValue, appearanceValue) {
|
||||||
invalid := true
|
|
||||||
for _, value := range UserSettingAppearanceValue {
|
|
||||||
if appearanceValue == value {
|
|
||||||
invalid = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invalid {
|
|
||||||
return fmt.Errorf("invalid user setting appearance value")
|
return fmt.Errorf("invalid user setting appearance value")
|
||||||
}
|
}
|
||||||
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
} else if upsert.Key == UserSettingMemoVisibilityKey {
|
||||||
@ -94,15 +80,7 @@ func (upsert UserSettingUpsert) Validate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
return fmt.Errorf("failed to unmarshal user setting memo visibility value")
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(UserSettingMemoVisibilityValue, memoVisibilityValue) {
|
||||||
invalid := true
|
|
||||||
for _, value := range UserSettingMemoVisibilityValue {
|
|
||||||
if memoVisibilityValue == value {
|
|
||||||
invalid = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invalid {
|
|
||||||
return fmt.Errorf("invalid user setting memo visibility value")
|
return fmt.Errorf("invalid user setting memo visibility value")
|
||||||
}
|
}
|
||||||
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
|
} else if upsert.Key == UserSettingMemoDisplayTsOptionKey {
|
||||||
@ -111,15 +89,7 @@ func (upsert UserSettingUpsert) Validate() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to unmarshal user setting memo display ts option")
|
return fmt.Errorf("failed to unmarshal user setting memo display ts option")
|
||||||
}
|
}
|
||||||
|
if !slices.Contains(UserSettingMemoDisplayTsOptionKeyValue, memoDisplayTsOption) {
|
||||||
invalid := true
|
|
||||||
for _, value := range UserSettingMemoDisplayTsOptionKeyValue {
|
|
||||||
if memoDisplayTsOption == value {
|
|
||||||
invalid = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invalid {
|
|
||||||
return fmt.Errorf("invalid user setting memo display ts option value")
|
return fmt.Errorf("invalid user setting memo display ts option value")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
7
go.mod
7
go.mod
@ -38,11 +38,14 @@ require (
|
|||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.1 // indirect
|
github.com/valyala/fasttemplate v1.2.1 // indirect
|
||||||
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // 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/text v0.3.7 // indirect
|
||||||
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // 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=
|
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 h1:zuSxTR4o9y82ebqCUJYNGJbGPo6sKVl54f/TVDObg1c=
|
||||||
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
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 h1:bvOltf3SADAfG05iRml8lAB3qjoEX5RCyN4K6G5v3N0=
|
||||||
golang.org/x/net v0.0.0-20220728030405-41545e8bf201/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
|
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-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-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-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-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10 h1:WIoqL4EROvwiPdUtaip4VcDdpZ4kha7wBWZrbVKCIZg=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
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 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
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=
|
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: "/",
|
Path: "/",
|
||||||
MaxAge: 3600 * 24 * 30,
|
MaxAge: 3600 * 24 * 30,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
|
Secure: true,
|
||||||
}
|
}
|
||||||
sess.Values[userIDContextKey] = user.ID
|
sess.Values[userIDContextKey] = user.ID
|
||||||
err := sess.Save(ctx.Request(), ctx.Response())
|
err := sess.Save(ctx.Request(), ctx.Response())
|
||||||
|
@ -48,6 +48,11 @@ func (s *Server) registerSystemRoutes(g *echo.Group) {
|
|||||||
AdditionalScript: "",
|
AdditionalScript: "",
|
||||||
CustomizedProfile: api.CustomizedProfile{
|
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{
|
systemStatus.CustomizedProfile = api.CustomizedProfile{
|
||||||
Name: valueMap["name"].(string),
|
Name: valueMap["name"].(string),
|
||||||
IconURL: valueMap["iconUrl"].(string),
|
IconURL: valueMap["iconUrl"].(string),
|
||||||
|
Description: valueMap["description"].(string),
|
||||||
|
Locale: valueMap["locale"].(string),
|
||||||
|
Appearance: valueMap["appearance"].(string),
|
||||||
ExternalURL: valueMap["externalUrl"].(string),
|
ExternalURL: valueMap["externalUrl"].(string),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,19 @@
|
|||||||
import { Option, Select } from "@mui/joy";
|
import { Option, Select } from "@mui/joy";
|
||||||
|
import { FC } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useGlobalStore, useUserStore } from "../store/module";
|
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
value: Appearance;
|
||||||
|
onChange: (appearance: Appearance) => void;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
const appearanceList = ["system", "light", "dark"];
|
const appearanceList = ["system", "light", "dark"];
|
||||||
|
|
||||||
const AppearanceSelect = () => {
|
const AppearanceSelect: FC<Props> = (props: Props) => {
|
||||||
|
const { onChange, value, className } = props;
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const globalStore = useGlobalStore();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
const { appearance } = globalStore.state;
|
|
||||||
const user = userStore.state.user;
|
|
||||||
|
|
||||||
const getPrefixIcon = (apperance: Appearance) => {
|
const getPrefixIcon = (apperance: Appearance) => {
|
||||||
const className = "w-4 h-auto";
|
const className = "w-4 h-auto";
|
||||||
@ -24,22 +27,19 @@ const AppearanceSelect = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectChange = async (appearance: Appearance) => {
|
const handleSelectChange = async (appearance: Appearance) => {
|
||||||
if (user) {
|
onChange(appearance);
|
||||||
await userStore.upsertUserSetting("appearance", appearance);
|
|
||||||
}
|
|
||||||
globalStore.setAppearance(appearance);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
className="!min-w-[10rem] w-auto text-sm"
|
className={`!min-w-[10rem] w-auto whitespace-nowrap ${className ?? ""}`}
|
||||||
value={appearance}
|
value={value}
|
||||||
onChange={(_, appearance) => {
|
onChange={(_, appearance) => {
|
||||||
if (appearance) {
|
if (appearance) {
|
||||||
handleSelectChange(appearance);
|
handleSelectChange(appearance);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
startDecorator={getPrefixIcon(appearance)}
|
startDecorator={getPrefixIcon(value)}
|
||||||
>
|
>
|
||||||
{appearanceList.map((item) => (
|
{appearanceList.map((item) => (
|
||||||
<Option key={item} value={item} className="whitespace-nowrap">
|
<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 { useTranslation } from "react-i18next";
|
||||||
import { useGlobalStore, useUserStore } from "../../store/module";
|
import { useGlobalStore, useUserStore } from "../../store/module";
|
||||||
import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
|
import { VISIBILITY_SELECTOR_ITEMS, MEMO_DISPLAY_TS_OPTION_SELECTOR_ITEMS } from "../../helpers/consts";
|
||||||
import Icon from "../Icon";
|
|
||||||
import AppearanceSelect from "../AppearanceSelect";
|
import AppearanceSelect from "../AppearanceSelect";
|
||||||
|
import LocaleSelect from "../LocaleSelect";
|
||||||
import "../../less/settings/preferences-section.less";
|
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 PreferencesSection = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
const { appearance, locale } = globalStore.state;
|
||||||
const { setting, localSetting } = userStore.state.user as User;
|
const { setting, localSetting } = userStore.state.user as User;
|
||||||
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
const visibilitySelectorItems = VISIBILITY_SELECTOR_ITEMS.map((item) => {
|
||||||
return {
|
return {
|
||||||
@ -60,9 +26,14 @@ const PreferencesSection = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleLocaleChanged = async (value: string) => {
|
const handleLocaleSelectChange = async (locale: Locale) => {
|
||||||
await userStore.upsertUserSetting("locale", value);
|
await userStore.upsertUserSetting("locale", locale);
|
||||||
globalStore.setLocale(value as Locale);
|
globalStore.setLocale(locale);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppearanceSelectChange = async (appearance: Appearance) => {
|
||||||
|
await userStore.upsertUserSetting("appearance", appearance);
|
||||||
|
globalStore.setAppearance(appearance);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDefaultMemoVisibilityChanged = async (value: string) => {
|
const handleDefaultMemoVisibilityChanged = async (value: string) => {
|
||||||
@ -82,26 +53,11 @@ const PreferencesSection = () => {
|
|||||||
<p className="title-text">{t("common.basic")}</p>
|
<p className="title-text">{t("common.basic")}</p>
|
||||||
<div className="form-label selector">
|
<div className="form-label selector">
|
||||||
<span className="normal-text">{t("common.language")}</span>
|
<span className="normal-text">{t("common.language")}</span>
|
||||||
<Select
|
<LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
|
||||||
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>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="form-label selector">
|
<div className="form-label selector">
|
||||||
<span className="normal-text">{t("setting.preference-section.theme")}</span>
|
<span className="normal-text">{t("setting.preference-section.theme")}</span>
|
||||||
<AppearanceSelect />
|
<AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
|
||||||
</div>
|
</div>
|
||||||
<p className="title-text">{t("setting.preference")}</p>
|
<p className="title-text">{t("setting.preference")}</p>
|
||||||
<div className="form-label selector">
|
<div className="form-label selector">
|
||||||
|
@ -5,6 +5,8 @@ import * as api from "../helpers/api";
|
|||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { generateDialog } from "./Dialog";
|
import { generateDialog } from "./Dialog";
|
||||||
import toastHelper from "./Toast";
|
import toastHelper from "./Toast";
|
||||||
|
import LocaleSelect from "./LocaleSelect";
|
||||||
|
import AppearanceSelect from "./AppearanceSelect";
|
||||||
|
|
||||||
type Props = DialogProps;
|
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 () => {
|
const handleSaveBtnClick = async () => {
|
||||||
if (state.name === "" || state.iconUrl === "") {
|
if (state.name === "" || state.iconUrl === "") {
|
||||||
toastHelper.error(t("message.fill-all"));
|
toastHelper.error(t("message.fill-all"));
|
||||||
@ -61,13 +90,13 @@ const UpdateCustomizedProfileDialog: React.FC<Props> = ({ destroy }: Props) => {
|
|||||||
|
|
||||||
return (
|
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>
|
<p className="title-text">{t("setting.system-section.customize-server.title")}</p>
|
||||||
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
<button className="btn close-btn" onClick={handleCloseBtnClick}>
|
||||||
<Icon.X />
|
<Icon.X />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="dialog-content-container">
|
<div className="dialog-content-container !w-80">
|
||||||
<p className="text-sm mb-1">
|
<p className="text-sm mb-1">
|
||||||
{t("setting.system-section.server-name")}
|
{t("setting.system-section.server-name")}
|
||||||
<span className="text-sm text-gray-400 ml-1">({t("setting.system-section.customize-server.default")})</span>
|
<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} />
|
<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>
|
<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} />
|
<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">
|
<div className="mt-4 w-full flex flex-row justify-end items-center space-x-2">
|
||||||
<span className="btn-text" onClick={handleCloseBtnClick}>
|
<span className="btn-text" onClick={handleCloseBtnClick}>
|
||||||
{t("common.cancel")}
|
{t("common.cancel")}
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import { Option, Select } from "@mui/joy";
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@ -9,6 +8,7 @@ import useLoading from "../hooks/useLoading";
|
|||||||
import Icon from "../components/Icon";
|
import Icon from "../components/Icon";
|
||||||
import toastHelper from "../components/Toast";
|
import toastHelper from "../components/Toast";
|
||||||
import AppearanceSelect from "../components/AppearanceSelect";
|
import AppearanceSelect from "../components/AppearanceSelect";
|
||||||
|
import LocaleSelect from "../components/LocaleSelect";
|
||||||
import "../less/auth.less";
|
import "../less/auth.less";
|
||||||
|
|
||||||
const validateConfig: ValidatorConfig = {
|
const validateConfig: ValidatorConfig = {
|
||||||
@ -19,12 +19,12 @@ const validateConfig: ValidatorConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Auth = () => {
|
const Auth = () => {
|
||||||
const { t, i18n } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const globalStore = useGlobalStore();
|
const globalStore = useGlobalStore();
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const actionBtnLoadingState = useLoading(false);
|
const actionBtnLoadingState = useLoading(false);
|
||||||
const systemStatus = globalStore.state.systemStatus;
|
const { appearance, locale, systemStatus } = globalStore.state;
|
||||||
const mode = systemStatus.profile.mode;
|
const mode = systemStatus.profile.mode;
|
||||||
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
const [username, setUsername] = useState(mode === "dev" ? "demohero" : "");
|
||||||
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
const [password, setPassword] = useState(mode === "dev" ? "secret" : "");
|
||||||
@ -43,6 +43,14 @@ const Auth = () => {
|
|||||||
setPassword(text);
|
setPassword(text);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleLocaleSelectChange = (locale: Locale) => {
|
||||||
|
globalStore.setLocale(locale);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAppearanceSelectChange = (appearance: Appearance) => {
|
||||||
|
globalStore.setAppearance(appearance);
|
||||||
|
};
|
||||||
|
|
||||||
const handleSigninBtnsClick = async () => {
|
const handleSigninBtnsClick = async () => {
|
||||||
if (actionBtnLoadingState.isLoading) {
|
if (actionBtnLoadingState.isLoading) {
|
||||||
return;
|
return;
|
||||||
@ -109,10 +117,6 @@ const Auth = () => {
|
|||||||
actionBtnLoadingState.setFinish();
|
actionBtnLoadingState.setFinish();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLocaleItemClick = (locale: Locale) => {
|
|
||||||
globalStore.setLocale(locale);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="page-wrapper auth">
|
<div className="page-wrapper auth">
|
||||||
<div className="page-container">
|
<div className="page-container">
|
||||||
@ -122,7 +126,7 @@ const Auth = () => {
|
|||||||
<img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
|
<img className="logo-img" src={systemStatus.customizedProfile.iconUrl} alt="" />
|
||||||
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
<p className="logo-text">{systemStatus.customizedProfile.name}</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="slogan-text">{t("slogan")}</p>
|
<p className="slogan-text">{systemStatus.customizedProfile.description || t("slogan")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
|
<div className={`page-content-container ${actionBtnLoadingState.isLoading ? "requesting" : ""}`}>
|
||||||
<div className="form-item-container input-form-container">
|
<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>}
|
{!systemStatus?.host && <p className="tip-text">{t("auth.host-tip")}</p>}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center justify-center w-full gap-2">
|
<div className="flex flex-row items-center justify-center w-full gap-2">
|
||||||
<Select
|
<LocaleSelect value={locale} onChange={handleLocaleSelectChange} />
|
||||||
className="!min-w-[9rem] w-auto whitespace-nowrap"
|
<AppearanceSelect value={appearance} onChange={handleAppearanceSelectChange} />
|
||||||
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 />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -13,7 +13,10 @@ export const initialGlobalState = async () => {
|
|||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
name: "memos",
|
name: "memos",
|
||||||
iconUrl: "/logo.webp",
|
iconUrl: "https://usememos.com/logo.webp",
|
||||||
|
description: "",
|
||||||
|
locale: "en",
|
||||||
|
appearance: "system",
|
||||||
externalUrl: "",
|
externalUrl: "",
|
||||||
},
|
},
|
||||||
} as SystemStatus,
|
} as SystemStatus,
|
||||||
@ -31,6 +34,8 @@ export const initialGlobalState = async () => {
|
|||||||
const { data } = (await api.getSystemStatus()).data;
|
const { data } = (await api.getSystemStatus()).data;
|
||||||
if (data) {
|
if (data) {
|
||||||
defaultGlobalState.systemStatus = data;
|
defaultGlobalState.systemStatus = data;
|
||||||
|
defaultGlobalState.locale = data.customizedProfile.locale;
|
||||||
|
defaultGlobalState.appearance = data.customizedProfile.appearance;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// do nth
|
// do nth
|
||||||
|
@ -23,7 +23,10 @@ const globalSlice = createSlice({
|
|||||||
additionalScript: "",
|
additionalScript: "",
|
||||||
customizedProfile: {
|
customizedProfile: {
|
||||||
name: "memos",
|
name: "memos",
|
||||||
iconUrl: "/logo.webp",
|
iconUrl: "https://usememos.com/logo.webp",
|
||||||
|
description: "",
|
||||||
|
locale: "en",
|
||||||
|
appearance: "system",
|
||||||
externalUrl: "",
|
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 {
|
interface CustomizedProfile {
|
||||||
name: string;
|
name: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
|
description: string;
|
||||||
|
locale: Locale;
|
||||||
|
appearance: Appearance;
|
||||||
externalUrl: string;
|
externalUrl: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user