save popup state

This commit is contained in:
2026-02-09 15:49:03 +03:00
parent 16d4270442
commit 46976fc696
3 changed files with 152 additions and 11 deletions

View File

@@ -1,5 +1,7 @@
import { StorageService } from './services/StorageService.js'; import { StorageService } from './services/StorageService.js';
const POPUP_STATE_KEY = 'goose-popup-ui-state';
class BackgroundService { class BackgroundService {
constructor() { constructor() {
this.initialize(); this.initialize();
@@ -8,6 +10,17 @@ class BackgroundService {
private initialize(): void { private initialize(): void {
this.setupTabUpdateListener(); this.setupTabUpdateListener();
this.setupInstallListener(); this.setupInstallListener();
this.setupPopupStateListener();
}
private setupPopupStateListener(): void {
chrome.runtime.onMessage.addListener((msg: { type?: string; payload?: unknown }, _sender, sendResponse) => {
if (msg.type === 'SAVE_POPUP_STATE' && msg.payload !== undefined) {
chrome.storage.local.set({ [POPUP_STATE_KEY]: JSON.stringify(msg.payload) }).then(() => sendResponse(undefined)).catch(() => sendResponse(undefined));
return true;
}
return false;
});
} }
private setupTabUpdateListener(): void { private setupTabUpdateListener(): void {

View File

@@ -22,15 +22,21 @@ export class PopupController {
private wordMenuOpenForIndex: number | null = null; private wordMenuOpenForIndex: number | null = null;
private wordMenuCopyOnly = false; private wordMenuCopyOnly = false;
private wordMenuCloseListener: (() => void) | null = null; private wordMenuCloseListener: (() => void) | null = null;
private periodicSaveInterval: ReturnType<typeof setInterval> | null = null;
async initialize(): Promise<void> { async initialize(): Promise<void> {
await this.loadData(); await this.loadData();
await this.loadPopupState();
await this.getCurrentTab(); await this.getCurrentTab();
this.loadActiveTab();
this.translateTitles(); this.translateTitles();
this.setupEventListeners(); this.setupEventListeners();
this.render(); this.render();
this.restoreWordSearchInput();
requestAnimationFrame(() => {
requestAnimationFrame(() => this.restoreScrollPositions());
});
this.hideLoadingOverlay(); this.hideLoadingOverlay();
this.startPeriodicSave();
} }
private hideLoadingOverlay(): void { private hideLoadingOverlay(): void {
@@ -73,10 +79,114 @@ export class PopupController {
} }
} }
private loadActiveTab(): void { private static readonly POPUP_STATE_KEY = 'goose-popup-ui-state';
const saved = localStorage.getItem('goose-highlighter-active-tab'); private scrollPositions: Record<string, number> = {};
if (saved && saved !== 'options') {
this.activeTab = saved; private async loadPopupState(): Promise<void> {
try {
const result = await chrome.storage.local.get(PopupController.POPUP_STATE_KEY);
const raw = result[PopupController.POPUP_STATE_KEY];
if (raw === undefined || typeof raw !== 'string') return;
const state = JSON.parse(raw) as {
activeTab?: string;
currentListIndex?: number;
wordSearchQuery?: string;
currentPage?: number;
scrollPositions?: Record<string, number>;
};
if (typeof state.activeTab === 'string' && state.activeTab !== 'options') {
this.activeTab = state.activeTab;
}
if (typeof state.currentListIndex === 'number' && state.currentListIndex >= 0) {
this.currentListIndex = Math.min(state.currentListIndex, Math.max(0, this.lists.length - 1));
}
if (typeof state.wordSearchQuery === 'string') {
this.wordSearchQuery = state.wordSearchQuery;
}
if (typeof state.currentPage === 'number' && state.currentPage >= 1) {
this.currentPage = state.currentPage;
}
if (state.scrollPositions && typeof state.scrollPositions === 'object') {
this.scrollPositions = { ...state.scrollPositions };
}
} catch {
// keep defaults
}
}
private getPopupStatePayload(): { activeTab: string; currentListIndex: number; wordSearchQuery: string; currentPage: number; scrollPositions: Record<string, number> } {
return {
activeTab: this.activeTab,
currentListIndex: this.currentListIndex,
wordSearchQuery: this.wordSearchQuery,
currentPage: this.currentPage,
scrollPositions: this.scrollPositions
};
}
private savePopupState(): void {
chrome.storage.local.set({ [PopupController.POPUP_STATE_KEY]: JSON.stringify(this.getPopupStatePayload()) }).catch(() => {});
}
private startPeriodicSave(): void {
this.periodicSaveInterval = setInterval(() => {
const scrollEl = this.getScrollContainer(this.activeTab);
if (scrollEl) this.scrollPositions[this.activeTab] = scrollEl.scrollTop;
this.savePopupState();
}, 800);
}
captureScrollAndSave(): void {
if (this.periodicSaveInterval) {
clearInterval(this.periodicSaveInterval);
this.periodicSaveInterval = null;
}
const scrollEl = this.getScrollContainer(this.activeTab);
if (scrollEl) this.scrollPositions[this.activeTab] = scrollEl.scrollTop;
chrome.runtime.sendMessage({ type: 'SAVE_POPUP_STATE', payload: this.getPopupStatePayload() }).catch(() => {});
}
private restoreWordSearchInput(): void {
const wordSearch = document.getElementById('wordSearch') as HTMLInputElement;
if (wordSearch) {
wordSearch.value = this.wordSearchQuery;
}
}
private static readonly SCROLL_SELECTORS: Record<string, string> = {
lists: '.tab-inner',
words: '.word-list-container',
'page-highlights': '.page-highlights-list',
exceptions: '.exceptions-list'
};
private getScrollContainer(tabName: string): HTMLElement | null {
const sel = PopupController.SCROLL_SELECTORS[tabName];
if (!sel) return null;
const content = document.querySelector(`.tab-content[data-tab-content="${tabName}"]`);
return content?.querySelector(sel) ?? null;
}
private setupScrollListeners(): void {
const tabNames = ['lists', 'words', 'page-highlights', 'exceptions'];
tabNames.forEach(tabName => {
const el = this.getScrollContainer(tabName);
if (el) {
el.addEventListener('scroll', () => {
this.scrollPositions[tabName] = el.scrollTop;
this.savePopupState();
}, { passive: true });
}
});
}
private restoreScrollPositions(): void {
const el = this.getScrollContainer(this.activeTab);
if (el) {
const saved = this.scrollPositions[this.activeTab];
if (typeof saved === 'number' && saved >= 0) {
el.scrollTop = saved;
}
} }
} }
@@ -92,13 +202,16 @@ export class PopupController {
}); });
} }
private saveActiveTab(): void {
localStorage.setItem('goose-highlighter-active-tab', this.activeTab);
}
private switchTab(tabName: string): void { private switchTab(tabName: string): void {
const isUserSwitch = tabName !== this.activeTab;
if (isUserSwitch) {
const scrollEl = this.getScrollContainer(this.activeTab);
if (scrollEl) {
this.scrollPositions[this.activeTab] = scrollEl.scrollTop;
}
this.activeTab = tabName; this.activeTab = tabName;
this.saveActiveTab(); this.savePopupState();
}
document.querySelectorAll('.tab-button').forEach(btn => { document.querySelectorAll('.tab-button').forEach(btn => {
btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName); btn.classList.toggle('active', btn.getAttribute('data-tab') === tabName);
@@ -111,10 +224,12 @@ export class PopupController {
if (tabName === 'page-highlights') { if (tabName === 'page-highlights') {
this.loadPageHighlights(); this.loadPageHighlights();
} }
requestAnimationFrame(() => this.restoreScrollPositions());
} }
private setupEventListeners(): void { private setupEventListeners(): void {
this.setupTabs(); this.setupTabs();
this.setupScrollListeners();
this.setupSettingsOverlay(); this.setupSettingsOverlay();
this.setupListManagement(); this.setupListManagement();
this.setupWordManagement(); this.setupWordManagement();
@@ -197,6 +312,7 @@ export class PopupController {
words: [] words: []
}); });
this.currentListIndex = this.lists.length - 1; this.currentListIndex = this.lists.length - 1;
this.savePopupState();
this.save(); this.save();
}); });
@@ -209,6 +325,7 @@ export class PopupController {
if (confirm(chrome.i18n.getMessage('confirm_delete_list') || 'Delete this list?')) { if (confirm(chrome.i18n.getMessage('confirm_delete_list') || 'Delete this list?')) {
this.lists.splice(this.currentListIndex, 1); this.lists.splice(this.currentListIndex, 1);
this.currentListIndex = Math.max(0, this.currentListIndex - 1); this.currentListIndex = Math.max(0, this.currentListIndex - 1);
this.savePopupState();
this.save(); this.save();
} }
}); });
@@ -269,6 +386,7 @@ export class PopupController {
wordSearch.addEventListener('input', (e) => { wordSearch.addEventListener('input', (e) => {
this.wordSearchQuery = (e.target as HTMLInputElement).value; this.wordSearchQuery = (e.target as HTMLInputElement).value;
this.currentPage = 1; this.currentPage = 1;
this.savePopupState();
this.renderWords(); this.renderWords();
}); });
} }
@@ -805,6 +923,9 @@ export class PopupController {
</div> </div>
`; `;
}).join(''); }).join('');
if (this.activeTab === 'page-highlights') {
requestAnimationFrame(() => this.restoreScrollPositions());
}
} }
private setupExceptions(): void { private setupExceptions(): void {
@@ -1064,6 +1185,7 @@ export class PopupController {
this.selectedCheckboxes.clear(); this.selectedCheckboxes.clear();
this.currentListIndex = index; this.currentListIndex = index;
this.currentPage = 1; this.currentPage = 1;
this.savePopupState();
this.renderWords(); this.renderWords();
this.updateListForm(); this.updateListForm();
this.renderLists(); this.renderLists();
@@ -1117,6 +1239,7 @@ export class PopupController {
const totalPages = Math.ceil(this.totalWords / this.pageSize); const totalPages = Math.ceil(this.totalWords / this.pageSize);
if (this.currentPage > totalPages) { if (this.currentPage > totalPages) {
this.currentPage = Math.max(1, totalPages); this.currentPage = Math.max(1, totalPages);
this.savePopupState();
} }
const startIndex = (this.currentPage - 1) * this.pageSize; const startIndex = (this.currentPage - 1) * this.pageSize;
const endIndex = Math.min(startIndex + this.pageSize, this.totalWords); const endIndex = Math.min(startIndex + this.pageSize, this.totalWords);
@@ -1217,6 +1340,7 @@ export class PopupController {
if (page < 1 || page > totalPages) return; if (page < 1 || page > totalPages) return;
this.currentPage = page; this.currentPage = page;
this.savePopupState();
this.renderWords(); this.renderWords();
} }

View File

@@ -46,4 +46,8 @@ document.addEventListener('DOMContentLoaded', async () => {
displayVersion(); displayVersion();
const controller = new PopupController(); const controller = new PopupController();
await controller.initialize(); await controller.initialize();
const onClose = (): void => controller.captureScrollAndSave();
window.addEventListener('blur', onClose);
window.addEventListener('pagehide', onClose);
}); });