@@ -99,13 +99,21 @@ const FALLBACK_STRINGS: Dictionary = {
9999
100100// Safely retrieve the storage area (Sync if available, otherwise Local)
101101function getStorage ( ) : chrome . storage . SyncStorageArea | chrome . storage . LocalStorageArea {
102- return chrome ?. storage ?. sync || chrome ?. storage ?. local
102+ const sync = chrome ?. storage ?. sync ;
103+ const local = chrome ?. storage ?. local ;
104+ if ( sync ) return sync ;
105+ if ( local ) return local ;
106+ throw new Error ( 'Neither chrome.storage.sync nor chrome.storage.local is available in this environment.' ) ;
103107}
104108
105109// Heavier cache reads (like the large translation map) should come from 'local' storage
106110// when available to avoid hitting 'sync' storage quotas and latency.
107111function getCacheStorage ( ) : chrome . storage . LocalStorageArea | chrome . storage . SyncStorageArea {
108- return chrome ?. storage ?. local || chrome ?. storage ?. sync
112+ const local = chrome ?. storage ?. local ;
113+ const sync = chrome ?. storage ?. sync ;
114+ if ( local ) return local ;
115+ if ( sync ) return sync ;
116+ throw new Error ( 'Neither chrome.storage.local nor chrome.storage.sync is available in this environment.' ) ;
109117}
110118
111119// Get a localized string for the Options UI (fallback to English)
@@ -139,30 +147,31 @@ function normalizeSettings(raw: Partial<Settings>): Settings {
139147// STATE MANAGEMENT
140148// ---------------------------------------------------------------------------
141149
142- // Tracks the last rendered language to decide when a full re-render is needed
143- let lastRenderedLanguage : LanguageCode | null = null
144- // Holds the current application state
145- let currentSettings : Settings = { ...DEFAULT_SETTINGS }
146- // Latest locale source metadata (per language).
147- // This tells us if we are using the bundled JSON or a fresher version from CDN.
148- let latestLocaleMeta : Record < Exclude < LanguageCode , 'off' > , LocaleMeta > | null = null
149- // Track if a manual refresh is in progress to prevent UI flickering
150- let isManuallyRefreshing = false
150+ // Centralized state object for the options page.
151+ const appState = {
152+ lastRenderedLanguage : null as LanguageCode | null ,
153+ currentSettings : { ...DEFAULT_SETTINGS } as Settings ,
154+ // Latest locale source metadata (per language).
155+ // This tells us if we are using the bundled JSON or a fresher version from CDN.
156+ latestLocaleMeta : null as Record < Exclude < LanguageCode , 'off' > , LocaleMeta > | null ,
157+ // Track if a manual refresh is in progress to prevent UI flickering
158+ isManuallyRefreshing : false
159+ }
151160
152161// ---------------------------------------------------------------------------
153162// DOM RENDERING
154163// ---------------------------------------------------------------------------
155164
156165// Main render: full re-render on language change, otherwise just sync values
157166function renderApp ( settings : Settings ) {
158- currentSettings = settings
167+ appState . currentSettings = settings
159168 const root = document . getElementById ( 'root' )
160169 if ( ! root ) return
161170
162171 // Full re-render needed if language changed (to update UI text).
163172 // We must re-bind events because the DOM nodes are replaced.
164- if ( lastRenderedLanguage !== settings . language ) {
165- lastRenderedLanguage = settings . language
173+ if ( appState . lastRenderedLanguage !== settings . language ) {
174+ appState . lastRenderedLanguage = settings . language
166175 renderFullPage ( root , settings )
167176 bindEvents ( root )
168177 }
@@ -173,8 +182,8 @@ function renderApp(settings: Settings) {
173182 // Only update badge if not in the middle of a manual refresh result.
174183 // This prevents the "Done" message from being immediately overwritten by "Bundled"
175184 // before the user has a chance to see it.
176- if ( ! isManuallyRefreshing ) {
177- updateLocaleBadge ( root , latestLocaleMeta , settings )
185+ if ( ! appState . isManuallyRefreshing ) {
186+ updateLocaleBadge ( root , appState . latestLocaleMeta , settings )
178187 }
179188}
180189
@@ -422,12 +431,12 @@ function bindEvents(root: HTMLElement) {
422431 // Optimistic UI update: Toggle the switch visually immediately.
423432 // The actual storage save happens asynchronously below.
424433 if ( key === 'enabled' ) {
425- updateValues ( root , { ...currentSettings , enabled : val } )
434+ updateValues ( root , { ...appState . currentSettings , enabled : val } )
426435 }
427436
428437 // Save to storage
429438 storage . set ( { [ key ] : val } , ( ) => {
430- setStatusMsg ( root , getText ( currentSettings . language , 'options_saved_msg' ) )
439+ setStatusMsg ( root , getText ( appState . currentSettings . language , 'options_saved_msg' ) )
431440 } )
432441 } )
433442 } )
@@ -441,32 +450,32 @@ function bindEvents(root: HTMLElement) {
441450
442451 // Saving language triggers 'onChanged', which will call renderApp() and re-render the page.
443452 storage . set ( { language : val , enabled : true } , ( ) => {
444- setStatusMsg ( root , getText ( currentSettings . language , 'options_saved_msg' ) )
453+ setStatusMsg ( root , getText ( appState . currentSettings . language , 'options_saved_msg' ) )
445454 } )
446455
447456 // Updates internal state loosely until the re-render happens
448- currentSettings . language = val
449- currentSettings . enabled = true
457+ appState . currentSettings . language = val
458+ appState . currentSettings . enabled = true
450459 }
451460 } )
452461
453462 // Badge click handler (Force Refresh)
454463 const badge = root . querySelector ( '#cdn_json_badge' )
455464 badge ?. addEventListener ( 'click' , ( ) => {
456465 // Only allow refresh if using CDN (checked via valid class or settings)
457- if ( ! currentSettings . useCdn ) return
466+ if ( ! appState . currentSettings . useCdn ) return
458467
459468 const el = badge as HTMLElement
460469 // 1. Set Loading Text
461- el . textContent = getText ( currentSettings . language , 'options_refreshing_msg' )
462- isManuallyRefreshing = true
470+ el . textContent = getText ( appState . currentSettings . language , 'options_refreshing_msg' )
471+ appState . isManuallyRefreshing = true
463472
464473 chrome . storage . local . remove ( LOCALE_CACHE_KEY , ( ) => {
465474 // 2. Set Done Text on completion
466475 // We keep isManuallyRefreshing = true so onChanged doesn't overwrite this with "Bundled"
467476 // It stays true so this message persists until the user reloads the Page
468477 // or until a NEW cache entry appears (which happens when they visit Webflow).
469- el . textContent = getText ( currentSettings . language , 'options_refresh_done_msg' )
478+ el . textContent = getText ( appState . currentSettings . language , 'options_refresh_done_msg' )
470479 } )
471480 } )
472481
@@ -501,7 +510,7 @@ function bindEvents(root: HTMLElement) {
501510 setTimeout ( ( ) => status . classList . remove ( 'visible' ) , 2000 )
502511 }
503512 // Update local state
504- currentSettings . exclusionSelectors = lines
513+ appState . currentSettings . exclusionSelectors = lines
505514 } )
506515 } )
507516 }
@@ -516,8 +525,16 @@ function bindEvents(root: HTMLElement) {
516525 let resetConfirming = false
517526 let resetConfirmTimeout : ReturnType < typeof setTimeout > | undefined
518527
519- const defaultResetLabel = getText ( currentSettings . language , 'options_advanced_reset' )
520- const confirmResetLabel = getText ( currentSettings . language , 'options_advanced_reset_confirm' )
528+ // Clean up the timeout if the page is unloaded, to prevent memory leaks
529+ window . addEventListener ( 'beforeunload' , ( ) => {
530+ if ( resetConfirmTimeout ) {
531+ clearTimeout ( resetConfirmTimeout )
532+ resetConfirmTimeout = undefined
533+ }
534+ } ) ;
535+
536+ const defaultResetLabel = getText ( appState . currentSettings . language , 'options_advanced_reset' )
537+ const confirmResetLabel = getText ( appState . currentSettings . language , 'options_advanced_reset_confirm' )
521538
522539 const setResetState = ( confirming : boolean ) => {
523540 resetConfirming = confirming
@@ -542,7 +559,7 @@ function bindEvents(root: HTMLElement) {
542559 const status = root . querySelector ( '.save_status' ) as HTMLElement
543560
544561 const defaults = getDefaultExclusionSelectors ( )
545- const updatedSettings = { ...currentSettings , exclusionSelectors : defaults }
562+ const updatedSettings = { ...appState . currentSettings , exclusionSelectors : defaults }
546563
547564 // Update UI immediately
548565 if ( textarea ) textarea . value = defaults . join ( '\n' )
@@ -572,7 +589,7 @@ function setStatusMsg(root: HTMLElement, msg: string) {
572589 setTimeout ( ( ) => {
573590 // Revert to idle message if still in 'changed' state
574591 if ( el . dataset . status === 'changed' ) {
575- el . textContent = getText ( currentSettings . language , 'options_status_idle' )
592+ el . textContent = getText ( appState . currentSettings . language , 'options_status_idle' )
576593 el . dataset . status = 'idle'
577594 }
578595 } , 2000 )
@@ -589,9 +606,9 @@ export default function initOptionsPage() {
589606
590607 // 1. Initial Load: Get settings and cache meta from storage.
591608 // We fetch both user settings (sync/local) and the translation cache metadata (local).
592- storage . get ( { ... DEFAULT_SETTINGS , exclusionSelectors : getDefaultExclusionSelectors ( ) } , ( items ) => {
609+ storage . get ( Object . assign ( { } , DEFAULT_SETTINGS , { exclusionSelectors : getDefaultExclusionSelectors ( ) } ) , ( items ) => {
593610 cacheStorage . get ( { [ LOCALE_CACHE_KEY ] : null } , ( cacheItems : { [ LOCALE_CACHE_KEY ] : Record < Exclude < LanguageCode , 'off' > , { source ?: string ; fetchedAt ?: number } > | null } ) => {
594- latestLocaleMeta = extractLocaleMeta ( cacheItems [ LOCALE_CACHE_KEY ] )
611+ appState . latestLocaleMeta = extractLocaleMeta ( cacheItems [ LOCALE_CACHE_KEY ] )
595612 // Merge defaults with loaded items to ensure complete object
596613 const settings = normalizeSettings ( { ...DEFAULT_SETTINGS , ...items } )
597614 renderApp ( settings )
@@ -602,7 +619,7 @@ export default function initOptionsPage() {
602619 chrome . storage . onChanged . addListener ( ( changes , area ) => {
603620 if ( area !== 'sync' && area !== 'local' ) return
604621
605- const newSettings = { ...currentSettings }
622+ const newSettings = { ...appState . currentSettings }
606623 let hasChange = false
607624
608625 // Update settings object with any changed values
@@ -637,17 +654,17 @@ export default function initOptionsPage() {
637654 // The options page listens for this to update the "JSON: Cloudflare" badge.
638655 if ( changes [ LOCALE_CACHE_KEY ] ) {
639656 const newValue = changes [ LOCALE_CACHE_KEY ] . newValue
640- latestLocaleMeta = extractLocaleMeta ( newValue )
657+ appState . latestLocaleMeta = extractLocaleMeta ( newValue )
641658
642659 // If we receive a NEW valid cache, we can clear the manual refresh state
643660 // and show the new source (e.g. Cloudflare).
644661 if ( newValue && Object . keys ( newValue ) . length > 0 ) {
645- isManuallyRefreshing = false
662+ appState . isManuallyRefreshing = false
646663 }
647664
648665 const root = document . getElementById ( 'root' )
649- if ( root && ! isManuallyRefreshing ) {
650- updateLocaleBadge ( root , latestLocaleMeta , newSettings )
666+ if ( root && ! appState . isManuallyRefreshing ) {
667+ updateLocaleBadge ( root , appState . latestLocaleMeta , newSettings )
651668 }
652669 }
653670
0 commit comments