mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-08 20:19:06 +03:00
save popup state
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user