mirror of https://github.com/usememos/memos.git
chore: theme in user setting
This commit is contained in:
parent
f907619752
commit
533591af2b
|
|
@ -369,6 +369,11 @@ message UserSetting {
|
||||||
|
|
||||||
// The default visibility of the memo.
|
// The default visibility of the memo.
|
||||||
string memo_visibility = 4 [(google.api.field_behavior) = OPTIONAL];
|
string memo_visibility = 4 [(google.api.field_behavior) = OPTIONAL];
|
||||||
|
|
||||||
|
// The preferred theme of the user.
|
||||||
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
|
// If not set, the default theme will be used.
|
||||||
|
string theme = 5 [(google.api.field_behavior) = OPTIONAL];
|
||||||
}
|
}
|
||||||
|
|
||||||
message GetUserSettingRequest {
|
message GetUserSettingRequest {
|
||||||
|
|
|
||||||
|
|
@ -943,8 +943,12 @@ type UserSetting struct {
|
||||||
Appearance string `protobuf:"bytes,3,opt,name=appearance,proto3" json:"appearance,omitempty"`
|
Appearance string `protobuf:"bytes,3,opt,name=appearance,proto3" json:"appearance,omitempty"`
|
||||||
// The default visibility of the memo.
|
// The default visibility of the memo.
|
||||||
MemoVisibility string `protobuf:"bytes,4,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
MemoVisibility string `protobuf:"bytes,4,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// The preferred theme of the user.
|
||||||
sizeCache protoimpl.SizeCache
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
|
// If not set, the default theme will be used.
|
||||||
|
Theme string `protobuf:"bytes,5,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *UserSetting) Reset() {
|
func (x *UserSetting) Reset() {
|
||||||
|
|
@ -1005,6 +1009,13 @@ func (x *UserSetting) GetMemoVisibility() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *UserSetting) GetTheme() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Theme
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type GetUserSettingRequest struct {
|
type GetUserSettingRequest struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
// Required. The resource name of the user.
|
// Required. The resource name of the user.
|
||||||
|
|
@ -2005,14 +2016,15 @@ const file_api_v1_user_service_proto_rawDesc = "" +
|
||||||
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" +
|
"\x16memos.api.v1/UserStats\x12\fusers/{user}*\tuserStats2\tuserStats\"D\n" +
|
||||||
"\x13GetUserStatsRequest\x12-\n" +
|
"\x13GetUserStatsRequest\x12-\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
||||||
"\x11memos.api.v1/UserR\x04name\"\xde\x01\n" +
|
"\x11memos.api.v1/UserR\x04name\"\xf9\x01\n" +
|
||||||
"\vUserSetting\x12\x17\n" +
|
"\vUserSetting\x12\x17\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1b\n" +
|
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12\x1b\n" +
|
||||||
"\x06locale\x18\x02 \x01(\tB\x03\xe0A\x01R\x06locale\x12#\n" +
|
"\x06locale\x18\x02 \x01(\tB\x03\xe0A\x01R\x06locale\x12#\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"appearance\x18\x03 \x01(\tB\x03\xe0A\x01R\n" +
|
"appearance\x18\x03 \x01(\tB\x03\xe0A\x01R\n" +
|
||||||
"appearance\x12,\n" +
|
"appearance\x12,\n" +
|
||||||
"\x0fmemo_visibility\x18\x04 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility:F\xeaAC\n" +
|
"\x0fmemo_visibility\x18\x04 \x01(\tB\x03\xe0A\x01R\x0ememoVisibility\x12\x19\n" +
|
||||||
|
"\x05theme\x18\x05 \x01(\tB\x03\xe0A\x01R\x05theme:F\xeaAC\n" +
|
||||||
"\x18memos.api.v1/UserSetting\x12\fusers/{user}*\fuserSettings2\vuserSetting\"F\n" +
|
"\x18memos.api.v1/UserSetting\x12\fusers/{user}*\fuserSettings2\vuserSetting\"F\n" +
|
||||||
"\x15GetUserSettingRequest\x12-\n" +
|
"\x15GetUserSettingRequest\x12-\n" +
|
||||||
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
"\x04name\x18\x01 \x01(\tB\x19\xe0A\x02\xfaA\x13\n" +
|
||||||
|
|
|
||||||
|
|
@ -2218,6 +2218,12 @@ paths:
|
||||||
memoVisibility:
|
memoVisibility:
|
||||||
type: string
|
type: string
|
||||||
description: The default visibility of the memo.
|
description: The default visibility of the memo.
|
||||||
|
theme:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
The preferred theme of the user.
|
||||||
|
This references a CSS file in the web/public/themes/ directory.
|
||||||
|
If not set, the default theme will be used.
|
||||||
title: Required. The user setting to update.
|
title: Required. The user setting to update.
|
||||||
required:
|
required:
|
||||||
- setting
|
- setting
|
||||||
|
|
@ -2866,6 +2872,12 @@ definitions:
|
||||||
memoVisibility:
|
memoVisibility:
|
||||||
type: string
|
type: string
|
||||||
description: The default visibility of the memo.
|
description: The default visibility of the memo.
|
||||||
|
theme:
|
||||||
|
type: string
|
||||||
|
description: |-
|
||||||
|
The preferred theme of the user.
|
||||||
|
This references a CSS file in the web/public/themes/ directory.
|
||||||
|
If not set, the default theme will be used.
|
||||||
title: User settings message
|
title: User settings message
|
||||||
apiv1Webhook:
|
apiv1Webhook:
|
||||||
type: object
|
type: object
|
||||||
|
|
|
||||||
|
|
@ -239,8 +239,11 @@ type GeneralUserSetting struct {
|
||||||
Appearance string `protobuf:"bytes,2,opt,name=appearance,proto3" json:"appearance,omitempty"`
|
Appearance string `protobuf:"bytes,2,opt,name=appearance,proto3" json:"appearance,omitempty"`
|
||||||
// The user's memo visibility setting.
|
// The user's memo visibility setting.
|
||||||
MemoVisibility string `protobuf:"bytes,3,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
MemoVisibility string `protobuf:"bytes,3,opt,name=memo_visibility,json=memoVisibility,proto3" json:"memo_visibility,omitempty"`
|
||||||
unknownFields protoimpl.UnknownFields
|
// The user's theme preference.
|
||||||
sizeCache protoimpl.SizeCache
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
|
Theme string `protobuf:"bytes,4,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *GeneralUserSetting) Reset() {
|
func (x *GeneralUserSetting) Reset() {
|
||||||
|
|
@ -294,6 +297,13 @@ func (x *GeneralUserSetting) GetMemoVisibility() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *GeneralUserSetting) GetTheme() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Theme
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
type SessionsUserSetting struct {
|
type SessionsUserSetting struct {
|
||||||
state protoimpl.MessageState `protogen:"open.v1"`
|
state protoimpl.MessageState `protogen:"open.v1"`
|
||||||
Sessions []*SessionsUserSetting_Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
|
Sessions []*SessionsUserSetting_Session `protobuf:"bytes,1,rep,name=sessions,proto3" json:"sessions,omitempty"`
|
||||||
|
|
@ -822,13 +832,14 @@ const file_store_user_setting_proto_rawDesc = "" +
|
||||||
"\rACCESS_TOKENS\x10\x03\x12\r\n" +
|
"\rACCESS_TOKENS\x10\x03\x12\r\n" +
|
||||||
"\tSHORTCUTS\x10\x04\x12\f\n" +
|
"\tSHORTCUTS\x10\x04\x12\f\n" +
|
||||||
"\bWEBHOOKS\x10\x05B\a\n" +
|
"\bWEBHOOKS\x10\x05B\a\n" +
|
||||||
"\x05value\"u\n" +
|
"\x05value\"\x8b\x01\n" +
|
||||||
"\x12GeneralUserSetting\x12\x16\n" +
|
"\x12GeneralUserSetting\x12\x16\n" +
|
||||||
"\x06locale\x18\x01 \x01(\tR\x06locale\x12\x1e\n" +
|
"\x06locale\x18\x01 \x01(\tR\x06locale\x12\x1e\n" +
|
||||||
"\n" +
|
"\n" +
|
||||||
"appearance\x18\x02 \x01(\tR\n" +
|
"appearance\x18\x02 \x01(\tR\n" +
|
||||||
"appearance\x12'\n" +
|
"appearance\x12'\n" +
|
||||||
"\x0fmemo_visibility\x18\x03 \x01(\tR\x0ememoVisibility\"\xf3\x03\n" +
|
"\x0fmemo_visibility\x18\x03 \x01(\tR\x0ememoVisibility\x12\x14\n" +
|
||||||
|
"\x05theme\x18\x04 \x01(\tR\x05theme\"\xf3\x03\n" +
|
||||||
"\x13SessionsUserSetting\x12D\n" +
|
"\x13SessionsUserSetting\x12D\n" +
|
||||||
"\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" +
|
"\bsessions\x18\x01 \x03(\v2(.memos.store.SessionsUserSetting.SessionR\bsessions\x1a\xfd\x01\n" +
|
||||||
"\aSession\x12\x1d\n" +
|
"\aSession\x12\x1d\n" +
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,9 @@ message GeneralUserSetting {
|
||||||
string appearance = 2;
|
string appearance = 2;
|
||||||
// The user's memo visibility setting.
|
// The user's memo visibility setting.
|
||||||
string memo_visibility = 3;
|
string memo_visibility = 3;
|
||||||
|
// The user's theme preference.
|
||||||
|
// This references a CSS file in the web/public/themes/ directory.
|
||||||
|
string theme = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
message SessionsUserSetting {
|
message SessionsUserSetting {
|
||||||
|
|
|
||||||
|
|
@ -249,7 +249,8 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
|
||||||
return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err)
|
return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err)
|
||||||
}
|
}
|
||||||
for _, field := range request.UpdateMask.Paths {
|
for _, field := range request.UpdateMask.Paths {
|
||||||
if field == "username" {
|
switch field {
|
||||||
|
case "username":
|
||||||
if workspaceGeneralSetting.DisallowChangeUsername {
|
if workspaceGeneralSetting.DisallowChangeUsername {
|
||||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change username")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change username")
|
||||||
}
|
}
|
||||||
|
|
@ -257,35 +258,35 @@ func (s *APIV1Service) UpdateUser(ctx context.Context, request *v1pb.UpdateUserR
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid username: %s", request.User.Username)
|
||||||
}
|
}
|
||||||
update.Username = &request.User.Username
|
update.Username = &request.User.Username
|
||||||
} else if field == "display_name" {
|
case "display_name":
|
||||||
if workspaceGeneralSetting.DisallowChangeNickname {
|
if workspaceGeneralSetting.DisallowChangeNickname {
|
||||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change nickname")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied: disallow change nickname")
|
||||||
}
|
}
|
||||||
update.Nickname = &request.User.DisplayName
|
update.Nickname = &request.User.DisplayName
|
||||||
} else if field == "email" {
|
case "email":
|
||||||
update.Email = &request.User.Email
|
update.Email = &request.User.Email
|
||||||
} else if field == "avatar_url" {
|
case "avatar_url":
|
||||||
update.AvatarURL = &request.User.AvatarUrl
|
update.AvatarURL = &request.User.AvatarUrl
|
||||||
} else if field == "description" {
|
case "description":
|
||||||
update.Description = &request.User.Description
|
update.Description = &request.User.Description
|
||||||
} else if field == "role" {
|
case "role":
|
||||||
// Only allow admin to update role.
|
// Only allow admin to update role.
|
||||||
if currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
if currentUser.Role != store.RoleAdmin && currentUser.Role != store.RoleHost {
|
||||||
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
return nil, status.Errorf(codes.PermissionDenied, "permission denied")
|
||||||
}
|
}
|
||||||
role := convertUserRoleToStore(request.User.Role)
|
role := convertUserRoleToStore(request.User.Role)
|
||||||
update.Role = &role
|
update.Role = &role
|
||||||
} else if field == "password" {
|
case "password":
|
||||||
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
|
passwordHash, err := bcrypt.GenerateFromPassword([]byte(request.User.Password), bcrypt.DefaultCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
|
return nil, echo.NewHTTPError(http.StatusInternalServerError, "failed to generate password hash").SetInternal(err)
|
||||||
}
|
}
|
||||||
passwordHashStr := string(passwordHash)
|
passwordHashStr := string(passwordHash)
|
||||||
update.PasswordHash = &passwordHashStr
|
update.PasswordHash = &passwordHashStr
|
||||||
} else if field == "state" {
|
case "state":
|
||||||
rowStatus := convertStateToStore(request.User.State)
|
rowStatus := convertStateToStore(request.User.State)
|
||||||
update.RowStatus = &rowStatus
|
update.RowStatus = &rowStatus
|
||||||
} else {
|
default:
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -334,6 +335,7 @@ func getDefaultUserSetting() *v1pb.UserSetting {
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
Appearance: "system",
|
Appearance: "system",
|
||||||
MemoVisibility: "PRIVATE",
|
MemoVisibility: "PRIVATE",
|
||||||
|
Theme: "",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -370,9 +372,24 @@ func (s *APIV1Service) GetUserSetting(ctx context.Context, request *v1pb.GetUser
|
||||||
userSettingMessage.Locale = general.Locale
|
userSettingMessage.Locale = general.Locale
|
||||||
userSettingMessage.Appearance = general.Appearance
|
userSettingMessage.Appearance = general.Appearance
|
||||||
userSettingMessage.MemoVisibility = general.MemoVisibility
|
userSettingMessage.MemoVisibility = general.MemoVisibility
|
||||||
|
userSettingMessage.Theme = general.Theme
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Backfill theme if empty: use workspace theme or default to "default"
|
||||||
|
if userSettingMessage.Theme == "" {
|
||||||
|
workspaceGeneralSetting, err := s.Store.GetWorkspaceGeneralSetting(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, status.Errorf(codes.Internal, "failed to get workspace general setting: %v", err)
|
||||||
|
}
|
||||||
|
workspaceTheme := workspaceGeneralSetting.Theme
|
||||||
|
if workspaceTheme == "" {
|
||||||
|
workspaceTheme = "default"
|
||||||
|
}
|
||||||
|
userSettingMessage.Theme = workspaceTheme
|
||||||
|
}
|
||||||
|
|
||||||
return userSettingMessage, nil
|
return userSettingMessage, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -411,6 +428,7 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
||||||
Locale: "en",
|
Locale: "en",
|
||||||
Appearance: "system",
|
Appearance: "system",
|
||||||
MemoVisibility: "PRIVATE",
|
MemoVisibility: "PRIVATE",
|
||||||
|
Theme: "",
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there's an existing setting, use its values as defaults
|
// If there's an existing setting, use its values as defaults
|
||||||
|
|
@ -419,6 +437,7 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
||||||
generalSetting.Locale = existing.Locale
|
generalSetting.Locale = existing.Locale
|
||||||
generalSetting.Appearance = existing.Appearance
|
generalSetting.Appearance = existing.Appearance
|
||||||
generalSetting.MemoVisibility = existing.MemoVisibility
|
generalSetting.MemoVisibility = existing.MemoVisibility
|
||||||
|
generalSetting.Theme = existing.Theme
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply updates based on the update mask
|
// Apply updates based on the update mask
|
||||||
|
|
@ -430,6 +449,8 @@ func (s *APIV1Service) UpdateUserSetting(ctx context.Context, request *v1pb.Upda
|
||||||
generalSetting.Appearance = request.Setting.Appearance
|
generalSetting.Appearance = request.Setting.Appearance
|
||||||
case "memo_visibility":
|
case "memo_visibility":
|
||||||
generalSetting.MemoVisibility = request.Setting.MemoVisibility
|
generalSetting.MemoVisibility = request.Setting.MemoVisibility
|
||||||
|
case "theme":
|
||||||
|
generalSetting.Theme = request.Setting.Theme
|
||||||
default:
|
default:
|
||||||
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
|
return nil, status.Errorf(codes.InvalidArgument, "invalid update path: %s", field)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -149,8 +149,14 @@ func convertWorkspaceGeneralSettingFromStore(setting *storepb.WorkspaceGeneralSe
|
||||||
if setting == nil {
|
if setting == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
// Backfill theme if empty
|
||||||
|
theme := setting.Theme
|
||||||
|
if theme == "" {
|
||||||
|
theme = "default"
|
||||||
|
}
|
||||||
|
|
||||||
generalSetting := &v1pb.WorkspaceGeneralSetting{
|
generalSetting := &v1pb.WorkspaceGeneralSetting{
|
||||||
Theme: setting.Theme,
|
Theme: theme,
|
||||||
DisallowUserRegistration: setting.DisallowUserRegistration,
|
DisallowUserRegistration: setting.DisallowUserRegistration,
|
||||||
DisallowPasswordAuth: setting.DisallowPasswordAuth,
|
DisallowPasswordAuth: setting.DisallowPasswordAuth,
|
||||||
AdditionalScript: setting.AdditionalScript,
|
AdditionalScript: setting.AdditionalScript,
|
||||||
|
|
|
||||||
|
|
@ -104,10 +104,12 @@ const App = observer(() => {
|
||||||
});
|
});
|
||||||
}, [userSetting?.locale, userSetting?.appearance]);
|
}, [userSetting?.locale, userSetting?.appearance]);
|
||||||
|
|
||||||
// Load theme when workspace setting changes, validate API response
|
// Load theme when user setting changes (user theme is already backfilled with workspace theme)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadTheme(workspaceGeneralSetting.theme);
|
if (userSetting?.theme) {
|
||||||
}, [workspaceGeneralSetting.theme]);
|
loadTheme(userSetting.theme);
|
||||||
|
}
|
||||||
|
}, [userSetting?.theme]);
|
||||||
|
|
||||||
return <Outlet />;
|
return <Outlet />;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,12 @@ import { useTranslate } from "@/utils/i18n";
|
||||||
interface Props {
|
interface Props {
|
||||||
value: Appearance;
|
value: Appearance;
|
||||||
onChange: (appearance: Appearance) => void;
|
onChange: (appearance: Appearance) => void;
|
||||||
className?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const appearanceList = ["system", "light", "dark"] as const;
|
const appearanceList = ["system", "light", "dark"] as const;
|
||||||
|
|
||||||
const AppearanceSelect: FC<Props> = (props: Props) => {
|
const AppearanceSelect: FC<Props> = (props: Props) => {
|
||||||
const { onChange, value, className } = props;
|
const { onChange, value } = props;
|
||||||
const t = useTranslate();
|
const t = useTranslate();
|
||||||
|
|
||||||
const getPrefixIcon = (appearance: Appearance) => {
|
const getPrefixIcon = (appearance: Appearance) => {
|
||||||
|
|
@ -32,7 +31,7 @@ const AppearanceSelect: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={value} onValueChange={handleSelectChange}>
|
<Select value={value} onValueChange={handleSelectChange}>
|
||||||
<SelectTrigger className={`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`}>
|
<SelectTrigger>
|
||||||
<SelectValue placeholder="Select appearance" />
|
<SelectValue placeholder="Select appearance" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
|
|
|
||||||
|
|
@ -5,12 +5,11 @@ import { locales } from "@/i18n";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value: Locale;
|
value: Locale;
|
||||||
className?: string;
|
|
||||||
onChange: (locale: Locale) => void;
|
onChange: (locale: Locale) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const LocaleSelect: FC<Props> = (props: Props) => {
|
const LocaleSelect: FC<Props> = (props: Props) => {
|
||||||
const { onChange, value, className } = props;
|
const { onChange, value } = props;
|
||||||
|
|
||||||
const handleSelectChange = async (locale: Locale) => {
|
const handleSelectChange = async (locale: Locale) => {
|
||||||
onChange(locale);
|
onChange(locale);
|
||||||
|
|
@ -18,7 +17,7 @@ const LocaleSelect: FC<Props> = (props: Props) => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select value={value} onValueChange={handleSelectChange}>
|
<Select value={value} onValueChange={handleSelectChange}>
|
||||||
<SelectTrigger className={`min-w-40 w-auto whitespace-nowrap ${className ?? ""}`}>
|
<SelectTrigger>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<GlobeIcon className="w-4 h-auto" />
|
<GlobeIcon className="w-4 h-auto" />
|
||||||
<SelectValue placeholder="Select language" />
|
<SelectValue placeholder="Select language" />
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import { useTranslate } from "@/utils/i18n";
|
||||||
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
|
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
|
||||||
import AppearanceSelect from "../AppearanceSelect";
|
import AppearanceSelect from "../AppearanceSelect";
|
||||||
import LocaleSelect from "../LocaleSelect";
|
import LocaleSelect from "../LocaleSelect";
|
||||||
|
import ThemeSelector from "../ThemeSelector";
|
||||||
import VisibilityIcon from "../VisibilityIcon";
|
import VisibilityIcon from "../VisibilityIcon";
|
||||||
import WebhookSection from "./WebhookSection";
|
import WebhookSection from "./WebhookSection";
|
||||||
|
|
||||||
|
|
@ -27,6 +28,10 @@ const PreferencesSection = observer(() => {
|
||||||
await userStore.updateUserSetting({ memoVisibility: value }, ["memo_visibility"]);
|
await userStore.updateUserSetting({ memoVisibility: value }, ["memo_visibility"]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleThemeChange = async (theme: string) => {
|
||||||
|
await userStore.updateUserSetting({ theme }, ["theme"]);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col gap-2 pt-2 pb-4">
|
<div className="w-full flex flex-col gap-2 pt-2 pb-4">
|
||||||
<p className="font-medium text-muted-foreground">{t("common.basic")}</p>
|
<p className="font-medium text-muted-foreground">{t("common.basic")}</p>
|
||||||
|
|
@ -41,6 +46,11 @@ const PreferencesSection = observer(() => {
|
||||||
<AppearanceSelect value={setting.appearance as Appearance} onChange={handleAppearanceSelectChange} />
|
<AppearanceSelect value={setting.appearance as Appearance} onChange={handleAppearanceSelectChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
|
<span>{t("setting.preference-section.theme")}</span>
|
||||||
|
<ThemeSelector value={setting.theme} onValueChange={handleThemeChange} />
|
||||||
|
</div>
|
||||||
|
|
||||||
<p className="font-medium text-muted-foreground">{t("setting.preference")}</p>
|
<p className="font-medium text-muted-foreground">{t("setting.preference")}</p>
|
||||||
|
|
||||||
<div className="w-full flex flex-row justify-between items-center">
|
<div className="w-full flex flex-row justify-between items-center">
|
||||||
|
|
|
||||||
|
|
@ -137,12 +137,12 @@ export function UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }:
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>{t("setting.system-section.customize-server.locale")}</Label>
|
<Label>{t("setting.system-section.customize-server.locale")}</Label>
|
||||||
<LocaleSelect className="w-full" value={customProfile.locale} onChange={handleLocaleSelectChange} />
|
<LocaleSelect value={customProfile.locale} onChange={handleLocaleSelectChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>{t("setting.system-section.customize-server.appearance")}</Label>
|
<Label>{t("setting.system-section.customize-server.appearance")}</Label>
|
||||||
<AppearanceSelect className="w-full" value={customProfile.appearance as Appearance} onChange={handleAppearanceSelectChange} />
|
<AppearanceSelect value={customProfile.appearance as Appearance} onChange={handleAppearanceSelectChange} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,12 @@ export interface UserSetting {
|
||||||
appearance: string;
|
appearance: string;
|
||||||
/** The default visibility of the memo. */
|
/** The default visibility of the memo. */
|
||||||
memoVisibility: string;
|
memoVisibility: string;
|
||||||
|
/**
|
||||||
|
* The preferred theme of the user.
|
||||||
|
* This references a CSS file in the web/public/themes/ directory.
|
||||||
|
* If not set, the default theme will be used.
|
||||||
|
*/
|
||||||
|
theme: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GetUserSettingRequest {
|
export interface GetUserSettingRequest {
|
||||||
|
|
@ -1532,7 +1538,7 @@ export const GetUserStatsRequest: MessageFns<GetUserStatsRequest> = {
|
||||||
};
|
};
|
||||||
|
|
||||||
function createBaseUserSetting(): UserSetting {
|
function createBaseUserSetting(): UserSetting {
|
||||||
return { name: "", locale: "", appearance: "", memoVisibility: "" };
|
return { name: "", locale: "", appearance: "", memoVisibility: "", theme: "" };
|
||||||
}
|
}
|
||||||
|
|
||||||
export const UserSetting: MessageFns<UserSetting> = {
|
export const UserSetting: MessageFns<UserSetting> = {
|
||||||
|
|
@ -1549,6 +1555,9 @@ export const UserSetting: MessageFns<UserSetting> = {
|
||||||
if (message.memoVisibility !== "") {
|
if (message.memoVisibility !== "") {
|
||||||
writer.uint32(34).string(message.memoVisibility);
|
writer.uint32(34).string(message.memoVisibility);
|
||||||
}
|
}
|
||||||
|
if (message.theme !== "") {
|
||||||
|
writer.uint32(42).string(message.theme);
|
||||||
|
}
|
||||||
return writer;
|
return writer;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -1591,6 +1600,14 @@ export const UserSetting: MessageFns<UserSetting> = {
|
||||||
message.memoVisibility = reader.string();
|
message.memoVisibility = reader.string();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
case 5: {
|
||||||
|
if (tag !== 42) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
message.theme = reader.string();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if ((tag & 7) === 4 || tag === 0) {
|
if ((tag & 7) === 4 || tag === 0) {
|
||||||
break;
|
break;
|
||||||
|
|
@ -1609,6 +1626,7 @@ export const UserSetting: MessageFns<UserSetting> = {
|
||||||
message.locale = object.locale ?? "";
|
message.locale = object.locale ?? "";
|
||||||
message.appearance = object.appearance ?? "";
|
message.appearance = object.appearance ?? "";
|
||||||
message.memoVisibility = object.memoVisibility ?? "";
|
message.memoVisibility = object.memoVisibility ?? "";
|
||||||
|
message.theme = object.theme ?? "";
|
||||||
return message;
|
return message;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue