mirror of https://github.com/usememos/memos.git
refactor: simplify theme/locale to user preferences and improve initialization
Remove theme and locale from instance settings to eliminate duplication and simplify the codebase. These are user-specific preferences and should only exist in user settings, not instance-wide settings. Backend changes: - Remove theme from InstanceGeneralSetting proto - Remove locale from InstanceCustomProfile proto - Update instance service converters to remove theme/locale handling - Simplify RSS feed to use static locale Frontend changes: - Remove theme/locale from instanceStore state - Create unified initialization flow with clear fallback priority: * Theme: user setting → localStorage → system preference * Locale: user setting → browser language - Add applyUserPreferences() to centralize theme/locale application - Simplify App.tsx by removing redundant state synchronization - Update all components to use new helper functions: * getThemeWithFallback() for theme resolution * getLocaleWithFallback() for locale resolution - Remove theme/locale selectors from instance profile dialog Theme utilities refactor: - Organize code into clear sections with JSDoc comments - Extract localStorage operations into getStoredTheme/setStoredTheme helpers - Split DOM manipulation into focused functions - Improve type safety with Theme and ResolvedTheme types - Reduce code duplication and improve maintainability 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
8154a411a9
commit
81da20c905
|
|
@ -83,9 +83,6 @@ message InstanceSetting {
|
|||
|
||||
// General instance settings configuration.
|
||||
message GeneralSetting {
|
||||
// theme is the name of the selected theme.
|
||||
// This references a CSS file in the web/public/themes/ directory.
|
||||
string theme = 1;
|
||||
// disallow_user_registration disallows user registration.
|
||||
bool disallow_user_registration = 2;
|
||||
// disallow_password_auth disallows password authentication.
|
||||
|
|
@ -111,7 +108,6 @@ message InstanceSetting {
|
|||
string title = 1;
|
||||
string description = 2;
|
||||
string logo_url = 3;
|
||||
string locale = 4;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/activity_service.proto
|
||||
|
||||
|
|
@ -80,10 +80,10 @@ type ActivityServiceServer interface {
|
|||
type UnimplementedActivityServiceServer struct{}
|
||||
|
||||
func (UnimplementedActivityServiceServer) ListActivities(context.Context, *ListActivitiesRequest) (*ListActivitiesResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListActivities not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListActivities not implemented")
|
||||
}
|
||||
func (UnimplementedActivityServiceServer) GetActivity(context.Context, *GetActivityRequest) (*Activity, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetActivity not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetActivity not implemented")
|
||||
}
|
||||
func (UnimplementedActivityServiceServer) mustEmbedUnimplementedActivityServiceServer() {}
|
||||
func (UnimplementedActivityServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -96,7 +96,7 @@ type UnsafeActivityServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterActivityServiceServer(s grpc.ServiceRegistrar, srv ActivityServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedActivityServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedActivityServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/attachment_service.proto
|
||||
|
||||
|
|
@ -142,22 +142,22 @@ type AttachmentServiceServer interface {
|
|||
type UnimplementedAttachmentServiceServer struct{}
|
||||
|
||||
func (UnimplementedAttachmentServiceServer) CreateAttachment(context.Context, *CreateAttachmentRequest) (*Attachment, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateAttachment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateAttachment not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) ListAttachments(context.Context, *ListAttachmentsRequest) (*ListAttachmentsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAttachments not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAttachments not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) GetAttachment(context.Context, *GetAttachmentRequest) (*Attachment, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAttachment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAttachment not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) GetAttachmentBinary(context.Context, *GetAttachmentBinaryRequest) (*httpbody.HttpBody, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetAttachmentBinary not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetAttachmentBinary not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) UpdateAttachment(context.Context, *UpdateAttachmentRequest) (*Attachment, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateAttachment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateAttachment not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) DeleteAttachment(context.Context, *DeleteAttachmentRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteAttachment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteAttachment not implemented")
|
||||
}
|
||||
func (UnimplementedAttachmentServiceServer) mustEmbedUnimplementedAttachmentServiceServer() {}
|
||||
func (UnimplementedAttachmentServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -170,7 +170,7 @@ type UnsafeAttachmentServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterAttachmentServiceServer(s grpc.ServiceRegistrar, srv AttachmentServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAttachmentServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAttachmentServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/auth_service.proto
|
||||
|
||||
|
|
@ -102,13 +102,13 @@ type AuthServiceServer interface {
|
|||
type UnimplementedAuthServiceServer struct{}
|
||||
|
||||
func (UnimplementedAuthServiceServer) GetCurrentSession(context.Context, *GetCurrentSessionRequest) (*GetCurrentSessionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetCurrentSession not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetCurrentSession not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) CreateSession(context.Context, *CreateSessionRequest) (*CreateSessionResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateSession not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateSession not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) DeleteSession(context.Context, *DeleteSessionRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteSession not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteSession not implemented")
|
||||
}
|
||||
func (UnimplementedAuthServiceServer) mustEmbedUnimplementedAuthServiceServer() {}
|
||||
func (UnimplementedAuthServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -121,7 +121,7 @@ type UnsafeAuthServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterAuthServiceServer(s grpc.ServiceRegistrar, srv AuthServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedAuthServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedAuthServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/idp_service.proto
|
||||
|
||||
|
|
@ -126,19 +126,19 @@ type IdentityProviderServiceServer interface {
|
|||
type UnimplementedIdentityProviderServiceServer struct{}
|
||||
|
||||
func (UnimplementedIdentityProviderServiceServer) ListIdentityProviders(context.Context, *ListIdentityProvidersRequest) (*ListIdentityProvidersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListIdentityProviders not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListIdentityProviders not implemented")
|
||||
}
|
||||
func (UnimplementedIdentityProviderServiceServer) GetIdentityProvider(context.Context, *GetIdentityProviderRequest) (*IdentityProvider, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetIdentityProvider not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetIdentityProvider not implemented")
|
||||
}
|
||||
func (UnimplementedIdentityProviderServiceServer) CreateIdentityProvider(context.Context, *CreateIdentityProviderRequest) (*IdentityProvider, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateIdentityProvider not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateIdentityProvider not implemented")
|
||||
}
|
||||
func (UnimplementedIdentityProviderServiceServer) UpdateIdentityProvider(context.Context, *UpdateIdentityProviderRequest) (*IdentityProvider, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateIdentityProvider not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateIdentityProvider not implemented")
|
||||
}
|
||||
func (UnimplementedIdentityProviderServiceServer) DeleteIdentityProvider(context.Context, *DeleteIdentityProviderRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteIdentityProvider not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteIdentityProvider not implemented")
|
||||
}
|
||||
func (UnimplementedIdentityProviderServiceServer) mustEmbedUnimplementedIdentityProviderServiceServer() {
|
||||
}
|
||||
|
|
@ -152,7 +152,7 @@ type UnsafeIdentityProviderServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterIdentityProviderServiceServer(s grpc.ServiceRegistrar, srv IdentityProviderServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedIdentityProviderServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedIdentityProviderServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -460,9 +460,6 @@ func (x *UpdateInstanceSettingRequest) GetUpdateMask() *fieldmaskpb.FieldMask {
|
|||
// General instance settings configuration.
|
||||
type InstanceSetting_GeneralSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// theme is the name of the selected theme.
|
||||
// This references a CSS file in the web/public/themes/ directory.
|
||||
Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||
// disallow_user_registration disallows user registration.
|
||||
DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"`
|
||||
// disallow_password_auth disallows password authentication.
|
||||
|
|
@ -515,13 +512,6 @@ func (*InstanceSetting_GeneralSetting) Descriptor() ([]byte, []int) {
|
|||
return file_api_v1_instance_service_proto_rawDescGZIP(), []int{2, 0}
|
||||
}
|
||||
|
||||
func (x *InstanceSetting_GeneralSetting) GetTheme() string {
|
||||
if x != nil {
|
||||
return x.Theme
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InstanceSetting_GeneralSetting) GetDisallowUserRegistration() bool {
|
||||
if x != nil {
|
||||
return x.DisallowUserRegistration
|
||||
|
|
@ -758,7 +748,6 @@ type InstanceSetting_GeneralSetting_CustomProfile struct {
|
|||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"`
|
||||
Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -814,13 +803,6 @@ func (x *InstanceSetting_GeneralSetting_CustomProfile) GetLogoUrl() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *InstanceSetting_GeneralSetting_CustomProfile) GetLocale() string {
|
||||
if x != nil {
|
||||
return x.Locale
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// S3 configuration for cloud storage backend.
|
||||
// Reference: https://developers.cloudflare.com/r2/examples/aws/aws-sdk-go/
|
||||
type InstanceSetting_StorageSetting_S3Config struct {
|
||||
|
|
@ -917,14 +899,13 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
|
|||
"\aversion\x18\x02 \x01(\tR\aversion\x12\x12\n" +
|
||||
"\x04mode\x18\x03 \x01(\tR\x04mode\x12!\n" +
|
||||
"\finstance_url\x18\x06 \x01(\tR\vinstanceUrl\"\x1b\n" +
|
||||
"\x19GetInstanceProfileRequest\"\x9d\x10\n" +
|
||||
"\x19GetInstanceProfileRequest\"\xef\x0f\n" +
|
||||
"\x0fInstanceSetting\x12\x17\n" +
|
||||
"\x04name\x18\x01 \x01(\tB\x03\xe0A\bR\x04name\x12W\n" +
|
||||
"\x0fgeneral_setting\x18\x02 \x01(\v2,.memos.api.v1.InstanceSetting.GeneralSettingH\x00R\x0egeneralSetting\x12W\n" +
|
||||
"\x0fstorage_setting\x18\x03 \x01(\v2,.memos.api.v1.InstanceSetting.StorageSettingH\x00R\x0estorageSetting\x12d\n" +
|
||||
"\x14memo_related_setting\x18\x04 \x01(\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\x00R\x12memoRelatedSetting\x1a\xf8\x04\n" +
|
||||
"\x0eGeneralSetting\x12\x14\n" +
|
||||
"\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" +
|
||||
"\x14memo_related_setting\x18\x04 \x01(\v20.memos.api.v1.InstanceSetting.MemoRelatedSettingH\x00R\x12memoRelatedSetting\x1a\xca\x04\n" +
|
||||
"\x0eGeneralSetting\x12<\n" +
|
||||
"\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" +
|
||||
"\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" +
|
||||
"\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" +
|
||||
|
|
@ -932,12 +913,11 @@ const file_api_v1_instance_service_proto_rawDesc = "" +
|
|||
"\x0ecustom_profile\x18\x06 \x01(\v2:.memos.api.v1.InstanceSetting.GeneralSetting.CustomProfileR\rcustomProfile\x121\n" +
|
||||
"\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" +
|
||||
"\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" +
|
||||
"\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\x1az\n" +
|
||||
"\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\x1ab\n" +
|
||||
"\rCustomProfile\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
|
||||
"\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" +
|
||||
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" +
|
||||
"\x06locale\x18\x04 \x01(\tR\x06locale\x1a\xbc\x04\n" +
|
||||
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x1a\xbc\x04\n" +
|
||||
"\x0eStorageSetting\x12[\n" +
|
||||
"\fstorage_type\x18\x01 \x01(\x0e28.memos.api.v1.InstanceSetting.StorageSetting.StorageTypeR\vstorageType\x12+\n" +
|
||||
"\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" +
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/instance_service.proto
|
||||
|
||||
|
|
@ -95,13 +95,13 @@ type InstanceServiceServer interface {
|
|||
type UnimplementedInstanceServiceServer struct{}
|
||||
|
||||
func (UnimplementedInstanceServiceServer) GetInstanceProfile(context.Context, *GetInstanceProfileRequest) (*InstanceProfile, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInstanceProfile not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetInstanceProfile not implemented")
|
||||
}
|
||||
func (UnimplementedInstanceServiceServer) GetInstanceSetting(context.Context, *GetInstanceSettingRequest) (*InstanceSetting, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetInstanceSetting not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetInstanceSetting not implemented")
|
||||
}
|
||||
func (UnimplementedInstanceServiceServer) UpdateInstanceSetting(context.Context, *UpdateInstanceSettingRequest) (*InstanceSetting, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateInstanceSetting not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateInstanceSetting not implemented")
|
||||
}
|
||||
func (UnimplementedInstanceServiceServer) mustEmbedUnimplementedInstanceServiceServer() {}
|
||||
func (UnimplementedInstanceServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -114,7 +114,7 @@ type UnsafeInstanceServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterInstanceServiceServer(s grpc.ServiceRegistrar, srv InstanceServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedInstanceServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedInstanceServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/memo_service.proto
|
||||
|
||||
|
|
@ -261,46 +261,46 @@ type MemoServiceServer interface {
|
|||
type UnimplementedMemoServiceServer struct{}
|
||||
|
||||
func (UnimplementedMemoServiceServer) CreateMemo(context.Context, *CreateMemoRequest) (*Memo, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateMemo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateMemo not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemos(context.Context, *ListMemosRequest) (*ListMemosResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMemos not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemos not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) GetMemo(context.Context, *GetMemoRequest) (*Memo, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetMemo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetMemo not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) UpdateMemo(context.Context, *UpdateMemoRequest) (*Memo, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateMemo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateMemo not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) DeleteMemo(context.Context, *DeleteMemoRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteMemo not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteMemo not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) SetMemoAttachments(context.Context, *SetMemoAttachmentsRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMemoAttachments not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMemoAttachments not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemoAttachments(context.Context, *ListMemoAttachmentsRequest) (*ListMemoAttachmentsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMemoAttachments not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemoAttachments not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) SetMemoRelations(context.Context, *SetMemoRelationsRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method SetMemoRelations not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method SetMemoRelations not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemoRelations(context.Context, *ListMemoRelationsRequest) (*ListMemoRelationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMemoRelations not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemoRelations not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) CreateMemoComment(context.Context, *CreateMemoCommentRequest) (*Memo, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateMemoComment not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateMemoComment not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemoComments(context.Context, *ListMemoCommentsRequest) (*ListMemoCommentsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMemoComments not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemoComments not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) ListMemoReactions(context.Context, *ListMemoReactionsRequest) (*ListMemoReactionsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListMemoReactions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListMemoReactions not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) UpsertMemoReaction(context.Context, *UpsertMemoReactionRequest) (*Reaction, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpsertMemoReaction not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpsertMemoReaction not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) DeleteMemoReaction(context.Context, *DeleteMemoReactionRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteMemoReaction not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteMemoReaction not implemented")
|
||||
}
|
||||
func (UnimplementedMemoServiceServer) mustEmbedUnimplementedMemoServiceServer() {}
|
||||
func (UnimplementedMemoServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -313,7 +313,7 @@ type UnsafeMemoServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterMemoServiceServer(s grpc.ServiceRegistrar, srv MemoServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedMemoServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedMemoServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/shortcut_service.proto
|
||||
|
||||
|
|
@ -126,19 +126,19 @@ type ShortcutServiceServer interface {
|
|||
type UnimplementedShortcutServiceServer struct{}
|
||||
|
||||
func (UnimplementedShortcutServiceServer) ListShortcuts(context.Context, *ListShortcutsRequest) (*ListShortcutsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListShortcuts not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListShortcuts not implemented")
|
||||
}
|
||||
func (UnimplementedShortcutServiceServer) GetShortcut(context.Context, *GetShortcutRequest) (*Shortcut, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetShortcut not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetShortcut not implemented")
|
||||
}
|
||||
func (UnimplementedShortcutServiceServer) CreateShortcut(context.Context, *CreateShortcutRequest) (*Shortcut, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateShortcut not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateShortcut not implemented")
|
||||
}
|
||||
func (UnimplementedShortcutServiceServer) UpdateShortcut(context.Context, *UpdateShortcutRequest) (*Shortcut, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateShortcut not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateShortcut not implemented")
|
||||
}
|
||||
func (UnimplementedShortcutServiceServer) DeleteShortcut(context.Context, *DeleteShortcutRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteShortcut not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteShortcut not implemented")
|
||||
}
|
||||
func (UnimplementedShortcutServiceServer) mustEmbedUnimplementedShortcutServiceServer() {}
|
||||
func (UnimplementedShortcutServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -151,7 +151,7 @@ type UnsafeShortcutServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterShortcutServiceServer(s grpc.ServiceRegistrar, srv ShortcutServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedShortcutServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedShortcutServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.5.1
|
||||
// - protoc-gen-go-grpc v1.6.0
|
||||
// - protoc (unknown)
|
||||
// source: api/v1/user_service.proto
|
||||
|
||||
|
|
@ -403,73 +403,73 @@ type UserServiceServer interface {
|
|||
type UnimplementedUserServiceServer struct{}
|
||||
|
||||
func (UnimplementedUserServiceServer) ListUsers(context.Context, *ListUsersRequest) (*ListUsersResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUsers not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUsers not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetUser(context.Context, *GetUserRequest) (*User, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) CreateUser(context.Context, *CreateUserRequest) (*User, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUser(context.Context, *UpdateUserRequest) (*User, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) DeleteUser(context.Context, *DeleteUserRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteUser not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteUser not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetUserAvatar(context.Context, *GetUserAvatarRequest) (*httpbody.HttpBody, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserAvatar not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserAvatar not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListAllUserStats(context.Context, *ListAllUserStatsRequest) (*ListAllUserStatsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListAllUserStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListAllUserStats not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetUserStats(context.Context, *GetUserStatsRequest) (*UserStats, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserStats not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserStats not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) GetUserSetting(context.Context, *GetUserSettingRequest) (*UserSetting, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetUserSetting not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method GetUserSetting not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserSetting(context.Context, *UpdateUserSettingRequest) (*UserSetting, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserSetting not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserSetting not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserSettings(context.Context, *ListUserSettingsRequest) (*ListUserSettingsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserSettings not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserSettings not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserAccessTokens(context.Context, *ListUserAccessTokensRequest) (*ListUserAccessTokensResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserAccessTokens not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserAccessTokens not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) CreateUserAccessToken(context.Context, *CreateUserAccessTokenRequest) (*UserAccessToken, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateUserAccessToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateUserAccessToken not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) DeleteUserAccessToken(context.Context, *DeleteUserAccessTokenRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserAccessToken not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteUserAccessToken not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserSessions(context.Context, *ListUserSessionsRequest) (*ListUserSessionsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserSessions not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserSessions not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) RevokeUserSession(context.Context, *RevokeUserSessionRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method RevokeUserSession not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method RevokeUserSession not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserWebhooks(context.Context, *ListUserWebhooksRequest) (*ListUserWebhooksResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserWebhooks not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserWebhooks not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) CreateUserWebhook(context.Context, *CreateUserWebhookRequest) (*UserWebhook, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method CreateUserWebhook not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method CreateUserWebhook not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserWebhook(context.Context, *UpdateUserWebhookRequest) (*UserWebhook, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserWebhook not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserWebhook not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) DeleteUserWebhook(context.Context, *DeleteUserWebhookRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserWebhook not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteUserWebhook not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) ListUserNotifications(context.Context, *ListUserNotificationsRequest) (*ListUserNotificationsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method ListUserNotifications not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method ListUserNotifications not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) UpdateUserNotification(context.Context, *UpdateUserNotificationRequest) (*UserNotification, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method UpdateUserNotification not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method UpdateUserNotification not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) DeleteUserNotification(context.Context, *DeleteUserNotificationRequest) (*emptypb.Empty, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method DeleteUserNotification not implemented")
|
||||
return nil, status.Error(codes.Unimplemented, "method DeleteUserNotification not implemented")
|
||||
}
|
||||
func (UnimplementedUserServiceServer) mustEmbedUnimplementedUserServiceServer() {}
|
||||
func (UnimplementedUserServiceServer) testEmbeddedByValue() {}
|
||||
|
|
@ -482,7 +482,7 @@ type UnsafeUserServiceServer interface {
|
|||
}
|
||||
|
||||
func RegisterUserServiceServer(s grpc.ServiceRegistrar, srv UserServiceServer) {
|
||||
// If the following call pancis, it indicates UnimplementedUserServiceServer was
|
||||
// If the following call panics, it indicates UnimplementedUserServiceServer was
|
||||
// embedded by pointer and is nil. This will cause panics if an
|
||||
// unimplemented method is ever invoked, so we test this at initialization
|
||||
// time to prevent it from happening at runtime later due to I/O.
|
||||
|
|
|
|||
|
|
@ -2198,8 +2198,6 @@ components:
|
|||
type: string
|
||||
logoUrl:
|
||||
type: string
|
||||
locale:
|
||||
type: string
|
||||
description: Custom profile configuration for instance branding.
|
||||
GetCurrentSessionResponse:
|
||||
type: object
|
||||
|
|
@ -2290,11 +2288,6 @@ components:
|
|||
InstanceSetting_GeneralSetting:
|
||||
type: object
|
||||
properties:
|
||||
theme:
|
||||
type: string
|
||||
description: |-
|
||||
theme is the name of the selected theme.
|
||||
This references a CSS file in the web/public/themes/ directory.
|
||||
disallowUserRegistration:
|
||||
type: boolean
|
||||
description: disallow_user_registration disallows user registration.
|
||||
|
|
|
|||
|
|
@ -313,9 +313,6 @@ func (x *InstanceBasicSetting) GetSchemaVersion() string {
|
|||
|
||||
type InstanceGeneralSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// theme is the name of the selected theme.
|
||||
// This references a CSS file in the web/public/themes/ directory.
|
||||
Theme string `protobuf:"bytes,1,opt,name=theme,proto3" json:"theme,omitempty"`
|
||||
// disallow_user_registration disallows user registration.
|
||||
DisallowUserRegistration bool `protobuf:"varint,2,opt,name=disallow_user_registration,json=disallowUserRegistration,proto3" json:"disallow_user_registration,omitempty"`
|
||||
// disallow_password_auth disallows password authentication.
|
||||
|
|
@ -368,13 +365,6 @@ func (*InstanceGeneralSetting) Descriptor() ([]byte, []int) {
|
|||
return file_store_instance_setting_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *InstanceGeneralSetting) GetTheme() string {
|
||||
if x != nil {
|
||||
return x.Theme
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *InstanceGeneralSetting) GetDisallowUserRegistration() bool {
|
||||
if x != nil {
|
||||
return x.DisallowUserRegistration
|
||||
|
|
@ -436,7 +426,6 @@ type InstanceCustomProfile struct {
|
|||
Title string `protobuf:"bytes,1,opt,name=title,proto3" json:"title,omitempty"`
|
||||
Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"`
|
||||
LogoUrl string `protobuf:"bytes,3,opt,name=logo_url,json=logoUrl,proto3" json:"logo_url,omitempty"`
|
||||
Locale string `protobuf:"bytes,4,opt,name=locale,proto3" json:"locale,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
|
@ -492,13 +481,6 @@ func (x *InstanceCustomProfile) GetLogoUrl() string {
|
|||
return ""
|
||||
}
|
||||
|
||||
func (x *InstanceCustomProfile) GetLocale() string {
|
||||
if x != nil {
|
||||
return x.Locale
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type InstanceStorageSetting struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// storage_type is the storage type.
|
||||
|
|
@ -771,9 +753,8 @@ const file_store_instance_setting_proto_rawDesc = "" +
|
|||
"\x14InstanceBasicSetting\x12\x1d\n" +
|
||||
"\n" +
|
||||
"secret_key\x18\x01 \x01(\tR\tsecretKey\x12%\n" +
|
||||
"\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xec\x03\n" +
|
||||
"\x16InstanceGeneralSetting\x12\x14\n" +
|
||||
"\x05theme\x18\x01 \x01(\tR\x05theme\x12<\n" +
|
||||
"\x0eschema_version\x18\x02 \x01(\tR\rschemaVersion\"\xd6\x03\n" +
|
||||
"\x16InstanceGeneralSetting\x12<\n" +
|
||||
"\x1adisallow_user_registration\x18\x02 \x01(\bR\x18disallowUserRegistration\x124\n" +
|
||||
"\x16disallow_password_auth\x18\x03 \x01(\bR\x14disallowPasswordAuth\x12+\n" +
|
||||
"\x11additional_script\x18\x04 \x01(\tR\x10additionalScript\x12)\n" +
|
||||
|
|
@ -781,12 +762,11 @@ const file_store_instance_setting_proto_rawDesc = "" +
|
|||
"\x0ecustom_profile\x18\x06 \x01(\v2\".memos.store.InstanceCustomProfileR\rcustomProfile\x121\n" +
|
||||
"\x15week_start_day_offset\x18\a \x01(\x05R\x12weekStartDayOffset\x128\n" +
|
||||
"\x18disallow_change_username\x18\b \x01(\bR\x16disallowChangeUsername\x128\n" +
|
||||
"\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"\x82\x01\n" +
|
||||
"\x18disallow_change_nickname\x18\t \x01(\bR\x16disallowChangeNickname\"j\n" +
|
||||
"\x15InstanceCustomProfile\x12\x14\n" +
|
||||
"\x05title\x18\x01 \x01(\tR\x05title\x12 \n" +
|
||||
"\vdescription\x18\x02 \x01(\tR\vdescription\x12\x19\n" +
|
||||
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\x12\x16\n" +
|
||||
"\x06locale\x18\x04 \x01(\tR\x06locale\"\xd3\x02\n" +
|
||||
"\blogo_url\x18\x03 \x01(\tR\alogoUrl\"\xd3\x02\n" +
|
||||
"\x16InstanceStorageSetting\x12R\n" +
|
||||
"\fstorage_type\x18\x01 \x01(\x0e2/.memos.store.InstanceStorageSetting.StorageTypeR\vstorageType\x12+\n" +
|
||||
"\x11filepath_template\x18\x02 \x01(\tR\x10filepathTemplate\x12/\n" +
|
||||
|
|
|
|||
|
|
@ -34,9 +34,6 @@ message InstanceBasicSetting {
|
|||
}
|
||||
|
||||
message InstanceGeneralSetting {
|
||||
// theme is the name of the selected theme.
|
||||
// This references a CSS file in the web/public/themes/ directory.
|
||||
string theme = 1;
|
||||
// disallow_user_registration disallows user registration.
|
||||
bool disallow_user_registration = 2;
|
||||
// disallow_password_auth disallows password authentication.
|
||||
|
|
@ -61,7 +58,6 @@ message InstanceCustomProfile {
|
|||
string title = 1;
|
||||
string description = 2;
|
||||
string logo_url = 3;
|
||||
string locale = 4;
|
||||
}
|
||||
|
||||
message InstanceStorageSetting {
|
||||
|
|
|
|||
|
|
@ -154,14 +154,8 @@ func convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSett
|
|||
if setting == nil {
|
||||
return nil
|
||||
}
|
||||
// Backfill theme if empty
|
||||
theme := setting.Theme
|
||||
if theme == "" {
|
||||
theme = "default"
|
||||
}
|
||||
|
||||
generalSetting := &v1pb.InstanceSetting_GeneralSetting{
|
||||
Theme: theme,
|
||||
DisallowUserRegistration: setting.DisallowUserRegistration,
|
||||
DisallowPasswordAuth: setting.DisallowPasswordAuth,
|
||||
AdditionalScript: setting.AdditionalScript,
|
||||
|
|
@ -175,7 +169,6 @@ func convertInstanceGeneralSettingFromStore(setting *storepb.InstanceGeneralSett
|
|||
Title: setting.CustomProfile.Title,
|
||||
Description: setting.CustomProfile.Description,
|
||||
LogoUrl: setting.CustomProfile.LogoUrl,
|
||||
Locale: setting.CustomProfile.Locale,
|
||||
}
|
||||
}
|
||||
return generalSetting
|
||||
|
|
@ -186,7 +179,6 @@ func convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralS
|
|||
return nil
|
||||
}
|
||||
generalSetting := &storepb.InstanceGeneralSetting{
|
||||
Theme: setting.Theme,
|
||||
DisallowUserRegistration: setting.DisallowUserRegistration,
|
||||
DisallowPasswordAuth: setting.DisallowPasswordAuth,
|
||||
AdditionalScript: setting.AdditionalScript,
|
||||
|
|
@ -200,7 +192,6 @@ func convertInstanceGeneralSettingToStore(setting *v1pb.InstanceSetting_GeneralS
|
|||
Title: setting.CustomProfile.Title,
|
||||
Description: setting.CustomProfile.Description,
|
||||
LogoUrl: setting.CustomProfile.LogoUrl,
|
||||
Locale: setting.CustomProfile.Locale,
|
||||
}
|
||||
}
|
||||
return generalSetting
|
||||
|
|
|
|||
|
|
@ -415,15 +415,9 @@ func getRSSHeading(ctx context.Context, stores *store.Store) (RSSHeading, error)
|
|||
}
|
||||
customProfile := settings.CustomProfile
|
||||
|
||||
// Use locale as language if available, default to en-us
|
||||
language := "en-us"
|
||||
if customProfile.Locale != "" {
|
||||
language = customProfile.Locale
|
||||
}
|
||||
|
||||
return RSSHeading{
|
||||
Title: customProfile.Title,
|
||||
Description: customProfile.Description,
|
||||
Language: language,
|
||||
Language: "en-us",
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { Outlet } from "react-router-dom";
|
|||
import useNavigateTo from "./hooks/useNavigateTo";
|
||||
import { instanceStore, userStore } from "./store";
|
||||
import { cleanupExpiredOAuthState } from "./utils/oauth";
|
||||
import { loadTheme, setupSystemThemeListener } from "./utils/theme";
|
||||
import { getThemeWithFallback, loadTheme, setupSystemThemeListener } from "./utils/theme";
|
||||
|
||||
const App = observer(() => {
|
||||
const { i18n } = useTranslation();
|
||||
|
|
@ -54,55 +54,44 @@ const App = observer(() => {
|
|||
link.href = instanceGeneralSetting.customProfile.logoUrl || "/logo.webp";
|
||||
}, [instanceGeneralSetting.customProfile]);
|
||||
|
||||
// Update HTML lang and dir attributes based on current locale
|
||||
useEffect(() => {
|
||||
const currentLocale = instanceStore.state.locale;
|
||||
// This will trigger re-rendering of the whole app.
|
||||
i18n.changeLanguage(currentLocale);
|
||||
const currentLocale = i18n.language;
|
||||
document.documentElement.setAttribute("lang", currentLocale);
|
||||
if (["ar", "fa"].includes(currentLocale)) {
|
||||
document.documentElement.setAttribute("dir", "rtl");
|
||||
} else {
|
||||
document.documentElement.setAttribute("dir", "ltr");
|
||||
}
|
||||
}, [instanceStore.state.locale]);
|
||||
}, [i18n.language]);
|
||||
|
||||
// Apply theme when user setting changes
|
||||
useEffect(() => {
|
||||
if (!userGeneralSetting) {
|
||||
return;
|
||||
}
|
||||
|
||||
instanceStore.state.setPartial({
|
||||
locale: userGeneralSetting.locale || instanceStore.state.locale,
|
||||
theme: userGeneralSetting.theme || instanceStore.state.theme,
|
||||
});
|
||||
}, [userGeneralSetting?.locale, userGeneralSetting?.theme]);
|
||||
|
||||
// Load theme when instance theme changes or user setting changes
|
||||
useEffect(() => {
|
||||
const currentTheme = userGeneralSetting?.theme || instanceStore.state.theme;
|
||||
if (currentTheme) {
|
||||
loadTheme(currentTheme);
|
||||
}
|
||||
}, [userGeneralSetting?.theme, instanceStore.state.theme]);
|
||||
const theme = getThemeWithFallback(userGeneralSetting.theme);
|
||||
loadTheme(theme);
|
||||
}, [userGeneralSetting?.theme]);
|
||||
|
||||
// Listen for system theme changes when using "system" theme
|
||||
useEffect(() => {
|
||||
const currentTheme = userGeneralSetting?.theme || instanceStore.state.theme;
|
||||
const theme = getThemeWithFallback(userGeneralSetting?.theme);
|
||||
|
||||
// Only set up listener if theme is "system"
|
||||
if (currentTheme !== "system") {
|
||||
if (theme !== "system") {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set up listener for OS theme preference changes
|
||||
const cleanup = setupSystemThemeListener(() => {
|
||||
// Reload theme when system preference changes
|
||||
loadTheme(currentTheme);
|
||||
loadTheme(theme);
|
||||
});
|
||||
|
||||
// Cleanup listener on unmount or when theme changes
|
||||
return cleanup;
|
||||
}, [userGeneralSetting?.theme, instanceStore.state.theme]);
|
||||
}, [userGeneralSetting?.theme]);
|
||||
|
||||
return <Outlet />;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import i18n from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { instanceStore } from "@/store";
|
||||
import { getInitialTheme, loadTheme } from "@/utils/theme";
|
||||
import LocaleSelect from "./LocaleSelect";
|
||||
import ThemeSelect from "./ThemeSelect";
|
||||
|
||||
|
|
@ -9,10 +11,22 @@ interface Props {
|
|||
}
|
||||
|
||||
const AuthFooter = observer(({ className }: Props) => {
|
||||
const { i18n: i18nInstance } = useTranslation();
|
||||
const currentLocale = i18nInstance.language as Locale;
|
||||
const currentTheme = getInitialTheme();
|
||||
|
||||
const handleLocaleChange = (locale: Locale) => {
|
||||
i18n.changeLanguage(locale);
|
||||
};
|
||||
|
||||
const handleThemeChange = (theme: string) => {
|
||||
loadTheme(theme);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("mt-4 flex flex-row items-center justify-center w-full gap-2", className)}>
|
||||
<LocaleSelect value={instanceStore.state.locale} onChange={(locale) => instanceStore.state.setPartial({ locale })} />
|
||||
<ThemeSelect value={instanceStore.state.theme} onValueChange={(theme) => instanceStore.state.setPartial({ theme })} />
|
||||
<LocaleSelect value={currentLocale} onChange={handleLocaleChange} />
|
||||
<ThemeSelect value={currentTheme} onValueChange={handleThemeChange} />
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -4,7 +4,8 @@ import { CheckIcon, CopyIcon } from "lucide-react";
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { instanceStore } from "@/store";
|
||||
import { userStore } from "@/store";
|
||||
import { getThemeWithFallback, resolveTheme } from "@/utils/theme";
|
||||
import { MermaidBlock } from "./MermaidBlock";
|
||||
|
||||
interface PreProps {
|
||||
|
|
@ -93,8 +94,9 @@ export const CodeBlock = observer(({ children, className, ...props }: PreProps)
|
|||
);
|
||||
}
|
||||
|
||||
const appTheme = instanceStore.state.theme;
|
||||
const isDarkTheme = appTheme.includes("dark");
|
||||
const theme = getThemeWithFallback(userStore.state.userGeneralSetting?.theme);
|
||||
const resolvedTheme = resolveTheme(theme);
|
||||
const isDarkTheme = resolvedTheme.includes("dark");
|
||||
|
||||
// Dynamically load highlight.js theme based on app theme
|
||||
useEffect(() => {
|
||||
|
|
@ -121,7 +123,7 @@ export const CodeBlock = observer(({ children, className, ...props }: PreProps)
|
|||
};
|
||||
|
||||
dynamicImportStyle();
|
||||
}, [appTheme, isDarkTheme]);
|
||||
}, [resolvedTheme, isDarkTheme]);
|
||||
|
||||
// Highlight code using highlight.js
|
||||
const highlightedCode = useMemo(() => {
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ import mermaid from "mermaid";
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { instanceStore, userStore } from "@/store";
|
||||
import { resolveTheme, setupSystemThemeListener } from "@/utils/theme";
|
||||
import { userStore } from "@/store";
|
||||
import { getThemeWithFallback, resolveTheme, setupSystemThemeListener } from "@/utils/theme";
|
||||
|
||||
interface MermaidBlockProps {
|
||||
children?: React.ReactNode;
|
||||
|
|
@ -25,7 +25,8 @@ export const MermaidBlock = observer(({ children, className }: MermaidBlockProps
|
|||
const codeContent = String(codeElement?.props?.children || "").replace(/\n$/, "");
|
||||
|
||||
// Get theme preference (reactive via MobX observer)
|
||||
const themePreference = userStore.state.userGeneralSetting?.theme || instanceStore.state.theme;
|
||||
// Falls back to localStorage or system preference if no user setting
|
||||
const themePreference = getThemeWithFallback(userStore.state.userGeneralSetting?.theme);
|
||||
|
||||
// Resolve theme to actual value (handles "system" theme + system theme changes)
|
||||
const currentTheme = useMemo(() => resolveTheme(themePreference), [themePreference, systemThemeChange]);
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import { instanceSettingNamePrefix } from "@/store/common";
|
|||
import { IdentityProvider } from "@/types/proto/api/v1/idp_service";
|
||||
import { InstanceSetting_GeneralSetting, InstanceSetting_Key } from "@/types/proto/api/v1/instance_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import ThemeSelect from "../ThemeSelect";
|
||||
import UpdateCustomizedProfileDialog from "../UpdateCustomizedProfileDialog";
|
||||
import SettingGroup from "./SettingGroup";
|
||||
import SettingRow from "./SettingRow";
|
||||
|
|
@ -79,14 +78,6 @@ const InstanceSection = observer(() => {
|
|||
</SettingGroup>
|
||||
|
||||
<SettingGroup title={t("setting.system-section.title")} showSeparator>
|
||||
<SettingRow label="Theme">
|
||||
<ThemeSelect
|
||||
value={instanceGeneralSetting.theme || "default"}
|
||||
onValueChange={(value: string) => updatePartialSetting({ theme: value })}
|
||||
className="min-w-fit"
|
||||
/>
|
||||
</SettingRow>
|
||||
|
||||
<SettingRow label={t("setting.system-section.additional-style")} vertical>
|
||||
<Textarea
|
||||
className="font-mono w-full"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { observer } from "mobx-react-lite";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { instanceStore, userStore } from "@/store";
|
||||
import i18n from "@/i18n";
|
||||
import { userStore } from "@/store";
|
||||
import { Visibility } from "@/types/proto/api/v1/memo_service";
|
||||
import { UserSetting_GeneralSetting } from "@/types/proto/api/v1/user_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import { convertVisibilityFromString, convertVisibilityToString } from "@/utils/memo";
|
||||
import { loadTheme } from "@/utils/theme";
|
||||
import LocaleSelect from "../LocaleSelect";
|
||||
import ThemeSelect from "../ThemeSelect";
|
||||
import VisibilityIcon from "../VisibilityIcon";
|
||||
|
|
@ -18,8 +20,8 @@ const PreferencesSection = observer(() => {
|
|||
const generalSetting = userStore.state.userGeneralSetting;
|
||||
|
||||
const handleLocaleSelectChange = async (locale: Locale) => {
|
||||
// Update instance store immediately for instant UI feedback
|
||||
instanceStore.state.setPartial({ locale });
|
||||
// Apply locale immediately for instant UI feedback
|
||||
i18n.changeLanguage(locale);
|
||||
// Persist to user settings
|
||||
await userStore.updateUserGeneralSetting({ locale }, ["locale"]);
|
||||
};
|
||||
|
|
@ -29,8 +31,8 @@ const PreferencesSection = observer(() => {
|
|||
};
|
||||
|
||||
const handleThemeChange = async (theme: string) => {
|
||||
// Update instance store immediately for instant UI feedback
|
||||
instanceStore.state.setPartial({ theme });
|
||||
// Apply theme immediately for instant UI feedback
|
||||
loadTheme(theme);
|
||||
// Persist to user settings
|
||||
await userStore.updateUserGeneralSetting({ theme }, ["theme"]);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { Monitor, Moon, MoonStar, Palette, Sun, Wallpaper } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { instanceStore } from "@/store";
|
||||
import { THEME_OPTIONS } from "@/utils/theme";
|
||||
|
||||
interface ThemeSelectProps {
|
||||
|
|
@ -19,13 +18,11 @@ const THEME_ICONS: Record<string, JSX.Element> = {
|
|||
};
|
||||
|
||||
const ThemeSelect = ({ value, onValueChange, className }: ThemeSelectProps = {}) => {
|
||||
const currentTheme = value || instanceStore.state.theme || "system";
|
||||
const currentTheme = value || "system";
|
||||
|
||||
const handleThemeChange = (newTheme: Theme) => {
|
||||
const handleThemeChange = (newTheme: string) => {
|
||||
if (onValueChange) {
|
||||
onValueChange(newTheme);
|
||||
} else {
|
||||
instanceStore.setTheme(newTheme);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,8 +9,6 @@ import { instanceStore } from "@/store";
|
|||
import { instanceSettingNamePrefix } from "@/store/common";
|
||||
import { InstanceSetting_GeneralSetting_CustomProfile, InstanceSetting_Key } from "@/types/proto/api/v1/instance_service";
|
||||
import { useTranslate } from "@/utils/i18n";
|
||||
import LocaleSelect from "./LocaleSelect";
|
||||
import ThemeSelect from "./ThemeSelect";
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -52,18 +50,11 @@ function UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }: Props)
|
|||
});
|
||||
};
|
||||
|
||||
const handleLocaleSelectChange = (locale: Locale) => {
|
||||
setPartialState({
|
||||
locale: locale,
|
||||
});
|
||||
};
|
||||
|
||||
const handleRestoreButtonClick = () => {
|
||||
setPartialState({
|
||||
title: "Memos",
|
||||
logoUrl: "/logo.webp",
|
||||
description: "",
|
||||
locale: "en",
|
||||
});
|
||||
};
|
||||
|
||||
|
|
@ -126,16 +117,6 @@ function UpdateCustomizedProfileDialog({ open, onOpenChange, onSuccess }: Props)
|
|||
placeholder="Enter description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>{t("setting.system-section.customize-server.locale")}</Label>
|
||||
<LocaleSelect value={customProfile.locale} onChange={handleLocaleSelectChange} />
|
||||
</div>
|
||||
|
||||
<div className="grid gap-2">
|
||||
<Label>Theme</Label>
|
||||
<ThemeSelect />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-col sm:flex-row sm:justify-between gap-2">
|
||||
|
|
|
|||
|
|
@ -3,12 +3,12 @@ import { observer } from "mobx-react-lite";
|
|||
import { authServiceClient } from "@/grpcweb";
|
||||
import useCurrentUser from "@/hooks/useCurrentUser";
|
||||
import useNavigateTo from "@/hooks/useNavigateTo";
|
||||
import { locales } from "@/i18n";
|
||||
import i18n, { locales } from "@/i18n";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Routes } from "@/router";
|
||||
import { instanceStore, userStore } from "@/store";
|
||||
import { userStore } from "@/store";
|
||||
import { getLocaleDisplayName, useTranslate } from "@/utils/i18n";
|
||||
import { THEME_OPTIONS } from "@/utils/theme";
|
||||
import { loadTheme, THEME_OPTIONS } from "@/utils/theme";
|
||||
import UserAvatar from "./UserAvatar";
|
||||
import {
|
||||
DropdownMenu,
|
||||
|
|
@ -34,13 +34,16 @@ const UserMenu = observer((props: Props) => {
|
|||
const currentTheme = generalSetting?.theme || "default";
|
||||
|
||||
const handleLocaleChange = async (locale: Locale) => {
|
||||
// Update instance store immediately for instant UI feedback
|
||||
instanceStore.state.setPartial({ locale });
|
||||
// Apply locale immediately for instant UI feedback
|
||||
i18n.changeLanguage(locale);
|
||||
// Persist to user settings
|
||||
await userStore.updateUserGeneralSetting({ locale }, ["locale"]);
|
||||
};
|
||||
|
||||
const handleThemeChange = async (theme: string) => {
|
||||
// Apply theme immediately for instant UI feedback
|
||||
loadTheme(theme);
|
||||
// Persist to user settings
|
||||
await userStore.updateUserGeneralSetting({ theme }, ["theme"]);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -9,11 +9,12 @@ import router from "./router";
|
|||
// Configure MobX before importing any stores
|
||||
import "./store/config";
|
||||
import { initialInstanceStore } from "./store/instance";
|
||||
import { initialUserStore } from "./store/user";
|
||||
import userStore, { initialUserStore } from "./store/user";
|
||||
import { applyThemeEarly } from "./utils/theme";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
|
||||
// Apply theme early to prevent flash of wrong theme
|
||||
// This uses localStorage as the source before user settings are loaded
|
||||
applyThemeEarly();
|
||||
|
||||
const Main = observer(() => (
|
||||
|
|
@ -24,9 +25,14 @@ const Main = observer(() => (
|
|||
));
|
||||
|
||||
(async () => {
|
||||
// Initialize stores
|
||||
await initialInstanceStore();
|
||||
await initialUserStore();
|
||||
|
||||
// Apply user preferences (theme & locale) after user settings are loaded
|
||||
// This will override the early theme with user's actual preference
|
||||
userStore.applyUserPreferences();
|
||||
|
||||
const container = document.getElementById("root");
|
||||
const root = createRoot(container as HTMLElement);
|
||||
root.render(<Main />);
|
||||
|
|
|
|||
|
|
@ -22,9 +22,6 @@ export {
|
|||
memoNamePrefix,
|
||||
userNamePrefix,
|
||||
} from "./common";
|
||||
// Re-export instance types
|
||||
export type { Theme } from "./instance";
|
||||
export { isValidTheme } from "./instance";
|
||||
// Re-export filter types
|
||||
export type { FilterFactor, MemoFilter } from "./memoFilter";
|
||||
export { getMemoFilterKey, parseFilterQuery, stringifyFilters } from "./memoFilter";
|
||||
|
|
|
|||
|
|
@ -9,21 +9,11 @@ import {
|
|||
InstanceSetting_Key,
|
||||
InstanceSetting_MemoRelatedSetting,
|
||||
} from "@/types/proto/api/v1/instance_service";
|
||||
import { isValidateLocale } from "@/utils/i18n";
|
||||
import { createServerStore, StandardState } from "./base-store";
|
||||
import { instanceSettingNamePrefix } from "./common";
|
||||
import { createRequestKey } from "./store-utils";
|
||||
|
||||
const VALID_THEMES = ["system", "default", "default-dark", "midnight", "paper", "whitewall"] as const;
|
||||
export type Theme = (typeof VALID_THEMES)[number];
|
||||
|
||||
export function isValidTheme(theme: string): theme is Theme {
|
||||
return VALID_THEMES.includes(theme as Theme);
|
||||
}
|
||||
|
||||
class InstanceState extends StandardState {
|
||||
locale: string = "en";
|
||||
theme: Theme | string = "system";
|
||||
profile: InstanceProfile = InstanceProfile.fromPartial({});
|
||||
settings: InstanceSetting[] = [];
|
||||
|
||||
|
|
@ -42,29 +32,6 @@ class InstanceState extends StandardState {
|
|||
return setting?.memoRelatedSetting || InstanceSetting_MemoRelatedSetting.fromPartial({});
|
||||
}).get();
|
||||
}
|
||||
|
||||
setPartial(partial: Partial<InstanceState>): void {
|
||||
const finalState = { ...this, ...partial };
|
||||
|
||||
// Validate locale
|
||||
if (partial.locale !== undefined && !isValidateLocale(finalState.locale)) {
|
||||
console.warn(`Invalid locale "${finalState.locale}", falling back to "en"`);
|
||||
finalState.locale = "en";
|
||||
}
|
||||
|
||||
// Validate theme - accept string and validate
|
||||
if (partial.theme !== undefined) {
|
||||
const themeStr = String(finalState.theme);
|
||||
if (!isValidTheme(themeStr)) {
|
||||
console.warn(`Invalid theme "${themeStr}", falling back to "default"`);
|
||||
finalState.theme = "default";
|
||||
} else {
|
||||
finalState.theme = themeStr;
|
||||
}
|
||||
}
|
||||
|
||||
Object.assign(this, finalState);
|
||||
}
|
||||
}
|
||||
|
||||
const instanceStore = (() => {
|
||||
|
|
@ -114,33 +81,6 @@ const instanceStore = (() => {
|
|||
return setting || InstanceSetting.fromPartial({});
|
||||
};
|
||||
|
||||
const setTheme = async (theme: string): Promise<void> => {
|
||||
// Validate theme
|
||||
if (!isValidTheme(theme)) {
|
||||
console.warn(`Invalid theme "${theme}", ignoring`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Update local state immediately
|
||||
state.setPartial({ theme });
|
||||
|
||||
// Persist to server
|
||||
const generalSetting = state.generalSetting;
|
||||
const updatedGeneralSetting = InstanceSetting_GeneralSetting.fromPartial({
|
||||
...generalSetting,
|
||||
customProfile: {
|
||||
...generalSetting.customProfile,
|
||||
},
|
||||
});
|
||||
|
||||
await upsertInstanceSetting(
|
||||
InstanceSetting.fromPartial({
|
||||
name: `${instanceSettingNamePrefix}${InstanceSetting_Key.GENERAL}`,
|
||||
generalSetting: updatedGeneralSetting,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const fetchInstanceProfile = async (): Promise<InstanceProfile> => {
|
||||
const requestKey = createRequestKey("fetchInstanceProfile");
|
||||
|
||||
|
|
@ -161,7 +101,6 @@ const instanceStore = (() => {
|
|||
fetchInstanceProfile,
|
||||
upsertInstanceSetting,
|
||||
getInstanceSettingByKey,
|
||||
setTheme,
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
@ -178,19 +117,9 @@ export const initialInstanceStore = async (): Promise<void> => {
|
|||
]);
|
||||
|
||||
// Apply settings to state
|
||||
const instanceGeneralSetting = instanceStore.state.generalSetting;
|
||||
instanceStore.state.setPartial({
|
||||
locale: instanceGeneralSetting.customProfile?.locale || "en",
|
||||
theme: instanceGeneralSetting.theme || "system",
|
||||
profile: instanceProfile,
|
||||
});
|
||||
Object.assign(instanceStore.state, { profile: instanceProfile });
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize instance store:", error);
|
||||
// Set default fallback values
|
||||
instanceStore.state.setPartial({
|
||||
locale: "en",
|
||||
theme: "system",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import { uniqueId } from "lodash-es";
|
||||
import { computed, makeAutoObservable } from "mobx";
|
||||
import { authServiceClient, shortcutServiceClient, userServiceClient } from "@/grpcweb";
|
||||
import i18n from "@/i18n";
|
||||
import { Shortcut } from "@/types/proto/api/v1/shortcut_service";
|
||||
import {
|
||||
User,
|
||||
|
|
@ -13,8 +14,8 @@ import {
|
|||
UserSetting_WebhooksSetting,
|
||||
UserStats,
|
||||
} from "@/types/proto/api/v1/user_service";
|
||||
import { findNearestMatchedLanguage } from "@/utils/i18n";
|
||||
import instanceStore from "./instance";
|
||||
import { getLocaleWithFallback } from "@/utils/i18n";
|
||||
import { getThemeWithFallback, loadTheme } from "@/utils/theme";
|
||||
import { createRequestKey, RequestDeduplicator, StoreError } from "./store-utils";
|
||||
|
||||
class LocalState {
|
||||
|
|
@ -283,6 +284,20 @@ const userStore = (() => {
|
|||
state.statsStateId = id;
|
||||
};
|
||||
|
||||
// Applies user preferences (theme and locale) with proper fallbacks
|
||||
// This should be called after user settings are loaded
|
||||
const applyUserPreferences = () => {
|
||||
const generalSetting = state.userGeneralSetting;
|
||||
|
||||
// Apply theme with fallback: user setting -> localStorage -> system
|
||||
const theme = getThemeWithFallback(generalSetting?.theme);
|
||||
loadTheme(theme);
|
||||
|
||||
// Apply locale with fallback: user setting -> browser language
|
||||
const locale = getLocaleWithFallback(generalSetting?.locale);
|
||||
i18n.changeLanguage(locale);
|
||||
};
|
||||
|
||||
return {
|
||||
state,
|
||||
getOrFetchUserByName,
|
||||
|
|
@ -299,6 +314,7 @@ const userStore = (() => {
|
|||
deleteNotification,
|
||||
fetchUserStats,
|
||||
setStatsStateId,
|
||||
applyUserPreferences,
|
||||
};
|
||||
})();
|
||||
|
||||
|
|
@ -306,22 +322,18 @@ const userStore = (() => {
|
|||
// 1. Fetch current authenticated user session
|
||||
// 2. Set current user in store (required for subsequent calls)
|
||||
// 3. Fetch user settings (depends on currentUser being set)
|
||||
// 4. Apply user preferences to instance store
|
||||
export const initialUserStore = async () => {
|
||||
try {
|
||||
// Step 1: Authenticate and get current user
|
||||
const { user: currentUser } = await authServiceClient.getCurrentSession({});
|
||||
|
||||
if (!currentUser) {
|
||||
// No authenticated user - clear state and use default locale
|
||||
// No authenticated user - clear state
|
||||
userStore.state.setPartial({
|
||||
currentUser: undefined,
|
||||
userGeneralSetting: undefined,
|
||||
userMapByName: {},
|
||||
});
|
||||
|
||||
const locale = findNearestMatchedLanguage(navigator.language);
|
||||
instanceStore.state.setPartial({ locale });
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -339,27 +351,8 @@ export const initialUserStore = async () => {
|
|||
// CRITICAL: This must happen after currentUser is set in step 2
|
||||
// The fetchUserSettings() and fetchUserStats() methods check state.currentUser internally
|
||||
await Promise.all([userStore.fetchUserSettings(), userStore.fetchUserStats()]);
|
||||
|
||||
// Step 4: Apply user preferences to instance
|
||||
// CRITICAL: This must happen after fetchUserSettings() completes
|
||||
// We need userGeneralSetting to be populated before accessing it
|
||||
const generalSetting = userStore.state.userGeneralSetting;
|
||||
if (generalSetting) {
|
||||
// Note: setPartial will validate theme automatically
|
||||
instanceStore.state.setPartial({
|
||||
locale: generalSetting.locale,
|
||||
theme: generalSetting.theme || "default", // Validation handled by setPartial
|
||||
});
|
||||
} else {
|
||||
// Fallback if settings weren't loaded
|
||||
const locale = findNearestMatchedLanguage(navigator.language);
|
||||
instanceStore.state.setPartial({ locale });
|
||||
}
|
||||
} catch (error) {
|
||||
// On any error, fall back to browser language detection
|
||||
console.error("Failed to initialize user store:", error);
|
||||
const locale = findNearestMatchedLanguage(navigator.language);
|
||||
instanceStore.state.setPartial({ locale });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -92,11 +92,6 @@ export function instanceSetting_KeyToNumber(object: InstanceSetting_Key): number
|
|||
|
||||
/** General instance settings configuration. */
|
||||
export interface InstanceSetting_GeneralSetting {
|
||||
/**
|
||||
* theme is the name of the selected theme.
|
||||
* This references a CSS file in the web/public/themes/ directory.
|
||||
*/
|
||||
theme: string;
|
||||
/** disallow_user_registration disallows user registration. */
|
||||
disallowUserRegistration: boolean;
|
||||
/** disallow_password_auth disallows password authentication. */
|
||||
|
|
@ -126,7 +121,6 @@ export interface InstanceSetting_GeneralSetting_CustomProfile {
|
|||
title: string;
|
||||
description: string;
|
||||
logoUrl: string;
|
||||
locale: string;
|
||||
}
|
||||
|
||||
/** Storage configuration settings for instance attachments. */
|
||||
|
|
@ -453,7 +447,6 @@ export const InstanceSetting: MessageFns<InstanceSetting> = {
|
|||
|
||||
function createBaseInstanceSetting_GeneralSetting(): InstanceSetting_GeneralSetting {
|
||||
return {
|
||||
theme: "",
|
||||
disallowUserRegistration: false,
|
||||
disallowPasswordAuth: false,
|
||||
additionalScript: "",
|
||||
|
|
@ -467,9 +460,6 @@ function createBaseInstanceSetting_GeneralSetting(): InstanceSetting_GeneralSett
|
|||
|
||||
export const InstanceSetting_GeneralSetting: MessageFns<InstanceSetting_GeneralSetting> = {
|
||||
encode(message: InstanceSetting_GeneralSetting, writer: BinaryWriter = new BinaryWriter()): BinaryWriter {
|
||||
if (message.theme !== "") {
|
||||
writer.uint32(10).string(message.theme);
|
||||
}
|
||||
if (message.disallowUserRegistration !== false) {
|
||||
writer.uint32(16).bool(message.disallowUserRegistration);
|
||||
}
|
||||
|
|
@ -504,14 +494,6 @@ export const InstanceSetting_GeneralSetting: MessageFns<InstanceSetting_GeneralS
|
|||
while (reader.pos < end) {
|
||||
const tag = reader.uint32();
|
||||
switch (tag >>> 3) {
|
||||
case 1: {
|
||||
if (tag !== 10) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.theme = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 2: {
|
||||
if (tag !== 16) {
|
||||
break;
|
||||
|
|
@ -590,7 +572,6 @@ export const InstanceSetting_GeneralSetting: MessageFns<InstanceSetting_GeneralS
|
|||
},
|
||||
fromPartial(object: DeepPartial<InstanceSetting_GeneralSetting>): InstanceSetting_GeneralSetting {
|
||||
const message = createBaseInstanceSetting_GeneralSetting();
|
||||
message.theme = object.theme ?? "";
|
||||
message.disallowUserRegistration = object.disallowUserRegistration ?? false;
|
||||
message.disallowPasswordAuth = object.disallowPasswordAuth ?? false;
|
||||
message.additionalScript = object.additionalScript ?? "";
|
||||
|
|
@ -606,7 +587,7 @@ export const InstanceSetting_GeneralSetting: MessageFns<InstanceSetting_GeneralS
|
|||
};
|
||||
|
||||
function createBaseInstanceSetting_GeneralSetting_CustomProfile(): InstanceSetting_GeneralSetting_CustomProfile {
|
||||
return { title: "", description: "", logoUrl: "", locale: "" };
|
||||
return { title: "", description: "", logoUrl: "" };
|
||||
}
|
||||
|
||||
export const InstanceSetting_GeneralSetting_CustomProfile: MessageFns<InstanceSetting_GeneralSetting_CustomProfile> = {
|
||||
|
|
@ -623,9 +604,6 @@ export const InstanceSetting_GeneralSetting_CustomProfile: MessageFns<InstanceSe
|
|||
if (message.logoUrl !== "") {
|
||||
writer.uint32(26).string(message.logoUrl);
|
||||
}
|
||||
if (message.locale !== "") {
|
||||
writer.uint32(34).string(message.locale);
|
||||
}
|
||||
return writer;
|
||||
},
|
||||
|
||||
|
|
@ -660,14 +638,6 @@ export const InstanceSetting_GeneralSetting_CustomProfile: MessageFns<InstanceSe
|
|||
message.logoUrl = reader.string();
|
||||
continue;
|
||||
}
|
||||
case 4: {
|
||||
if (tag !== 34) {
|
||||
break;
|
||||
}
|
||||
|
||||
message.locale = reader.string();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ((tag & 7) === 4 || tag === 0) {
|
||||
break;
|
||||
|
|
@ -689,7 +659,6 @@ export const InstanceSetting_GeneralSetting_CustomProfile: MessageFns<InstanceSe
|
|||
message.title = object.title ?? "";
|
||||
message.description = object.description ?? "";
|
||||
message.logoUrl = object.logoUrl ?? "";
|
||||
message.locale = object.locale ?? "";
|
||||
return message;
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -52,6 +52,19 @@ export const isValidateLocale = (locale: string | undefined | null): boolean =>
|
|||
return locales.includes(locale);
|
||||
};
|
||||
|
||||
// Gets the locale to use with proper priority:
|
||||
// 1. User setting (if logged in and has preference)
|
||||
// 2. Browser language preference
|
||||
export const getLocaleWithFallback = (userLocale?: string): Locale => {
|
||||
// Priority 1: User setting (if logged in and valid)
|
||||
if (userLocale && isValidateLocale(userLocale)) {
|
||||
return userLocale as Locale;
|
||||
}
|
||||
|
||||
// Priority 2: Browser language
|
||||
return findNearestMatchedLanguage(navigator.language);
|
||||
};
|
||||
|
||||
// Get the display name for a locale in its native language
|
||||
export const getLocaleDisplayName = (locale: string): string => {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -3,11 +3,24 @@ import midnightThemeContent from "../themes/midnight.css?raw";
|
|||
import paperThemeContent from "../themes/paper.css?raw";
|
||||
import whitewallThemeContent from "../themes/whitewall.css?raw";
|
||||
|
||||
const VALID_THEMES = ["system", "default", "default-dark", "midnight", "paper", "whitewall"] as const;
|
||||
type ValidTheme = (typeof VALID_THEMES)[number];
|
||||
// ============================================================================
|
||||
// Types and Constants
|
||||
// ============================================================================
|
||||
|
||||
const THEME_CONTENT: Record<ValidTheme, string | null> = {
|
||||
system: null, // System theme dynamically chooses between default and default-dark
|
||||
const VALID_THEMES = ["system", "default", "default-dark", "midnight", "paper", "whitewall"] as const;
|
||||
|
||||
export type Theme = (typeof VALID_THEMES)[number];
|
||||
export type ResolvedTheme = Exclude<Theme, "system">;
|
||||
|
||||
export interface ThemeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = "memos-theme";
|
||||
const STYLE_ELEMENT_ID = "instance-theme";
|
||||
|
||||
const THEME_CONTENT: Record<ResolvedTheme, string | null> = {
|
||||
default: null,
|
||||
"default-dark": defaultDarkThemeContent,
|
||||
midnight: midnightThemeContent,
|
||||
|
|
@ -15,11 +28,6 @@ const THEME_CONTENT: Record<ValidTheme, string | null> = {
|
|||
whitewall: whitewallThemeContent,
|
||||
};
|
||||
|
||||
export interface ThemeOption {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export const THEME_OPTIONS: ThemeOption[] = [
|
||||
{ value: "system", label: "Sync with system" },
|
||||
{ value: "default", label: "Light" },
|
||||
|
|
@ -29,102 +37,201 @@ export const THEME_OPTIONS: ThemeOption[] = [
|
|||
{ value: "whitewall", label: "Whitewall" },
|
||||
];
|
||||
|
||||
const validateTheme = (theme: string): ValidTheme => {
|
||||
return VALID_THEMES.includes(theme as ValidTheme) ? (theme as ValidTheme) : "default";
|
||||
// ============================================================================
|
||||
// Theme Validation and Detection
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Validates and normalizes a theme string to a valid theme.
|
||||
* Falls back to "default" for invalid themes.
|
||||
*/
|
||||
const validateTheme = (theme: string): Theme => {
|
||||
return VALID_THEMES.includes(theme as Theme) ? (theme as Theme) : "default";
|
||||
};
|
||||
|
||||
export const getSystemTheme = (): "default" | "default-dark" => {
|
||||
if (typeof window !== "undefined" && window.matchMedia) {
|
||||
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "default-dark" : "default";
|
||||
/**
|
||||
* Detects the system's preferred color scheme.
|
||||
* @returns "default-dark" for dark mode, "default" for light mode
|
||||
*/
|
||||
export const getSystemTheme = (): ResolvedTheme => {
|
||||
if (typeof window !== "undefined" && window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
|
||||
return "default-dark";
|
||||
}
|
||||
return "default";
|
||||
};
|
||||
|
||||
// Resolves "system" to actual theme based on OS preference
|
||||
export const resolveTheme = (theme: string): "default" | "default-dark" | "midnight" | "paper" | "whitewall" => {
|
||||
if (theme === "system") {
|
||||
return getSystemTheme();
|
||||
}
|
||||
/**
|
||||
* Resolves "system" theme to the actual theme based on OS preference.
|
||||
* Other themes are returned as-is after validation.
|
||||
*/
|
||||
export const resolveTheme = (theme: string): ResolvedTheme => {
|
||||
const validTheme = validateTheme(theme);
|
||||
return validTheme === "system" ? getSystemTheme() : validTheme;
|
||||
};
|
||||
|
||||
// Gets the theme that should be applied on initial load
|
||||
export const getInitialTheme = (): ValidTheme => {
|
||||
// Try to get stored theme from localStorage
|
||||
// ============================================================================
|
||||
// LocalStorage Helpers
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Safely reads the theme from localStorage.
|
||||
* @returns The stored theme, or null if not found or unavailable
|
||||
*/
|
||||
const getStoredTheme = (): Theme | null => {
|
||||
try {
|
||||
const storedTheme = localStorage.getItem("memos-theme");
|
||||
if (storedTheme && VALID_THEMES.includes(storedTheme as ValidTheme)) {
|
||||
return storedTheme as ValidTheme;
|
||||
}
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
return stored && VALID_THEMES.includes(stored as Theme) ? (stored as Theme) : null;
|
||||
} catch {
|
||||
// localStorage might not be available
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Safely stores the theme to localStorage.
|
||||
*/
|
||||
const setStoredTheme = (theme: Theme): void => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, theme);
|
||||
} catch {
|
||||
// localStorage might not be available (SSR, private browsing, etc.)
|
||||
}
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Theme Selection with Fallbacks
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Gets the theme for initial page load (before user settings are available).
|
||||
* Priority: localStorage -> system preference
|
||||
*/
|
||||
export const getInitialTheme = (): Theme => {
|
||||
return getStoredTheme() ?? "system";
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the theme with full fallback chain.
|
||||
* Priority:
|
||||
* 1. User setting (if logged in and has preference)
|
||||
* 2. localStorage (from previous session)
|
||||
* 3. System preference
|
||||
*/
|
||||
export const getThemeWithFallback = (userTheme?: string): Theme => {
|
||||
// Priority 1: User setting
|
||||
if (userTheme && VALID_THEMES.includes(userTheme as Theme)) {
|
||||
return userTheme as Theme;
|
||||
}
|
||||
|
||||
// Priority 2: localStorage
|
||||
const stored = getStoredTheme();
|
||||
if (stored) {
|
||||
return stored;
|
||||
}
|
||||
|
||||
// Priority 3: System preference
|
||||
return "system";
|
||||
};
|
||||
|
||||
// Applies the theme early to prevent flash of wrong theme
|
||||
// ============================================================================
|
||||
// DOM Manipulation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Removes the existing theme style element from the DOM.
|
||||
*/
|
||||
const removeThemeStyle = (): void => {
|
||||
document.getElementById(STYLE_ELEMENT_ID)?.remove();
|
||||
};
|
||||
|
||||
/**
|
||||
* Injects theme CSS into the document head.
|
||||
* Skips injection for the default theme (uses base CSS).
|
||||
*/
|
||||
const injectThemeStyle = (theme: ResolvedTheme): void => {
|
||||
removeThemeStyle();
|
||||
|
||||
if (theme === "default") {
|
||||
return; // Use base CSS for default theme
|
||||
}
|
||||
|
||||
const css = THEME_CONTENT[theme];
|
||||
if (css) {
|
||||
const style = document.createElement("style");
|
||||
style.id = STYLE_ELEMENT_ID;
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets the data-theme attribute on the document element.
|
||||
* This allows CSS to react to the current theme.
|
||||
*/
|
||||
const setThemeAttribute = (theme: ResolvedTheme): void => {
|
||||
document.documentElement.setAttribute("data-theme", theme);
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Main Theme Loading
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Loads and applies a theme.
|
||||
* This function:
|
||||
* 1. Validates the theme
|
||||
* 2. Resolves "system" to actual theme
|
||||
* 3. Injects theme CSS
|
||||
* 4. Sets data-theme attribute
|
||||
* 5. Persists to localStorage
|
||||
*/
|
||||
export const loadTheme = (themeName: string): void => {
|
||||
const validTheme = validateTheme(themeName);
|
||||
const resolvedTheme = resolveTheme(validTheme);
|
||||
|
||||
injectThemeStyle(resolvedTheme);
|
||||
setThemeAttribute(resolvedTheme);
|
||||
setStoredTheme(validTheme); // Store original theme preference (not resolved)
|
||||
};
|
||||
|
||||
/**
|
||||
* Applies theme early during initial page load to prevent FOUC.
|
||||
* Uses only localStorage and system preference (no user settings yet).
|
||||
*/
|
||||
export const applyThemeEarly = (): void => {
|
||||
const theme = getInitialTheme();
|
||||
loadTheme(theme);
|
||||
};
|
||||
|
||||
export const loadTheme = (themeName: string): void => {
|
||||
const validTheme = validateTheme(themeName);
|
||||
// ============================================================================
|
||||
// System Theme Listener
|
||||
// ============================================================================
|
||||
|
||||
// Resolve "system" to actual theme based on OS preference
|
||||
const resolvedTheme = resolveTheme(validTheme);
|
||||
|
||||
// Remove existing theme
|
||||
document.getElementById("instance-theme")?.remove();
|
||||
|
||||
// Apply theme (skip for default)
|
||||
if (resolvedTheme !== "default") {
|
||||
const css = THEME_CONTENT[resolvedTheme];
|
||||
if (css) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "instance-theme";
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Set data attribute with resolved theme
|
||||
document.documentElement.setAttribute("data-theme", resolvedTheme);
|
||||
|
||||
// Store theme preference (original, not resolved) for future loads
|
||||
try {
|
||||
localStorage.setItem("memos-theme", validTheme);
|
||||
} catch {
|
||||
// localStorage might not be available
|
||||
}
|
||||
};
|
||||
|
||||
// Sets up a listener for system theme preference changes
|
||||
/**
|
||||
* Sets up a listener for OS-level theme preference changes.
|
||||
* Supports both modern (addEventListener) and legacy (addListener) APIs.
|
||||
*
|
||||
* @param onThemeChange - Callback invoked when system theme changes
|
||||
* @returns Cleanup function to remove the listener
|
||||
*/
|
||||
export const setupSystemThemeListener = (onThemeChange: () => void): (() => void) => {
|
||||
// Guard against SSR
|
||||
if (typeof window === "undefined" || !window.matchMedia) {
|
||||
return () => {}; // No-op cleanup
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
|
||||
// Handle theme change
|
||||
const handleChange = () => {
|
||||
onThemeChange();
|
||||
};
|
||||
|
||||
// Modern API (addEventListener)
|
||||
// Modern API (preferred)
|
||||
if (mediaQuery.addEventListener) {
|
||||
mediaQuery.addEventListener("change", handleChange);
|
||||
return () => mediaQuery.removeEventListener("change", handleChange);
|
||||
mediaQuery.addEventListener("change", onThemeChange);
|
||||
return () => mediaQuery.removeEventListener("change", onThemeChange);
|
||||
}
|
||||
|
||||
// Legacy API (addListener) - for older browsers
|
||||
// Legacy API (Safari < 14)
|
||||
if (mediaQuery.addListener) {
|
||||
mediaQuery.addListener(handleChange);
|
||||
return () => mediaQuery.removeListener(handleChange);
|
||||
mediaQuery.addListener(onThemeChange);
|
||||
return () => mediaQuery.removeListener(onThemeChange);
|
||||
}
|
||||
|
||||
return () => {}; // No-op cleanup
|
||||
return () => {};
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue