diff --git a/src/interfaces.ts b/src/interfaces.ts index a5dcf8dbe4..99b19a44d3 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -184,3 +184,39 @@ export interface PMOperationOptions { dir: string; packageManager: IPackageManager; } + +export enum GlobalSetting { + acceleratorsToBlock = 'acceleratorsToBlock', + channelsToShow = 'channelsToShow', + electronMirror = 'electronMirror', + environmentVariables = 'environmentVariables', + executionFlags = 'executionFlags', + fontFamily = 'fontFamily', + fontSize = 'fontSize', + gitHubAvatarUrl = 'gitHubAvatarUrl', + gitHubLogin = 'gitHubLogin', + gitHubName = 'gitHubName', + gitHubToken = 'gitHubToken', + hasShownTour = 'hasShownTour', + isClearingConsoleOnRun = 'isClearingConsoleOnRun', + isEnablingElectronLogging = 'isEnablingElectronLogging', + isKeepingUserDataDirs = 'isKeepingUserDataDirs', + isPublishingGistAsRevision = 'isPublishingGistAsRevision', + isUsingSystemTheme = 'isUsingSystemTheme', + knownVersion = 'known-electron-versions', + localVersion = 'local-electron-versions', + packageAuthor = 'packageAuthor', + packageManager = 'packageManager', + showObsoleteVersions = 'showObsoleteVersions', + showUndownloadedVersions = 'showUndownloadedVersions', + theme = 'theme', +} + +export type GlobalSettingKey = keyof typeof GlobalSetting; + +export enum WindowSpecificSetting { + gitHubPublishAsPublic = 'gitHubPublishAsPublic', + version = 'version', +} + +export type WindowSpecificSettingKey = keyof typeof WindowSpecificSetting; diff --git a/src/renderer/components/settings-execution.tsx b/src/renderer/components/settings-execution.tsx index 95ab62c8ba..43bf1cb59a 100644 --- a/src/renderer/components/settings-execution.tsx +++ b/src/renderer/components/settings-execution.tsx @@ -12,13 +12,20 @@ import { } from '@blueprintjs/core'; import { observer } from 'mobx-react'; -import { IPackageManager } from '../../interfaces'; +import { + GlobalSetting, + GlobalSettingKey, + IPackageManager, +} from '../../interfaces'; import { AppState } from '../state'; -export enum SettingItemType { - EnvVars = 'environmentVariables', - Flags = 'executionFlags', -} +/** + * @TODO make this a proper enum again once we update Typescript + */ +export const SettingItemType = { + EnvVars: GlobalSetting.environmentVariables, + Flags: GlobalSetting.executionFlags, +} as const; interface ExecutionSettingsProps { appState: AppState; @@ -119,7 +126,7 @@ export const ExecutionSettings = observer( */ public handleSettingsItemChange( event: React.ChangeEvent, - type: SettingItemType, + type: GlobalSettingKey, ) { const { name, value } = event.currentTarget; @@ -136,7 +143,7 @@ export const ExecutionSettings = observer( * * @param {SettingItemType} type */ - private addNewSettingsItem(type: SettingItemType) { + private addNewSettingsItem(type: GlobalSettingKey) { const array = Object.entries(this.state[type]); this.setState((prevState) => ({ @@ -160,7 +167,7 @@ export const ExecutionSettings = observer( appState.packageManager = value as IPackageManager; }; - public renderDeleteItem(idx: string, type: SettingItemType): JSX.Element { + public renderDeleteItem(idx: string, type: GlobalSettingKey): JSX.Element { const updated = this.state[type]; const removeFn = () => { diff --git a/src/renderer/state.ts b/src/renderer/state.ts index 9d59579737..d2663563a4 100644 --- a/src/renderer/state.ts +++ b/src/renderer/state.ts @@ -21,6 +21,8 @@ import { GenericDialogOptions, GenericDialogType, GistActionState, + GlobalSetting, + GlobalSettingKey, IPackageManager, InstallState, OutputEntry, @@ -29,6 +31,8 @@ import { SetFiddleOptions, Version, VersionSource, + WindowSpecificSetting, + WindowSpecificSettingKey, } from '../interfaces'; import { Bisector } from './bisect'; import { ELECTRON_DOWNLOAD_PATH, ELECTRON_INSTALL_PATH } from './constants'; @@ -63,63 +67,87 @@ export class AppState { timeStyle: 'medium', }); + private settingKeyTypeGuard(key: never): never { + throw new Error( + `Unhandled setting ${key}, please handle it in the \`AppState\`.`, + ); + } + // -- Persisted settings ------------------ - public theme: string | null = localStorage.getItem('theme'); + public theme: string | null = localStorage.getItem(GlobalSetting.theme); public gitHubAvatarUrl: string | null = localStorage.getItem( - 'gitHubAvatarUrl', + GlobalSetting.gitHubAvatarUrl, + ); + public gitHubName: string | null = localStorage.getItem( + GlobalSetting.gitHubName, + ); + public gitHubLogin: string | null = localStorage.getItem( + GlobalSetting.gitHubLogin, ); - public gitHubName: string | null = localStorage.getItem('gitHubName'); - public gitHubLogin: string | null = localStorage.getItem('gitHubLogin'); public gitHubToken: string | null = - localStorage.getItem('gitHubToken') || null; - public gitHubPublishAsPublic = !!this.retrieve('gitHubPublishAsPublic'); + localStorage.getItem(GlobalSetting.gitHubToken) || null; + public gitHubPublishAsPublic = !!this.retrieve( + WindowSpecificSetting.gitHubPublishAsPublic, + ); public channelsToShow: Array = (this.retrieve( - 'channelsToShow', + GlobalSetting.channelsToShow, ) as Array) || [ ElectronReleaseChannel.stable, ElectronReleaseChannel.beta, ]; public showObsoleteVersions = !!( - this.retrieve('showObsoleteVersions') ?? false + this.retrieve(GlobalSetting.showObsoleteVersions) ?? false ); public showUndownloadedVersions = !!( - this.retrieve('showUndownloadedVersions') ?? true + this.retrieve(GlobalSetting.showUndownloadedVersions) ?? true + ); + public isKeepingUserDataDirs = !!this.retrieve( + GlobalSetting.isKeepingUserDataDirs, ); - public isKeepingUserDataDirs = !!this.retrieve('isKeepingUserDataDirs'); public isEnablingElectronLogging = !!this.retrieve( - 'isEnablingElectronLogging', + GlobalSetting.isEnablingElectronLogging, + ); + public isClearingConsoleOnRun = !!this.retrieve( + GlobalSetting.isClearingConsoleOnRun, + ); + public isUsingSystemTheme = !!( + this.retrieve(GlobalSetting.isUsingSystemTheme) ?? true ); - public isClearingConsoleOnRun = !!this.retrieve('isClearingConsoleOnRun'); - public isUsingSystemTheme = !!(this.retrieve('isUsingSystemTheme') ?? true); public isPublishingGistAsRevision = !!( - this.retrieve('isPublishingGistAsRevision') ?? true + this.retrieve(GlobalSetting.isPublishingGistAsRevision) ?? true ); public executionFlags: Array = - (this.retrieve('executionFlags') as Array) === null + (this.retrieve(GlobalSetting.executionFlags) as Array) === null ? [] - : (this.retrieve('executionFlags') as Array); + : (this.retrieve(GlobalSetting.executionFlags) as Array); public environmentVariables: Array = - (this.retrieve('environmentVariables') as Array) === null + (this.retrieve(GlobalSetting.environmentVariables) as Array) === + null ? [] - : (this.retrieve('environmentVariables') as Array); + : (this.retrieve(GlobalSetting.environmentVariables) as Array); public packageManager: IPackageManager = - (localStorage.getItem('packageManager') as IPackageManager) || 'npm'; + (localStorage.getItem(GlobalSetting.packageManager) as IPackageManager) || + 'npm'; public acceleratorsToBlock: Array = - (this.retrieve('acceleratorsToBlock') as Array) || []; + (this.retrieve( + GlobalSetting.acceleratorsToBlock, + ) as Array) || []; public packageAuthor = - (localStorage.getItem('packageAuthor') as string) ?? + (localStorage.getItem(GlobalSetting.packageAuthor) as string) ?? window.ElectronFiddle.getUsername(); public electronMirror: typeof ELECTRON_MIRROR = - (this.retrieve('electronMirror') as typeof ELECTRON_MIRROR) === null + (this.retrieve(GlobalSetting.electronMirror) as typeof ELECTRON_MIRROR) === + null ? { ...ELECTRON_MIRROR, sourceType: navigator.language === 'zh-CN' ? 'CHINA' : 'DEFAULT', } - : (this.retrieve('electronMirror') as typeof ELECTRON_MIRROR); + : (this.retrieve(GlobalSetting.electronMirror) as typeof ELECTRON_MIRROR); public fontFamily: string | undefined = - (localStorage.getItem('fontFamily') as string) || undefined; + (localStorage.getItem(GlobalSetting.fontFamily) as string) || undefined; public fontSize: number | undefined = - ((localStorage.getItem('fontSize') as any) as number) || undefined; + ((localStorage.getItem(GlobalSetting.fontSize) as any) as number) || + undefined; // -- Various session-only state ------------------ public gistId: string | undefined = undefined; @@ -158,7 +186,7 @@ export class AppState { public isSettingsShowing = false; public isThemeDialogShowing = false; public isTokenDialogShowing = false; - public isTourShowing = !localStorage.getItem('hasShownTour'); + public isTourShowing = !localStorage.getItem(GlobalSetting.hasShownTour); public isUpdatingElectronVersions = false; // -- Editor Values stored when we close the editor ------------------ @@ -329,41 +357,137 @@ export class AppState { ); window.ElectronFiddle.addEventListener('before-quit', this.setIsQuitting); + /** + * Listens for changes in the app settings made in other windows + * and refreshes the current window settings accordingly. + */ + window.addEventListener('storage', (event) => { + const key = event.key as GlobalSettingKey; + const { newValue } = event; + + let parsedValue: unknown; + + try { + parsedValue = JSON.parse(newValue as string) as unknown; + } catch { + // The new value is a plain string, not a well-formed stringified object. + parsedValue = newValue; + } + + if (key in GlobalSetting && key in this) { + switch (key) { + case 'theme': { + this.setTheme(parsedValue as string); + break; + } + + case 'acceleratorsToBlock': + case 'channelsToShow': + case 'electronMirror': + case 'environmentVariables': + case 'executionFlags': + case 'fontFamily': + case 'fontSize': + case 'gitHubAvatarUrl': + case 'gitHubLogin': + case 'gitHubName': + case 'gitHubToken': + case 'hasShownTour': + case 'isClearingConsoleOnRun': + case 'isEnablingElectronLogging': + case 'isKeepingUserDataDirs': + case 'isPublishingGistAsRevision': + case 'isUsingSystemTheme': + case 'knownVersion': + case 'localVersion': + case 'packageAuthor': + case 'packageManager': + case 'showObsoleteVersions': + case 'showUndownloadedVersions': { + // Fall back to updating the state. + this[key] = parsedValue; + break; + } + + default: { + this.settingKeyTypeGuard(key); + } + } + } else { + console.warn( + `"${key}" is not a recognized localStorage key. If you're using this key to persist a setting, please add it to the relevant enum.`, + ); + } + }); + // Setup auto-runs - autorun(() => this.save('theme', this.theme)); + autorun(() => this.save(GlobalSetting.theme, this.theme)); + autorun(() => + this.save( + GlobalSetting.isClearingConsoleOnRun, + this.isClearingConsoleOnRun, + ), + ); + autorun(() => + this.save(GlobalSetting.isUsingSystemTheme, this.isUsingSystemTheme), + ); + autorun(() => + this.save( + GlobalSetting.isPublishingGistAsRevision, + this.isPublishingGistAsRevision, + ), + ); + autorun(() => + this.save(GlobalSetting.gitHubAvatarUrl, this.gitHubAvatarUrl), + ); + autorun(() => this.save(GlobalSetting.gitHubLogin, this.gitHubLogin)); + autorun(() => this.save(GlobalSetting.gitHubName, this.gitHubName)); + autorun(() => this.save(GlobalSetting.gitHubToken, this.gitHubToken)); + autorun(() => + this.save( + WindowSpecificSetting.gitHubPublishAsPublic, + this.gitHubPublishAsPublic, + ), + ); + autorun(() => + this.save( + GlobalSetting.isKeepingUserDataDirs, + this.isKeepingUserDataDirs, + ), + ); + autorun(() => + this.save( + GlobalSetting.isEnablingElectronLogging, + this.isEnablingElectronLogging, + ), + ); + autorun(() => this.save(GlobalSetting.executionFlags, this.executionFlags)); autorun(() => - this.save('isClearingConsoleOnRun', this.isClearingConsoleOnRun), + this.save(GlobalSetting.environmentVariables, this.environmentVariables), ); - autorun(() => this.save('isUsingSystemTheme', this.isUsingSystemTheme)); + autorun(() => this.save(WindowSpecificSetting.version, this.version)); + autorun(() => this.save(GlobalSetting.channelsToShow, this.channelsToShow)); autorun(() => - this.save('isPublishingGistAsRevision', this.isPublishingGistAsRevision), + this.save( + GlobalSetting.showUndownloadedVersions, + this.showUndownloadedVersions, + ), ); - autorun(() => this.save('gitHubAvatarUrl', this.gitHubAvatarUrl)); - autorun(() => this.save('gitHubLogin', this.gitHubLogin)); - autorun(() => this.save('gitHubName', this.gitHubName)); - autorun(() => this.save('gitHubToken', this.gitHubToken)); autorun(() => - this.save('gitHubPublishAsPublic', this.gitHubPublishAsPublic), + this.save(GlobalSetting.showObsoleteVersions, this.showObsoleteVersions), ); autorun(() => - this.save('isKeepingUserDataDirs', this.isKeepingUserDataDirs), + this.save(GlobalSetting.packageManager, this.packageManager ?? 'npm'), ); autorun(() => - this.save('isEnablingElectronLogging', this.isEnablingElectronLogging), + this.save(GlobalSetting.acceleratorsToBlock, this.acceleratorsToBlock), ); - autorun(() => this.save('executionFlags', this.executionFlags)); - autorun(() => this.save('version', this.version)); - autorun(() => this.save('channelsToShow', this.channelsToShow)); + autorun(() => this.save(GlobalSetting.packageAuthor, this.packageAuthor)); autorun(() => - this.save('showUndownloadedVersions', this.showUndownloadedVersions), + this.save(GlobalSetting.electronMirror, this.electronMirror as any), ); - autorun(() => this.save('showObsoleteVersions', this.showObsoleteVersions)); - autorun(() => this.save('packageManager', this.packageManager ?? 'npm')); - autorun(() => this.save('acceleratorsToBlock', this.acceleratorsToBlock)); - autorun(() => this.save('packageAuthor', this.packageAuthor)); - autorun(() => this.save('electronMirror', this.electronMirror as any)); - autorun(() => this.save('fontFamily', this.fontFamily as any)); - autorun(() => this.save('fontSize', this.fontSize as any)); + autorun(() => this.save(GlobalSetting.fontFamily, this.fontFamily as any)); + autorun(() => this.save(GlobalSetting.fontSize, this.fontSize as any)); // Update our known versions this.updateElectronVersions(); @@ -514,7 +638,7 @@ export class AppState { public disableTour() { this.resetView(); - localStorage.setItem('hasShownTour', 'true'); + localStorage.setItem(GlobalSetting.hasShownTour, 'true'); } public showTour() { @@ -935,7 +1059,7 @@ export class AppState { * @param {(string | number | Array | Record | null | boolean)} [value] */ private save( - key: string, + key: GlobalSettingKey | WindowSpecificSettingKey, value?: | string | number @@ -961,7 +1085,9 @@ export class AppState { * @param {string} key * @returns {(T | string | null)} */ - private retrieve(key: string): T | string | null { + private retrieve( + key: GlobalSettingKey | WindowSpecificSettingKey, + ): T | string | null { const value = localStorage.getItem(key); return JSON.parse(value || 'null') as T; diff --git a/src/renderer/versions.ts b/src/renderer/versions.ts index 6ccfa5e58a..8e400af348 100644 --- a/src/renderer/versions.ts +++ b/src/renderer/versions.ts @@ -1,9 +1,11 @@ import { ElectronReleaseChannel, + GlobalSetting, InstallState, RunnableVersion, Version, VersionSource, + WindowSpecificSetting, } from '../interfaces'; import { normalizeVersion } from './utils/normalize-version'; @@ -14,8 +16,10 @@ import { normalizeVersion } from './utils/normalize-version'; * @returns {string} */ export function getDefaultVersion(versions: RunnableVersion[]): string { - const key = localStorage.getItem('version'); - if (key && versions.some(({ version }) => version === key)) return key; + const key = localStorage.getItem(WindowSpecificSetting.version); + if (key && versions.some(({ version }) => version === key)) { + return key; + } const latestStable = window.ElectronFiddle.getLatestStable(); if (latestStable) return latestStable.version; @@ -48,11 +52,6 @@ export function getReleaseChannel( return ElectronReleaseChannel.stable; } -export const enum VersionKeys { - local = 'local-electron-versions', - known = 'known-electron-versions', -} - export function makeRunnable(ver: Version): RunnableVersion { const ret: RunnableVersion = { ...ver, @@ -112,7 +111,7 @@ export function getLocalVersionForPath( * @returns {Array} */ export function getLocalVersions(): Array { - const fromLs = window.localStorage.getItem(VersionKeys.local); + const fromLs = window.localStorage.getItem(GlobalSetting.localVersion); if (fromLs) { try { @@ -148,12 +147,12 @@ export function saveLocalVersions( }); const stringified = JSON.stringify(filteredVersions); - window.localStorage.setItem(VersionKeys.local, stringified); + window.localStorage.setItem(GlobalSetting.localVersion, stringified); } function getReleasedVersions(): Array { const versions = window.ElectronFiddle.getReleasedVersions(); - const fromLs = window.localStorage.getItem(VersionKeys.known); + const fromLs = window.localStorage.getItem(GlobalSetting.knownVersion); if (fromLs) { try { @@ -181,7 +180,7 @@ export async function fetchVersions(): Promise { // Migrate away from known versions being stored in localStorage // Now that we've fetched new versions, it's safe to delete - window.localStorage.removeItem(VersionKeys.known); + window.localStorage.removeItem(GlobalSetting.knownVersion); console.log(`Fetched ${versions.length} new Electron versions`); return versions; diff --git a/tests/renderer/versions-spec.ts b/tests/renderer/versions-spec.ts index 53e710ae6a..787e5169d3 100644 --- a/tests/renderer/versions-spec.ts +++ b/tests/renderer/versions-spec.ts @@ -1,10 +1,10 @@ import { ElectronReleaseChannel, + GlobalSetting, RunnableVersion, VersionSource, } from '../../src/interfaces'; import { - VersionKeys, addLocalVersion, fetchVersions, getDefaultVersion, @@ -109,7 +109,7 @@ describe('versions', () => { saveLocalVersions(mockLocalVersions as Array); expect(window.localStorage.setItem).toBeCalledWith( - VersionKeys.local, + GlobalSetting.localVersion, JSON.stringify(mockLocalVersions), ); }); @@ -152,7 +152,9 @@ describe('versions', () => { it('removes knownVersions from localStorage', async () => { (window.ElectronFiddle.fetchVersions as jest.Mock).mockResolvedValue([]); await fetchVersions(); - expect(localStorage.removeItem).toHaveBeenCalledWith(VersionKeys.known); + expect(localStorage.removeItem).toHaveBeenCalledWith( + GlobalSetting.knownVersion, + ); }); }); });