chore(ui): store selected projects in settings (#21737)
This commit is contained in:
		
							parent
							
								
									403a194ac7
								
							
						
					
					
						commit
						b0bda92f9e
					
				|  | @ -15,7 +15,6 @@ const TODO_ITEMS = [ | ||||||
|   'book a doctors appointment' |   'book a doctors appointment' | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| 
 |  | ||||||
| test.describe('New Todo', () => { | test.describe('New Todo', () => { | ||||||
|   test('should allow me to add todo items', async ({ page }) => { |   test('should allow me to add todo items', async ({ page }) => { | ||||||
|     // create a new todo locator
 |     // create a new todo locator
 | ||||||
|  |  | ||||||
|  | @ -16,6 +16,7 @@ | ||||||
| 
 | 
 | ||||||
| import fs from 'fs'; | import fs from 'fs'; | ||||||
| import path from 'path'; | import path from 'path'; | ||||||
|  | import { isUnderTest } from 'playwright-core/lib/utils'; | ||||||
| import type { Page } from '../page'; | import type { Page } from '../page'; | ||||||
| import { registryDirectory } from '../registry'; | import { registryDirectory } from '../registry'; | ||||||
| import type { CRPage } from './crPage'; | import type { CRPage } from './crPage'; | ||||||
|  | @ -29,23 +30,21 @@ export async function installAppIcon(page: Page) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export async function syncLocalStorageWithSettings(page: Page, appName: string) { | export async function syncLocalStorageWithSettings(page: Page, appName: string) { | ||||||
|  |   if (isUnderTest()) | ||||||
|  |     return; | ||||||
|   const settingsFile = path.join(registryDirectory, '.settings', `${appName}.json`); |   const settingsFile = path.join(registryDirectory, '.settings', `${appName}.json`); | ||||||
|   await page.exposeBinding('saveSettings', false, (_, settings: any) => { |   await page.exposeBinding('_saveSerializedSettings', false, (_, settings) => { | ||||||
|     fs.mkdirSync(path.dirname(settingsFile), { recursive: true }); |     fs.mkdirSync(path.dirname(settingsFile), { recursive: true }); | ||||||
|     fs.writeFileSync(settingsFile, settings); |     fs.writeFileSync(settingsFile, settings); | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   const settings = await fs.promises.readFile(settingsFile, 'utf-8').catch(() => ('{}')); |   const settings = await fs.promises.readFile(settingsFile, 'utf-8').catch(() => ('{}')); | ||||||
|   await page.addInitScript(`(${String((settings: any) => { |   await page.addInitScript( | ||||||
|  |       `(${String((settings: any) => { | ||||||
|         Object.entries(settings).map(([k, v]) => localStorage[k] = v); |         Object.entries(settings).map(([k, v]) => localStorage[k] = v); | ||||||
| 
 |         (window as any).saveSettings = () => { | ||||||
|     let lastValue = JSON.stringify(localStorage); |           (window as any)._saveSerializedSettings(JSON.stringify({ ...localStorage })); | ||||||
|     setInterval(() => { |         }; | ||||||
|       const value = JSON.stringify(localStorage); |       })})(${settings}); | ||||||
|       if (value !== lastValue) { |   `);
 | ||||||
|         lastValue = value; |  | ||||||
|         window.saveSettings(value); |  | ||||||
|       } |  | ||||||
|     }, 2000); |  | ||||||
|   })})(${settings})`);
 |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -20,7 +20,7 @@ import '@web/common.css'; | ||||||
| import React from 'react'; | import React from 'react'; | ||||||
| import { TreeView } from '@web/components/treeView'; | import { TreeView } from '@web/components/treeView'; | ||||||
| import type { TreeState } from '@web/components/treeView'; | import type { TreeState } from '@web/components/treeView'; | ||||||
| import { TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver'; | import { baseFullConfig, TeleReporterReceiver, TeleSuite } from '@testIsomorphic/teleReceiver'; | ||||||
| import type { TeleTestCase } from '@testIsomorphic/teleReceiver'; | import type { TeleTestCase } from '@testIsomorphic/teleReceiver'; | ||||||
| import type { FullConfig, Suite, TestCase, TestResult, Location } from '../../../playwright-test/types/testReporter'; | import type { FullConfig, Suite, TestCase, TestResult, Location } from '../../../playwright-test/types/testReporter'; | ||||||
| import { SplitView } from '@web/components/splitView'; | import { SplitView } from '@web/components/splitView'; | ||||||
|  | @ -34,8 +34,9 @@ import { XtermWrapper } from '@web/components/xtermWrapper'; | ||||||
| import { Expandable } from '@web/components/expandable'; | import { Expandable } from '@web/components/expandable'; | ||||||
| import { toggleTheme } from '@web/theme'; | import { toggleTheme } from '@web/theme'; | ||||||
| import { artifactsFolderName } from '@testIsomorphic/folders'; | import { artifactsFolderName } from '@testIsomorphic/folders'; | ||||||
|  | import { settings } from '@web/uiUtils'; | ||||||
| 
 | 
 | ||||||
| let updateRootSuite: (rootSuite: Suite, progress: Progress) => void = () => {}; | let updateRootSuite: (config: FullConfig, rootSuite: Suite, progress: Progress) => void = () => {}; | ||||||
| let runWatchedTests = (fileName: string) => {}; | let runWatchedTests = (fileName: string) => {}; | ||||||
| let xtermSize = { cols: 80, rows: 24 }; | let xtermSize = { cols: 80, rows: 24 }; | ||||||
| 
 | 
 | ||||||
|  | @ -49,6 +50,11 @@ const xtermDataSource: XtermDataSource = { | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | type TestModel = { | ||||||
|  |   config: FullConfig | undefined; | ||||||
|  |   rootSuite: Suite | undefined; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
| export const WatchModeView: React.FC<{}> = ({ | export const WatchModeView: React.FC<{}> = ({ | ||||||
| }) => { | }) => { | ||||||
|   const [filterText, setFilterText] = React.useState<string>(''); |   const [filterText, setFilterText] = React.useState<string>(''); | ||||||
|  | @ -60,7 +66,7 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
|     ['skipped', false], |     ['skipped', false], | ||||||
|   ])); |   ])); | ||||||
|   const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map()); |   const [projectFilters, setProjectFilters] = React.useState<Map<string, boolean>>(new Map()); | ||||||
|   const [rootSuite, setRootSuite] = React.useState<{ value: Suite | undefined }>({ value: undefined }); |   const [testModel, setTestModel] = React.useState<TestModel>({ config: undefined, rootSuite: undefined }); | ||||||
|   const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 }); |   const [progress, setProgress] = React.useState<Progress>({ total: 0, passed: 0, failed: 0, skipped: 0 }); | ||||||
|   const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined); |   const [selectedTest, setSelectedTest] = React.useState<TestCase | undefined>(undefined); | ||||||
|   const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]); |   const [visibleTestIds, setVisibleTestIds] = React.useState<string[]>([]); | ||||||
|  | @ -71,7 +77,7 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
| 
 | 
 | ||||||
|   const reloadTests = () => { |   const reloadTests = () => { | ||||||
|     setIsLoading(true); |     setIsLoading(true); | ||||||
|     updateRootSuite(new TeleSuite('', 'root'), { total: 0, passed: 0, failed: 0, skipped: 0 }); |     updateRootSuite(baseFullConfig, new TeleSuite('', 'root'), { total: 0, passed: 0, failed: 0, skipped: 0 }); | ||||||
|     refreshRootSuite(true).then(() => { |     refreshRootSuite(true).then(() => { | ||||||
|       setIsLoading(false); |       setIsLoading(false); | ||||||
|     }); |     }); | ||||||
|  | @ -82,19 +88,20 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
|     reloadTests(); |     reloadTests(); | ||||||
|   }, []); |   }, []); | ||||||
| 
 | 
 | ||||||
|   updateRootSuite = (rootSuite: Suite, newProgress: Progress) => { |   updateRootSuite = (config: FullConfig, rootSuite: Suite, newProgress: Progress) => { | ||||||
|  |     const selectedProjects = config.configFile ? settings.getObject<string[] | undefined>(config.configFile + ':projects', undefined) : undefined; | ||||||
|     for (const projectName of projectFilters.keys()) { |     for (const projectName of projectFilters.keys()) { | ||||||
|       if (!rootSuite.suites.find(s => s.title === projectName)) |       if (!rootSuite.suites.find(s => s.title === projectName)) | ||||||
|         projectFilters.delete(projectName); |         projectFilters.delete(projectName); | ||||||
|     } |     } | ||||||
|     for (const projectSuite of rootSuite.suites) { |     for (const projectSuite of rootSuite.suites) { | ||||||
|       if (!projectFilters.has(projectSuite.title)) |       if (!projectFilters.has(projectSuite.title)) | ||||||
|         projectFilters.set(projectSuite.title, false); |         projectFilters.set(projectSuite.title, !!selectedProjects?.includes(projectSuite.title)); | ||||||
|     } |     } | ||||||
|     if (projectFilters.size && ![...projectFilters.values()].includes(true)) |     if (!selectedProjects && projectFilters.size && ![...projectFilters.values()].includes(true)) | ||||||
|       projectFilters.set(projectFilters.entries().next().value[0], true); |       projectFilters.set(projectFilters.entries().next().value[0], true); | ||||||
| 
 | 
 | ||||||
|     setRootSuite({ value: rootSuite }); |     setTestModel({ config, rootSuite }); | ||||||
|     setProjectFilters(new Map(projectFilters)); |     setProjectFilters(new Map(projectFilters)); | ||||||
|     setProgress(newProgress); |     setProgress(newProgress); | ||||||
|   }; |   }; | ||||||
|  | @ -103,11 +110,11 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
|     // Clear test results.
 |     // Clear test results.
 | ||||||
|     { |     { | ||||||
|       const testIdSet = new Set(testIds); |       const testIdSet = new Set(testIds); | ||||||
|       for (const test of rootSuite.value?.allTests() || []) { |       for (const test of testModel.rootSuite?.allTests() || []) { | ||||||
|         if (testIdSet.has(test.id)) |         if (testIdSet.has(test.id)) | ||||||
|           (test as TeleTestCase)._createTestResult('pending'); |           (test as TeleTestCase)._createTestResult('pending'); | ||||||
|       } |       } | ||||||
|       setRootSuite({ ...rootSuite }); |       setTestModel({ ...testModel }); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     const time = '  [' + new Date().toLocaleTimeString() + ']'; |     const time = '  [' + new Date().toLocaleTimeString() + ']'; | ||||||
|  | @ -154,6 +161,7 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
|           setStatusFilters={setStatusFilters} |           setStatusFilters={setStatusFilters} | ||||||
|           projectFilters={projectFilters} |           projectFilters={projectFilters} | ||||||
|           setProjectFilters={setProjectFilters} |           setProjectFilters={setProjectFilters} | ||||||
|  |           testModel={testModel} | ||||||
|           runTests={() => runTests(visibleTestIds)} /> |           runTests={() => runTests(visibleTestIds)} /> | ||||||
|         <Toolbar> |         <Toolbar> | ||||||
|           <div className='section-title'>Tests</div> |           <div className='section-title'>Tests</div> | ||||||
|  | @ -165,7 +173,7 @@ export const WatchModeView: React.FC<{}> = ({ | ||||||
|           statusFilters={statusFilters} |           statusFilters={statusFilters} | ||||||
|           projectFilters={projectFilters} |           projectFilters={projectFilters} | ||||||
|           filterText={filterText} |           filterText={filterText} | ||||||
|           rootSuite={rootSuite} |           testModel={testModel} | ||||||
|           runningState={runningState} |           runningState={runningState} | ||||||
|           runTests={runTests} |           runTests={runTests} | ||||||
|           onTestSelected={setSelectedTest} |           onTestSelected={setSelectedTest} | ||||||
|  | @ -191,8 +199,9 @@ const FiltersView: React.FC<{ | ||||||
|   setStatusFilters: (filters: Map<string, boolean>) => void; |   setStatusFilters: (filters: Map<string, boolean>) => void; | ||||||
|   projectFilters: Map<string, boolean>; |   projectFilters: Map<string, boolean>; | ||||||
|   setProjectFilters: (filters: Map<string, boolean>) => void; |   setProjectFilters: (filters: Map<string, boolean>) => void; | ||||||
|  |   testModel: TestModel | undefined, | ||||||
|   runTests: () => void; |   runTests: () => void; | ||||||
| }> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, runTests }) => { | }> = ({ filterText, setFilterText, statusFilters, setStatusFilters, projectFilters, setProjectFilters, testModel, runTests }) => { | ||||||
|   const [expanded, setExpanded] = React.useState(false); |   const [expanded, setExpanded] = React.useState(false); | ||||||
|   const inputRef = React.useRef<HTMLInputElement>(null); |   const inputRef = React.useRef<HTMLInputElement>(null); | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|  | @ -235,6 +244,9 @@ const FiltersView: React.FC<{ | ||||||
|               const copy = new Map(projectFilters); |               const copy = new Map(projectFilters); | ||||||
|               copy.set(projectName, !copy.get(projectName)); |               copy.set(projectName, !copy.get(projectName)); | ||||||
|               setProjectFilters(copy); |               setProjectFilters(copy); | ||||||
|  |               const configFile = testModel?.config?.configFile; | ||||||
|  |               if (configFile) | ||||||
|  |                 settings.setObject(configFile + ':projects', [...copy.entries()].filter(([_, v]) => v).map(([k]) => k)); | ||||||
|             }}/> |             }}/> | ||||||
|             <div>{projectName}</div> |             <div>{projectName}</div> | ||||||
|           </label> |           </label> | ||||||
|  | @ -254,18 +266,18 @@ const TestList: React.FC<{ | ||||||
|   statusFilters: Map<string, boolean>, |   statusFilters: Map<string, boolean>, | ||||||
|   projectFilters: Map<string, boolean>, |   projectFilters: Map<string, boolean>, | ||||||
|   filterText: string, |   filterText: string, | ||||||
|   rootSuite: { value: Suite | undefined }, |   testModel: { rootSuite: Suite | undefined, config: FullConfig | undefined }, | ||||||
|   runTests: (testIds: string[]) => void, |   runTests: (testIds: string[]) => void, | ||||||
|   runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean }, |   runningState?: { testIds: Set<string>, itemSelectedByUser?: boolean }, | ||||||
|   setVisibleTestIds: (testIds: string[]) => void, |   setVisibleTestIds: (testIds: string[]) => void, | ||||||
|   onTestSelected: (test: TestCase | undefined) => void, |   onTestSelected: (test: TestCase | undefined) => void, | ||||||
| }> = ({ statusFilters, projectFilters, filterText, rootSuite, runTests, runningState, onTestSelected, setVisibleTestIds }) => { | }> = ({ statusFilters, projectFilters, filterText, testModel, runTests, runningState, onTestSelected, setVisibleTestIds }) => { | ||||||
|   const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() }); |   const [treeState, setTreeState] = React.useState<TreeState>({ expandedItems: new Map() }); | ||||||
|   const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>(); |   const [selectedTreeItemId, setSelectedTreeItemId] = React.useState<string | undefined>(); | ||||||
|   const [watchedTreeIds] = React.useState<Set<string>>(new Set()); |   const [watchedTreeIds] = React.useState<Set<string>>(new Set()); | ||||||
| 
 | 
 | ||||||
|   const { rootItem, treeItemMap } = React.useMemo(() => { |   const { rootItem, treeItemMap } = React.useMemo(() => { | ||||||
|     const rootItem = createTree(rootSuite.value, projectFilters); |     const rootItem = createTree(testModel.rootSuite, projectFilters); | ||||||
|     filterTree(rootItem, filterText, statusFilters); |     filterTree(rootItem, filterText, statusFilters); | ||||||
|     hideOnlyTests(rootItem); |     hideOnlyTests(rootItem); | ||||||
|     const treeItemMap = new Map<string, TreeItem>(); |     const treeItemMap = new Map<string, TreeItem>(); | ||||||
|  | @ -279,7 +291,7 @@ const TestList: React.FC<{ | ||||||
|     visit(rootItem); |     visit(rootItem); | ||||||
|     setVisibleTestIds([...visibleTestIds]); |     setVisibleTestIds([...visibleTestIds]); | ||||||
|     return { rootItem, treeItemMap }; |     return { rootItem, treeItemMap }; | ||||||
|   }, [filterText, rootSuite, statusFilters, projectFilters, setVisibleTestIds]); |   }, [filterText, testModel, statusFilters, projectFilters, setVisibleTestIds]); | ||||||
| 
 | 
 | ||||||
|   React.useEffect(() => { |   React.useEffect(() => { | ||||||
|     // Look for a first failure within the run batch to select it.
 |     // Look for a first failure within the run batch to select it.
 | ||||||
|  | @ -439,15 +451,15 @@ declare global { | ||||||
| let receiver: TeleReporterReceiver | undefined; | let receiver: TeleReporterReceiver | undefined; | ||||||
| 
 | 
 | ||||||
| let throttleTimer: NodeJS.Timeout | undefined; | let throttleTimer: NodeJS.Timeout | undefined; | ||||||
| let throttleData: { rootSuite: Suite, progress: Progress } | undefined; | let throttleData: { config: FullConfig, rootSuite: Suite, progress: Progress } | undefined; | ||||||
| const throttledAction = () => { | const throttledAction = () => { | ||||||
|   clearTimeout(throttleTimer); |   clearTimeout(throttleTimer); | ||||||
|   throttleTimer = undefined; |   throttleTimer = undefined; | ||||||
|   updateRootSuite(throttleData!.rootSuite, throttleData!.progress); |   updateRootSuite(throttleData!.config, throttleData!.rootSuite, throttleData!.progress); | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| const throttleUpdateRootSuite = (rootSuite: Suite, progress: Progress, immediate = false) => { | const throttleUpdateRootSuite = (config: FullConfig, rootSuite: Suite, progress: Progress, immediate = false) => { | ||||||
|   throttleData = { rootSuite, progress }; |   throttleData = { config, rootSuite, progress }; | ||||||
|   if (immediate) |   if (immediate) | ||||||
|     throttledAction(); |     throttledAction(); | ||||||
|   else if (!throttleTimer) |   else if (!throttleTimer) | ||||||
|  | @ -465,23 +477,25 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => { | ||||||
|     failed: 0, |     failed: 0, | ||||||
|     skipped: 0, |     skipped: 0, | ||||||
|   }; |   }; | ||||||
|  |   let config: FullConfig; | ||||||
|   receiver = new TeleReporterReceiver({ |   receiver = new TeleReporterReceiver({ | ||||||
|     onBegin: (config: FullConfig, suite: Suite) => { |     onBegin: (c: FullConfig, suite: Suite) => { | ||||||
|       if (!rootSuite) |       if (!rootSuite) | ||||||
|         rootSuite = suite; |         rootSuite = suite; | ||||||
|  |       config = c; | ||||||
|       progress.total = suite.allTests().length; |       progress.total = suite.allTests().length; | ||||||
|       progress.passed = 0; |       progress.passed = 0; | ||||||
|       progress.failed = 0; |       progress.failed = 0; | ||||||
|       progress.skipped = 0; |       progress.skipped = 0; | ||||||
|       throttleUpdateRootSuite(rootSuite, progress, true); |       throttleUpdateRootSuite(config, rootSuite, progress, true); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     onEnd: () => { |     onEnd: () => { | ||||||
|       throttleUpdateRootSuite(rootSuite, progress, true); |       throttleUpdateRootSuite(config, rootSuite, progress, true); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     onTestBegin: () => { |     onTestBegin: () => { | ||||||
|       throttleUpdateRootSuite(rootSuite, progress); |       throttleUpdateRootSuite(config, rootSuite, progress); | ||||||
|     }, |     }, | ||||||
| 
 | 
 | ||||||
|     onTestEnd: (test: TestCase) => { |     onTestEnd: (test: TestCase) => { | ||||||
|  | @ -491,7 +505,7 @@ const refreshRootSuite = (eraseResults: boolean): Promise<void> => { | ||||||
|         ++progress.failed; |         ++progress.failed; | ||||||
|       else |       else | ||||||
|         ++progress.passed; |         ++progress.passed; | ||||||
|       throttleUpdateRootSuite(rootSuite, progress); |       throttleUpdateRootSuite(config, rootSuite, progress); | ||||||
|     }, |     }, | ||||||
|   }); |   }); | ||||||
|   return sendMessage('list', {}); |   return sendMessage('list', {}); | ||||||
|  |  | ||||||
|  | @ -14,6 +14,8 @@ | ||||||
|  * limitations under the License. |  * limitations under the License. | ||||||
|  */ |  */ | ||||||
| 
 | 
 | ||||||
|  | import { settings } from './uiUtils'; | ||||||
|  | 
 | ||||||
| export function applyTheme() { | export function applyTheme() { | ||||||
|   if ((document as any).playwrightThemeInitialized) |   if ((document as any).playwrightThemeInitialized) | ||||||
|     return; |     return; | ||||||
|  | @ -26,14 +28,14 @@ export function applyTheme() { | ||||||
|     document.body.classList.add('inactive'); |     document.body.classList.add('inactive'); | ||||||
|   }, false); |   }, false); | ||||||
| 
 | 
 | ||||||
|   const currentTheme = localStorage.getItem('theme'); |   const currentTheme = settings.getString('theme', 'light-mode'); | ||||||
|   const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); |   const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)'); | ||||||
|   if (currentTheme === 'dark-mode' || prefersDarkScheme.matches) |   if (currentTheme === 'dark-mode' || prefersDarkScheme.matches) | ||||||
|     document.body.classList.add('dark-mode'); |     document.body.classList.add('dark-mode'); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function toggleTheme() { | export function toggleTheme() { | ||||||
|   const oldTheme = localStorage.getItem('theme'); |   const oldTheme = settings.getString('theme', 'light-mode'); | ||||||
|   let newTheme: string; |   let newTheme: string; | ||||||
|   if (oldTheme === 'dark-mode') |   if (oldTheme === 'dark-mode') | ||||||
|     newTheme = 'light-mode'; |     newTheme = 'light-mode'; | ||||||
|  | @ -43,7 +45,7 @@ export function toggleTheme() { | ||||||
|   if (oldTheme) |   if (oldTheme) | ||||||
|     document.body.classList.remove(oldTheme); |     document.body.classList.remove(oldTheme); | ||||||
|   document.body.classList.add(newTheme); |   document.body.classList.add(newTheme); | ||||||
|   localStorage.setItem('theme', newTheme); |   settings.setString('theme', newTheme); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function isDarkTheme() { | export function isDarkTheme() { | ||||||
|  |  | ||||||
|  | @ -80,15 +80,41 @@ export function copy(text: string) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export function useSetting<S>(name: string, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] { | export function useSetting<S>(name: string, defaultValue: S): [S, React.Dispatch<React.SetStateAction<S>>] { | ||||||
|   const string = localStorage.getItem(name); |   const value = settings.getObject(name, defaultValue); | ||||||
|   let value = defaultValue; |  | ||||||
|   if (string !== null) |  | ||||||
|     value = JSON.parse(string); |  | ||||||
| 
 |  | ||||||
|   const [state, setState] = React.useState<S>(value); |   const [state, setState] = React.useState<S>(value); | ||||||
|   const setStateWrapper = (value: React.SetStateAction<S>) => { |   const setStateWrapper = (value: React.SetStateAction<S>) => { | ||||||
|     localStorage.setItem(name, JSON.stringify(value)); |     settings.setObject(name, value); | ||||||
|     setState(value); |     setState(value); | ||||||
|   }; |   }; | ||||||
|   return [state, setStateWrapper]; |   return [state, setStateWrapper]; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | export class Settings { | ||||||
|  |   getString(name: string, defaultValue: string): string { | ||||||
|  |     return localStorage[name] || defaultValue; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setString(name: string, value: string) { | ||||||
|  |     localStorage[name] = value; | ||||||
|  |     if ((window as any).saveSettings) | ||||||
|  |       (window as any).saveSettings(); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   getObject<T>(name: string, defaultValue: T): T { | ||||||
|  |     if (!localStorage[name]) | ||||||
|  |       return defaultValue; | ||||||
|  |     try { | ||||||
|  |       return JSON.parse(localStorage[name]); | ||||||
|  |     } catch { | ||||||
|  |       return defaultValue; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   setObject<T>(name: string, value: T) { | ||||||
|  |     localStorage[name] = JSON.stringify(value); | ||||||
|  |     if ((window as any).saveSettings) | ||||||
|  |       (window as any).saveSettings(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export const settings = new Settings(); | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue