feat: Themes and appeareance settings are now local to your device and not synced (#2847)

This commit is contained in:
Aman Harwara
2024-02-17 14:23:37 +05:30
committed by GitHub
parent 3d1a038393
commit bfbf9ab8ce
18 changed files with 250 additions and 117 deletions

View File

@@ -27,10 +27,10 @@ export const PrefDefaults = {
[PrefKey.NotesHideDate]: false, [PrefKey.NotesHideDate]: false,
[PrefKey.NotesHideTags]: false, [PrefKey.NotesHideTags]: false,
[PrefKey.NotesHideEditorIcon]: false, [PrefKey.NotesHideEditorIcon]: false,
[PrefKey.UseSystemColorScheme]: false, [PrefKey.DEPRECATED_UseSystemColorScheme]: false,
[PrefKey.UseTranslucentUI]: true, [PrefKey.DEPRECATED_UseTranslucentUI]: true,
[PrefKey.AutoLightThemeIdentifier]: 'Default', [PrefKey.DEPRECATED_AutoLightThemeIdentifier]: 'Default',
[PrefKey.AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme, [PrefKey.DEPRECATED_AutoDarkThemeIdentifier]: NativeFeatureIdentifier.TYPES.DarkTheme,
[PrefKey.NoteAddToParentFolders]: true, [PrefKey.NoteAddToParentFolders]: true,
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime, [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat.CurrentDateAndTime,
[PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A', [PrefKey.CustomNoteTitleFormat]: 'YYYY-MM-DD [at] hh:mm A',
@@ -46,7 +46,7 @@ export const PrefDefaults = {
[PrefKey.SystemViewPreferences]: {}, [PrefKey.SystemViewPreferences]: {},
[PrefKey.AuthenticatorNames]: '', [PrefKey.AuthenticatorNames]: '',
[PrefKey.ComponentPreferences]: {}, [PrefKey.ComponentPreferences]: {},
[PrefKey.ActiveThemes]: [], [PrefKey.DEPRECATED_ActiveThemes]: [],
[PrefKey.ActiveComponents]: [], [PrefKey.ActiveComponents]: [],
[PrefKey.AlwaysShowSuperToolbar]: true, [PrefKey.AlwaysShowSuperToolbar]: true,
[PrefKey.AddImportsToTag]: true, [PrefKey.AddImportsToTag]: true,

View File

@@ -28,10 +28,6 @@ export enum PrefKey {
NotesHideDate = 'hideDate', NotesHideDate = 'hideDate',
NotesHideTags = 'hideTags', NotesHideTags = 'hideTags',
NotesHideEditorIcon = 'hideEditorIcon', NotesHideEditorIcon = 'hideEditorIcon',
UseSystemColorScheme = 'useSystemColorScheme',
UseTranslucentUI = 'useTranslucentUI',
AutoLightThemeIdentifier = 'autoLightThemeIdentifier',
AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier',
NoteAddToParentFolders = 'noteAddToParentFolders', NoteAddToParentFolders = 'noteAddToParentFolders',
NewNoteTitleFormat = 'newNoteTitleFormat', NewNoteTitleFormat = 'newNoteTitleFormat',
CustomNoteTitleFormat = 'customNoteTitleFormat', CustomNoteTitleFormat = 'customNoteTitleFormat',
@@ -47,12 +43,16 @@ export enum PrefKey {
AuthenticatorNames = 'authenticatorNames', AuthenticatorNames = 'authenticatorNames',
PaneGesturesEnabled = 'paneGesturesEnabled', PaneGesturesEnabled = 'paneGesturesEnabled',
ComponentPreferences = 'componentPreferences', ComponentPreferences = 'componentPreferences',
ActiveThemes = 'activeThemes',
ActiveComponents = 'activeComponents', ActiveComponents = 'activeComponents',
AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar', AlwaysShowSuperToolbar = 'alwaysShowSuperToolbar',
AddImportsToTag = 'addImportsToTag', AddImportsToTag = 'addImportsToTag',
AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports', AlwaysCreateNewTagForImports = 'alwaysCreateNewTagForImports',
ExistingTagForImports = 'existingTagForImports', ExistingTagForImports = 'existingTagForImports',
DEPRECATED_ActiveThemes = 'activeThemes',
DEPRECATED_UseSystemColorScheme = 'useSystemColorScheme',
DEPRECATED_UseTranslucentUI = 'useTranslucentUI',
DEPRECATED_AutoLightThemeIdentifier = 'autoLightThemeIdentifier',
DEPRECATED_AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier',
} }
export type PrefValue = { export type PrefValue = {
@@ -73,10 +73,11 @@ export type PrefValue = {
[PrefKey.NotesHideDate]: boolean [PrefKey.NotesHideDate]: boolean
[PrefKey.NotesHideTags]: boolean [PrefKey.NotesHideTags]: boolean
[PrefKey.NotesHideEditorIcon]: boolean [PrefKey.NotesHideEditorIcon]: boolean
[PrefKey.UseSystemColorScheme]: boolean [PrefKey.DEPRECATED_ActiveThemes]: string[]
[PrefKey.UseTranslucentUI]: boolean [PrefKey.DEPRECATED_UseSystemColorScheme]: boolean
[PrefKey.AutoLightThemeIdentifier]: string [PrefKey.DEPRECATED_UseTranslucentUI]: boolean
[PrefKey.AutoDarkThemeIdentifier]: string [PrefKey.DEPRECATED_AutoLightThemeIdentifier]: string
[PrefKey.DEPRECATED_AutoDarkThemeIdentifier]: string
[PrefKey.NoteAddToParentFolders]: boolean [PrefKey.NoteAddToParentFolders]: boolean
[PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat [PrefKey.NewNoteTitleFormat]: NewNoteTitleFormat
[PrefKey.CustomNoteTitleFormat]: string [PrefKey.CustomNoteTitleFormat]: string
@@ -95,7 +96,6 @@ export type PrefValue = {
[PrefKey.AuthenticatorNames]: string [PrefKey.AuthenticatorNames]: string
[PrefKey.PaneGesturesEnabled]: boolean [PrefKey.PaneGesturesEnabled]: boolean
[PrefKey.ComponentPreferences]: AllComponentPreferences [PrefKey.ComponentPreferences]: AllComponentPreferences
[PrefKey.ActiveThemes]: string[]
[PrefKey.ActiveComponents]: string[] [PrefKey.ActiveComponents]: string[]
[PrefKey.AlwaysShowSuperToolbar]: boolean [PrefKey.AlwaysShowSuperToolbar]: boolean
[PrefKey.AddImportsToTag]: boolean [PrefKey.AddImportsToTag]: boolean

View File

@@ -49,6 +49,7 @@ export enum ApplicationEvent {
/** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */ /** When StorageService is ready (but NOT yet decrypted) to start servicing read/write requests */
StorageReady = 'Application:StorageReady', StorageReady = 'Application:StorageReady',
PreferencesChanged = 'Application:PreferencesChanged', PreferencesChanged = 'Application:PreferencesChanged',
LocalPreferencesChanged = 'Application:LocalPreferencesChanged',
UnprotectedSessionBegan = 'Application:UnprotectedSessionBegan', UnprotectedSessionBegan = 'Application:UnprotectedSessionBegan',
UserRolesChanged = 'Application:UserRolesChanged', UserRolesChanged = 'Application:UserRolesChanged',
FeaturesAvailabilityChanged = 'Application:FeaturesAvailabilityChanged', FeaturesAvailabilityChanged = 'Application:FeaturesAvailabilityChanged',

View File

@@ -0,0 +1,15 @@
export enum LocalPrefKey {
ActiveThemes = 'activeThemes',
UseSystemColorScheme = 'useSystemColorScheme',
UseTranslucentUI = 'useTranslucentUI',
AutoLightThemeIdentifier = 'autoLightThemeIdentifier',
AutoDarkThemeIdentifier = 'autoDarkThemeIdentifier',
}
export type LocalPrefValue = {
[LocalPrefKey.ActiveThemes]: string[]
[LocalPrefKey.UseSystemColorScheme]: boolean
[LocalPrefKey.UseTranslucentUI]: boolean
[LocalPrefKey.AutoLightThemeIdentifier]: string
[LocalPrefKey.AutoDarkThemeIdentifier]: string
}

View File

@@ -1,7 +1,9 @@
import { PrefKey, PrefValue } from '@standardnotes/models' import { PrefKey, PrefValue } from '@standardnotes/models'
import { AbstractService } from '../Service/AbstractService' import { AbstractService } from '../Service/AbstractService'
import { LocalPrefKey, LocalPrefValue } from './LocalPrefKey'
export enum PreferencesServiceEvent { export enum PreferencesServiceEvent {
LocalPreferencesChanged = 'LocalPreferencesChanged',
PreferencesChanged = 'PreferencesChanged', PreferencesChanged = 'PreferencesChanged',
} }
@@ -10,7 +12,15 @@ export interface PreferenceServiceInterface extends AbstractService<PreferencesS
getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
getLocalValue<K extends LocalPrefKey>(key: K, defaultValue: LocalPrefValue[K]): LocalPrefValue[K]
getLocalValue<K extends LocalPrefKey>(key: K, defaultValue?: LocalPrefValue[K]): LocalPrefValue[K] | undefined
getLocalValue<K extends LocalPrefKey>(
key: K,
defaultValue: LocalPrefValue[K] | undefined,
): LocalPrefValue[K] | undefined
setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
/** Set value without triggering sync or event notifications */ /** Set value without triggering sync or event notifications */
setValueDetached<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> setValueDetached<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void>
setLocalValue<K extends LocalPrefKey>(key: K, value: LocalPrefValue[K]): void
} }

View File

@@ -50,6 +50,7 @@ export enum StorageKey {
FileBackupsLocation = 'file_backups_location', FileBackupsLocation = 'file_backups_location',
VaultSelectionOptions = 'vault_selection_options', VaultSelectionOptions = 'vault_selection_options',
Subscription = 'subscription', Subscription = 'subscription',
LocalPreferences = 'local_preferences',
} }
export enum NonwrappedStorageKey { export enum NonwrappedStorageKey {

View File

@@ -134,6 +134,7 @@ export * from './KeySystem/KeySystemKeyManager'
export * from './Mfa/MfaServiceInterface' export * from './Mfa/MfaServiceInterface'
export * from './Mutator/MutatorClientInterface' export * from './Mutator/MutatorClientInterface'
export * from './Payloads/PayloadManagerInterface' export * from './Payloads/PayloadManagerInterface'
export * from './Preferences/LocalPrefKey'
export * from './Preferences/PreferenceId' export * from './Preferences/PreferenceId'
export * from './Preferences/PreferenceServiceInterface' export * from './Preferences/PreferenceServiceInterface'
export * from './Protection/MobileUnlockTiming' export * from './Protection/MobileUnlockTiming'

View File

@@ -81,6 +81,7 @@ import {
CreateDecryptedBackupFile, CreateDecryptedBackupFile,
CreateEncryptedBackupFile, CreateEncryptedBackupFile,
WebSocketsService, WebSocketsService,
PreferencesServiceEvent,
} from '@standardnotes/services' } from '@standardnotes/services'
import { import {
SNNote, SNNote,
@@ -326,8 +327,12 @@ export class SNApplication implements ApplicationInterface, AppGroupManagedAppli
const preferencesService = this.dependencies.get<PreferencesService>(TYPES.PreferencesService) const preferencesService = this.dependencies.get<PreferencesService>(TYPES.PreferencesService)
this.serviceObservers.push( this.serviceObservers.push(
preferencesService.addEventObserver(() => { preferencesService.addEventObserver((event) => {
void this.notifyEvent(ApplicationEvent.PreferencesChanged) if (event === PreferencesServiceEvent.PreferencesChanged) {
void this.notifyEvent(ApplicationEvent.PreferencesChanged)
} else if (event === PreferencesServiceEvent.LocalPreferencesChanged) {
void this.notifyEvent(ApplicationEvent.LocalPreferencesChanged)
}
}), }),
) )

View File

@@ -147,6 +147,7 @@ import {
DesignateSurvivor, DesignateSurvivor,
SyncBackoffService, SyncBackoffService,
SyncBackoffServiceInterface, SyncBackoffServiceInterface,
StorageServiceInterface,
} from '@standardnotes/services' } from '@standardnotes/services'
import { ItemManager } from '../../Services/Items/ItemManager' import { ItemManager } from '../../Services/Items/ItemManager'
import { PayloadManager } from '../../Services/Payloads/PayloadManager' import { PayloadManager } from '../../Services/Payloads/PayloadManager'
@@ -1282,6 +1283,7 @@ export class Dependencies {
this.get<ItemManager>(TYPES.ItemManager), this.get<ItemManager>(TYPES.ItemManager),
this.get<MutatorService>(TYPES.MutatorService), this.get<MutatorService>(TYPES.MutatorService),
this.get<SyncService>(TYPES.SyncService), this.get<SyncService>(TYPES.SyncService),
this.get<StorageServiceInterface>(TYPES.DiskStorageService),
this.get<InternalEventBus>(TYPES.InternalEventBus), this.get<InternalEventBus>(TYPES.InternalEventBus),
) )
}) })

View File

@@ -66,7 +66,7 @@ export class Migration2_202_1 extends Migration {
const activeThemes = allActiveitems.filter((component) => component.isTheme()) const activeThemes = allActiveitems.filter((component) => component.isTheme())
const activeComponents = allActiveitems.filter((component) => !component.isTheme()) const activeComponents = allActiveitems.filter((component) => !component.isTheme())
await this.services.preferences.setValueDetached(PrefKey.ActiveThemes, Uuids(activeThemes)) await this.services.preferences.setValueDetached(PrefKey.DEPRECATED_ActiveThemes, Uuids(activeThemes))
await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents)) await this.services.preferences.setValueDetached(PrefKey.ActiveComponents, Uuids(activeComponents))
} }
} }

View File

@@ -0,0 +1,55 @@
import { LocalPrefKey, ApplicationStage } from '@standardnotes/services'
import { Migration } from '@Lib/Migrations/Migration'
import { PrefDefaults, PrefKey } from '@standardnotes/models'
export class Migration2_208_0 extends Migration {
static override version(): string {
return '2.208.0'
}
protected registerStageHandlers(): void {
this.registerStageHandler(ApplicationStage.FullSyncCompleted_13, async () => {
await this.migrateSyncedPreferencesToLocal()
this.markDone()
})
}
private async migrateSyncedPreferencesToLocal(): Promise<void> {
this.services.preferences.setLocalValue(
LocalPrefKey.ActiveThemes,
this.services.preferences.getValue(
PrefKey.DEPRECATED_ActiveThemes,
PrefDefaults[PrefKey.DEPRECATED_ActiveThemes],
),
)
this.services.preferences.setLocalValue(
LocalPrefKey.UseSystemColorScheme,
this.services.preferences.getValue(
PrefKey.DEPRECATED_UseSystemColorScheme,
PrefDefaults[PrefKey.DEPRECATED_UseSystemColorScheme],
),
)
this.services.preferences.setLocalValue(
LocalPrefKey.AutoLightThemeIdentifier,
this.services.preferences.getValue(
PrefKey.DEPRECATED_AutoLightThemeIdentifier,
PrefDefaults[PrefKey.DEPRECATED_AutoLightThemeIdentifier],
),
)
this.services.preferences.setLocalValue(
LocalPrefKey.AutoDarkThemeIdentifier,
this.services.preferences.getValue(
PrefKey.DEPRECATED_AutoDarkThemeIdentifier,
PrefDefaults[PrefKey.DEPRECATED_AutoDarkThemeIdentifier],
),
)
this.services.preferences.setLocalValue(
LocalPrefKey.UseTranslucentUI,
this.services.preferences.getValue(
PrefKey.DEPRECATED_UseTranslucentUI,
PrefDefaults[PrefKey.DEPRECATED_UseTranslucentUI],
),
)
}
}

View File

@@ -6,6 +6,7 @@ import { Migration2_42_0 } from './2_42_0'
import { Migration2_167_6 } from './2_167_6' import { Migration2_167_6 } from './2_167_6'
import { Migration2_168_6 } from './2_168_6' import { Migration2_168_6 } from './2_168_6'
import { Migration2_202_1 } from './2_202_1' import { Migration2_202_1 } from './2_202_1'
import { Migration2_208_0 } from './2_208_0'
export const MigrationClasses = [ export const MigrationClasses = [
Migration2_0_15, Migration2_0_15,
@@ -16,6 +17,7 @@ export const MigrationClasses = [
Migration2_167_6, Migration2_167_6,
Migration2_168_6, Migration2_168_6,
Migration2_202_1, Migration2_202_1,
Migration2_208_0,
] ]
export { export {
@@ -27,4 +29,5 @@ export {
Migration2_167_6, Migration2_167_6,
Migration2_168_6, Migration2_168_6,
Migration2_202_1, Migration2_202_1,
Migration2_208_0,
} }

View File

@@ -48,6 +48,7 @@ import {
ItemManagerInterface, ItemManagerInterface,
SyncServiceInterface, SyncServiceInterface,
FeatureStatus, FeatureStatus,
LocalPrefKey,
} from '@standardnotes/services' } from '@standardnotes/services'
import { GetFeatureUrl } from './UseCase/GetFeatureUrl' import { GetFeatureUrl } from './UseCase/GetFeatureUrl'
import { ComponentManagerEventData } from './ComponentManagerEventData' import { ComponentManagerEventData } from './ComponentManagerEventData'
@@ -393,7 +394,7 @@ export class ComponentManager
this.logger.info('Toggling theme', uiFeature.uniqueIdentifier) this.logger.info('Toggling theme', uiFeature.uniqueIdentifier)
if (this.isThemeActive(uiFeature)) { if (this.isThemeActive(uiFeature)) {
await this.removeActiveTheme(uiFeature) this.removeActiveTheme(uiFeature)
return return
} }
@@ -403,7 +404,7 @@ export class ComponentManager
} }
/* Activate current before deactivating others, so as not to flicker */ /* Activate current before deactivating others, so as not to flicker */
await this.addActiveTheme(uiFeature) this.addActiveTheme(uiFeature)
/* Deactive currently active theme(s) if new theme is not layerable */ /* Deactive currently active theme(s) if new theme is not layerable */
if (!uiFeature.layerable) { if (!uiFeature.layerable) {
@@ -416,7 +417,7 @@ export class ComponentManager
} }
if (!candidate.layerable) { if (!candidate.layerable) {
await this.removeActiveTheme(candidate) this.removeActiveTheme(candidate)
} }
} }
} }
@@ -453,7 +454,7 @@ export class ComponentManager
const features: NativeFeatureIdentifier[] = [] const features: NativeFeatureIdentifier[] = []
const uuids: Uuid[] = [] const uuids: Uuid[] = []
const strings = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const strings = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, [])
for (const string of strings) { for (const string of strings) {
const nativeIdentifier = NativeFeatureIdentifier.create(string) const nativeIdentifier = NativeFeatureIdentifier.create(string)
if (!nativeIdentifier.isFailed()) { if (!nativeIdentifier.isFailed()) {
@@ -534,24 +535,24 @@ export class ComponentManager
return preferences[preferencesLookupKey] return preferences[preferencesLookupKey]
} }
async addActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { addActiveTheme(theme: UIFeature<ThemeFeatureDescription>) {
const activeThemes = (this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? []).slice() const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, []).slice()
activeThemes.push(theme.uniqueIdentifier.value) activeThemes.push(theme.uniqueIdentifier.value)
await this.preferences.setValue(PrefKey.ActiveThemes, activeThemes) this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, activeThemes)
} }
async replaceActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { replaceActiveTheme(theme: UIFeature<ThemeFeatureDescription>) {
await this.preferences.setValue(PrefKey.ActiveThemes, [theme.uniqueIdentifier.value]) this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, [theme.uniqueIdentifier.value])
} }
async removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>): Promise<void> { removeActiveTheme(theme: UIFeature<ThemeFeatureDescription>) {
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, [])
const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier.value) const filteredThemes = activeThemes.filter((activeTheme) => activeTheme !== theme.uniqueIdentifier.value)
await this.preferences.setValue(PrefKey.ActiveThemes, filteredThemes) this.preferences.setLocalValue(LocalPrefKey.ActiveThemes, filteredThemes)
} }
isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean { isThemeActive(theme: UIFeature<ThemeFeatureDescription>): boolean {
@@ -559,13 +560,13 @@ export class ComponentManager
return false return false
} }
const activeThemes = this.preferences.getValue(PrefKey.ActiveThemes, undefined) ?? [] const activeThemes = this.preferences.getLocalValue(LocalPrefKey.ActiveThemes, [])
return activeThemes.includes(theme.uniqueIdentifier.value) return activeThemes.includes(theme.uniqueIdentifier.value)
} }
async addActiveComponent(component: ComponentInterface): Promise<void> { async addActiveComponent(component: ComponentInterface): Promise<void> {
const activeComponents = (this.preferences.getValue(PrefKey.ActiveComponents, undefined) ?? []).slice() const activeComponents = this.preferences.getValue(PrefKey.ActiveComponents, []).slice()
activeComponents.push(component.uuid) activeComponents.push(component.uuid)

View File

@@ -14,6 +14,10 @@ import {
InternalEventInterface, InternalEventInterface,
ApplicationEvent, ApplicationEvent,
ApplicationStageChangedEventPayload, ApplicationStageChangedEventPayload,
StorageServiceInterface,
StorageKey,
LocalPrefKey,
LocalPrefValue,
} from '@standardnotes/services' } from '@standardnotes/services'
import { ContentType } from '@standardnotes/domain-core' import { ContentType } from '@standardnotes/domain-core'
@@ -24,6 +28,7 @@ export class PreferencesService
private shouldReload = true private shouldReload = true
private reloading = false private reloading = false
private preferences?: SNUserPrefs private preferences?: SNUserPrefs
private localPreferences: { [key in LocalPrefKey]?: LocalPrefValue[key] } = {}
private removeItemObserver?: () => void private removeItemObserver?: () => void
private removeSyncObserver?: () => void private removeSyncObserver?: () => void
@@ -32,6 +37,7 @@ export class PreferencesService
items: ItemManager, items: ItemManager,
private mutator: MutatorClientInterface, private mutator: MutatorClientInterface,
private sync: SyncService, private sync: SyncService,
private storage: StorageServiceInterface,
protected override internalEventBus: InternalEventBusInterface, protected override internalEventBus: InternalEventBusInterface,
) { ) {
super(internalEventBus) super(internalEventBus)
@@ -69,16 +75,36 @@ export class PreferencesService
if (this.preferences) { if (this.preferences) {
void this.notifyEvent(PreferencesServiceEvent.PreferencesChanged) void this.notifyEvent(PreferencesServiceEvent.PreferencesChanged)
} }
} else if (stage === ApplicationStage.StorageDecrypted_09) {
this.localPreferences = this.storage.getValue(StorageKey.LocalPreferences) ?? {}
void this.notifyEvent(PreferencesServiceEvent.LocalPreferencesChanged)
} }
} }
} }
getLocalValue<K extends LocalPrefKey>(
key: K,
defaultValue: LocalPrefValue[K] | undefined,
): LocalPrefValue[K] | undefined
getLocalValue<K extends LocalPrefKey>(key: K, defaultValue: LocalPrefValue[K]): LocalPrefValue[K]
getLocalValue<K extends LocalPrefKey>(key: K, defaultValue?: LocalPrefValue[K]): LocalPrefValue[K] | undefined {
return this.localPreferences[key] ?? defaultValue
}
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K] | undefined): PrefValue[K] | undefined
getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K] getValue<K extends PrefKey>(key: K, defaultValue: PrefValue[K]): PrefValue[K]
getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined { getValue<K extends PrefKey>(key: K, defaultValue?: PrefValue[K]): PrefValue[K] | undefined {
return this.preferences?.getPref(key) ?? defaultValue return this.preferences?.getPref(key) ?? defaultValue
} }
setLocalValue<K extends LocalPrefKey>(key: K, value: LocalPrefValue[K]): void {
this.localPreferences[key] = value
this.storage.setValue(StorageKey.LocalPreferences, this.localPreferences)
void this.notifyEvent(PreferencesServiceEvent.LocalPreferencesChanged)
}
async setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> { async setValue<K extends PrefKey>(key: K, value: PrefValue[K]): Promise<void> {
await this.setValueDetached(key, value) await this.setValueDetached(key, value)

View File

@@ -2,7 +2,6 @@ import {
UIFeature, UIFeature,
CreateDecryptedLocalStorageContextPayload, CreateDecryptedLocalStorageContextPayload,
LocalStorageDecryptedContextualPayload, LocalStorageDecryptedContextualPayload,
PrefKey,
PrefDefaults, PrefDefaults,
ComponentInterface, ComponentInterface,
} from '@standardnotes/models' } from '@standardnotes/models'
@@ -12,8 +11,8 @@ import {
StorageValueModes, StorageValueModes,
FeatureStatus, FeatureStatus,
PreferenceServiceInterface, PreferenceServiceInterface,
PreferencesServiceEvent,
ComponentManagerInterface, ComponentManagerInterface,
LocalPrefKey,
} from '@standardnotes/services' } from '@standardnotes/services'
import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features' import { NativeFeatureIdentifier, FindNativeTheme, ThemeFeatureDescription } from '@standardnotes/features'
import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface' import { WebApplicationInterface } from '../WebApplication/WebApplicationInterface'
@@ -75,56 +74,6 @@ export class ThemeManager extends AbstractUIService {
}), }),
) )
} }
this.eventDisposers.push(
this.preferences.addEventObserver(async (event) => {
if (event !== PreferencesServiceEvent.PreferencesChanged) {
return
}
this.toggleTranslucentUIColors()
let hasChange = false
const { features, uuids } = this.components.getActiveThemesIdentifiers()
const featuresList = new ActiveThemeList(this.application.items, features)
const uuidsList = new ActiveThemeList(this.application.items, uuids)
for (const active of this.themesActiveInTheUI.getList()) {
if (!featuresList.has(active) && !uuidsList.has(active)) {
this.deactivateThemeInTheUI(active)
hasChange = true
}
}
for (const feature of features) {
if (!this.themesActiveInTheUI.has(feature)) {
const theme = FindNativeTheme(feature.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature)
hasChange = true
}
}
}
for (const uuid of uuids) {
if (!this.themesActiveInTheUI.has(uuid)) {
const theme = this.application.items.findItem<ComponentInterface>(uuid.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature)
hasChange = true
}
}
}
if (hasChange) {
this.cacheThemeState().catch(console.error)
}
}),
)
} }
override async onAppEvent(event: ApplicationEvent) { override async onAppEvent(event: ApplicationEvent) {
@@ -154,15 +103,15 @@ export class ThemeManager extends AbstractUIService {
} }
break break
} }
case ApplicationEvent.PreferencesChanged: { case ApplicationEvent.LocalPreferencesChanged: {
void this.handlePreferencesChangeEvent() void this.handleLocalPreferencesChangeEvent()
break break
} }
} }
} }
async handleMobileColorSchemeChangeEvent() { async handleMobileColorSchemeChangeEvent() {
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
if (useDeviceThemeSettings) { if (useDeviceThemeSettings) {
const prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark' const prefersDarkColorScheme = (await this.application.mobileDevice.getColorScheme()) === 'dark'
@@ -170,10 +119,54 @@ export class ThemeManager extends AbstractUIService {
} }
} }
private async handlePreferencesChangeEvent() { private handleThemeStateChange() {
let hasChange = false
const { features, uuids } = this.components.getActiveThemesIdentifiers()
const featuresList = new ActiveThemeList(this.application.items, features)
const uuidsList = new ActiveThemeList(this.application.items, uuids)
for (const active of this.themesActiveInTheUI.getList()) {
if (!featuresList.has(active) && !uuidsList.has(active)) {
this.deactivateThemeInTheUI(active)
hasChange = true
}
}
for (const feature of features) {
if (!this.themesActiveInTheUI.has(feature)) {
const theme = FindNativeTheme(feature.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature)
hasChange = true
}
}
}
for (const uuid of uuids) {
if (!this.themesActiveInTheUI.has(uuid)) {
const theme = this.application.items.findItem<ComponentInterface>(uuid.value)
if (theme) {
const uiFeature = new UIFeature<ThemeFeatureDescription>(theme)
this.activateTheme(uiFeature)
hasChange = true
}
}
}
if (hasChange) {
this.cacheThemeState().catch(console.error)
}
}
private async handleLocalPreferencesChangeEvent() {
this.handleThemeStateChange()
this.toggleTranslucentUIColors() this.toggleTranslucentUIColors()
const useDeviceThemeSettings = this.application.getPreference(PrefKey.UseSystemColorScheme, false) const useDeviceThemeSettings = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings const hasPreferenceChanged = useDeviceThemeSettings !== this.lastUseDeviceThemeSettings
@@ -218,7 +211,7 @@ export class ThemeManager extends AbstractUIService {
} }
private colorSchemeEventHandler(event: MediaQueryListEvent) { private colorSchemeEventHandler(event: MediaQueryListEvent) {
const shouldChangeTheme = this.application.getPreference(PrefKey.UseSystemColorScheme, false) const shouldChangeTheme = this.preferences.getLocalValue(LocalPrefKey.UseSystemColorScheme, false)
if (shouldChangeTheme) { if (shouldChangeTheme) {
this.setThemeAsPerColorScheme(event.matches) this.setThemeAsPerColorScheme(event.matches)
@@ -226,10 +219,14 @@ export class ThemeManager extends AbstractUIService {
} }
private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) { private setThemeAsPerColorScheme(prefersDarkColorScheme: boolean) {
const preference = prefersDarkColorScheme ? PrefKey.AutoDarkThemeIdentifier : PrefKey.AutoLightThemeIdentifier const preference = prefersDarkColorScheme
? LocalPrefKey.AutoDarkThemeIdentifier
: LocalPrefKey.AutoLightThemeIdentifier
const preferenceDefault = const preferenceDefault =
preference === PrefKey.AutoDarkThemeIdentifier ? NativeFeatureIdentifier.TYPES.DarkTheme : DefaultThemeIdentifier preference === LocalPrefKey.AutoDarkThemeIdentifier
? NativeFeatureIdentifier.TYPES.DarkTheme
: DefaultThemeIdentifier
const usecase = new GetAllThemesUseCase(this.application.items) const usecase = new GetAllThemesUseCase(this.application.items)
const { thirdParty, native } = usecase.execute({ excludeLayerable: false }) const { thirdParty, native } = usecase.execute({ excludeLayerable: false })
@@ -237,7 +234,7 @@ export class ThemeManager extends AbstractUIService {
const activeTheme = themes.find((theme) => this.components.isThemeActive(theme) && !theme.layerable) const activeTheme = themes.find((theme) => this.components.isThemeActive(theme) && !theme.layerable)
const themeIdentifier = this.preferences.getValue(preference, preferenceDefault) const themeIdentifier = this.preferences.getLocalValue(preference, preferenceDefault)
const toggleActiveTheme = () => { const toggleActiveTheme = () => {
if (activeTheme) { if (activeTheme) {
@@ -337,7 +334,7 @@ export class ThemeManager extends AbstractUIService {
} }
private shouldUseTranslucentUI() { private shouldUseTranslucentUI() {
return this.application.getPreference(PrefKey.UseTranslucentUI, PrefDefaults[PrefKey.UseTranslucentUI]) return this.preferences.getLocalValue(LocalPrefKey.UseTranslucentUI, PrefDefaults[LocalPrefKey.UseTranslucentUI])
} }
private toggleTranslucentUIColors() { private toggleTranslucentUIColors() {

View File

@@ -4,7 +4,7 @@ import { usePremiumModal } from '@/Hooks/usePremiumModal'
import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator' import HorizontalSeparator from '@/Components/Shared/HorizontalSeparator'
import Switch from '@/Components/Switch/Switch' import Switch from '@/Components/Switch/Switch'
import { WebApplication } from '@/Application/WebApplication' import { WebApplication } from '@/Application/WebApplication'
import { PrefKey, FeatureStatus, naturalSort, PrefDefaults } from '@standardnotes/snjs' import { FeatureStatus, naturalSort, LocalPrefKey } from '@standardnotes/snjs'
import { observer } from 'mobx-react-lite' import { observer } from 'mobx-react-lite'
import { FunctionComponent, useEffect, useState } from 'react' import { FunctionComponent, useEffect, useState } from 'react'
import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content' import { Subtitle, Title, Text } from '@/Components/Preferences/PreferencesComponents/Content'
@@ -14,7 +14,7 @@ import PreferencesSegment from '../PreferencesComponents/PreferencesSegment'
import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon' import { PremiumFeatureIconName } from '@/Components/Icon/PremiumFeatureIcon'
import EditorAppearance from './Appearance/EditorAppearance' import EditorAppearance from './Appearance/EditorAppearance'
import { GetAllThemesUseCase } from '@standardnotes/ui-services' import { GetAllThemesUseCase } from '@standardnotes/ui-services'
import usePreference from '@/Hooks/usePreference' import { useLocalPreference } from '@/Hooks/usePreference'
type Props = { type Props = {
application: WebApplication application: WebApplication
@@ -24,19 +24,14 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
const premiumModal = usePremiumModal() const premiumModal = usePremiumModal()
const [themeItems, setThemeItems] = useState<DropdownItem[]>([]) const [themeItems, setThemeItems] = useState<DropdownItem[]>([])
const [autoLightTheme, setAutoLightTheme] = useState<string>(() =>
application.getPreference(PrefKey.AutoLightThemeIdentifier, PrefDefaults[PrefKey.AutoLightThemeIdentifier]),
)
const [autoDarkTheme, setAutoDarkTheme] = useState<string>(() =>
application.getPreference(PrefKey.AutoDarkThemeIdentifier, PrefDefaults[PrefKey.AutoDarkThemeIdentifier]),
)
const [useDeviceSettings, setUseDeviceSettings] = useState(() =>
application.getPreference(PrefKey.UseSystemColorScheme, PrefDefaults[PrefKey.UseSystemColorScheme]),
)
const useTranslucentUI = usePreference(PrefKey.UseTranslucentUI) const [autoLightTheme, setAutoLightTheme] = useLocalPreference(LocalPrefKey.AutoLightThemeIdentifier)
const [autoDarkTheme, setAutoDarkTheme] = useLocalPreference(LocalPrefKey.AutoDarkThemeIdentifier)
const [useDeviceSettings, setUseDeviceSettings] = useLocalPreference(LocalPrefKey.UseSystemColorScheme)
const [useTranslucentUI, setUseTranslucentUI] = useLocalPreference(LocalPrefKey.UseTranslucentUI)
const toggleTranslucentUI = () => { const toggleTranslucentUI = () => {
application.setPreference(PrefKey.UseTranslucentUI, !useTranslucentUI).catch(console.error) setUseTranslucentUI(!useTranslucentUI)
} }
useEffect(() => { useEffect(() => {
@@ -76,12 +71,12 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
}, [application]) }, [application])
const toggleUseDeviceSettings = () => { const toggleUseDeviceSettings = () => {
application.setPreference(PrefKey.UseSystemColorScheme, !useDeviceSettings).catch(console.error) setUseDeviceSettings(!useDeviceSettings)
if (!application.getPreference(PrefKey.AutoLightThemeIdentifier)) { if (!application.preferences.getLocalValue(LocalPrefKey.AutoLightThemeIdentifier)) {
application.setPreference(PrefKey.AutoLightThemeIdentifier, autoLightTheme).catch(console.error) setAutoLightTheme(autoLightTheme)
} }
if (!application.getPreference(PrefKey.AutoDarkThemeIdentifier)) { if (!application.preferences.getLocalValue(LocalPrefKey.AutoDarkThemeIdentifier)) {
application.setPreference(PrefKey.AutoDarkThemeIdentifier, autoDarkTheme).catch(console.error) setAutoDarkTheme(autoDarkTheme)
} }
setUseDeviceSettings(!useDeviceSettings) setUseDeviceSettings(!useDeviceSettings)
} }
@@ -92,7 +87,6 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
return return
} }
application.setPreference(PrefKey.AutoLightThemeIdentifier, value).catch(console.error)
setAutoLightTheme(value) setAutoLightTheme(value)
} }
@@ -102,7 +96,6 @@ const Appearance: FunctionComponent<Props> = ({ application }) => {
premiumModal.activate(`${item.label} theme`) premiumModal.activate(`${item.label} theme`)
return return
} }
application.setPreference(PrefKey.AutoDarkThemeIdentifier, value).catch(console.error)
setAutoDarkTheme(value) setAutoDarkTheme(value)
} }

View File

@@ -76,7 +76,7 @@ const QuickSettingsMenu: FunctionComponent<MenuProps> = ({ closeMenu }) => {
useEffect(() => { useEffect(() => {
return application.preferences.addEventObserver((event) => { return application.preferences.addEventObserver((event) => {
if (event === PreferencesServiceEvent.PreferencesChanged) { if (event === PreferencesServiceEvent.LocalPreferencesChanged) {
reloadThemes() reloadThemes()
} }
}) })

View File

@@ -1,6 +1,29 @@
import { useApplication } from '@/Components/ApplicationProvider' import { useApplication } from '@/Components/ApplicationProvider'
import { ApplicationEvent, PrefKey, PrefDefaults } from '@standardnotes/snjs' import { ApplicationEvent, PrefKey, PrefDefaults, LocalPrefKey, LocalPrefValue } from '@standardnotes/snjs'
import { useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
export function useLocalPreference<Key extends LocalPrefKey>(preference: Key) {
const application = useApplication()
const [value, setValue] = useState(application.preferences.getLocalValue(preference, PrefDefaults[preference]))
const setNewValue = useCallback(
(newValue: LocalPrefValue[Key]) => {
application.preferences.setLocalValue(preference, newValue)
},
[application, preference],
)
useEffect(() => {
return application.addEventObserver(async () => {
const latestValue = application.preferences.getLocalValue(preference, PrefDefaults[preference])
setValue(latestValue)
}, ApplicationEvent.LocalPreferencesChanged)
}, [application, preference])
return [value, setNewValue] as const
}
export default function usePreference<Key extends PrefKey>(preference: Key) { export default function usePreference<Key extends PrefKey>(preference: Key) {
const application = useApplication() const application = useApplication()