diff --git a/list-manager/list-manager.css b/list-manager/list-manager.css new file mode 100644 index 0000000..dcb327d --- /dev/null +++ b/list-manager/list-manager.css @@ -0,0 +1,406 @@ +:root { + --bg-color: #12100e; + --text-color: #f4ede6; + --input-bg: #1a1714; + --input-border: #3a2e26; + --button-bg: #2b211b; + --button-hover: #3a2c24; + --button-text: #f7efe9; + --accent: #f2a865; + --accent-hover: #f7c38a; + --accent-text: #1b120b; + --danger: #f87171; + --success: #4ade80; + --shadow: 0 12px 24px rgba(0, 0, 0, 0.4); + --border-radius: 14px; + --panel-bg: #17130f; +} + +* { + box-sizing: border-box; +} + +body { + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + margin: 0; + background: radial-gradient(120% 120% at 10% 0%, rgba(242, 168, 101, 0.08) 0%, transparent 45%), + linear-gradient(180deg, var(--bg-color) 0%, #0f0d0b 100%); + color: var(--text-color); + min-width: 800px; + min-height: 600px; +} + +.app { + display: flex; + flex-direction: column; + height: 100vh; +} + +.topbar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 16px 20px; + background: linear-gradient(145deg, #1a1511 0%, #0f0b08 100%); + border-bottom: 1px solid var(--input-border); + box-shadow: var(--shadow); +} + +.title { + display: flex; + align-items: center; + gap: 12px; +} + +.title img { + height: 36px; + width: 36px; +} + +.name { + font-weight: 600; + font-size: 1.05rem; +} + +.subtitle { + font-size: 0.85rem; + opacity: 0.7; +} + +.topbar-actions { + display: flex; + gap: 10px; +} + +.layout { + display: grid; + grid-template-columns: 320px 1fr; + gap: 16px; + padding: 16px; + height: calc(100vh - 72px); +} + +.panel { + background: var(--panel-bg); + border: 1px solid var(--input-border); + border-radius: var(--border-radius); + padding: 14px; + display: flex; + flex-direction: column; + gap: 12px; + overflow: hidden; +} + +.panel-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.lists-panel .panel-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +.lists-panel .panel-header h2 { + margin-top: 0; +} + +.panel-header h2 { + margin: 0; + font-size: 1rem; +} + +.panel-actions { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.lists-panel .panel-actions { + justify-content: flex-start; + width: 100%; +} + +.lists-panel .panel-actions button { + padding: 6px 10px; + font-size: 0.8rem; +} + +.panel-actions.secondary { + margin-top: 0; + gap: 6px; +} + +.list-hint { + font-size: 0.8rem; + opacity: 0.7; +} + +.lists { + display: flex; + flex-direction: column; + gap: 8px; + overflow-y: auto; + padding-right: 4px; +} + +.list-item { + background: #1f1813; + border: 1px solid transparent; + border-radius: 12px; + padding: 10px 12px; + display: grid; + grid-template-columns: auto 1fr auto; + gap: 10px; + align-items: center; + cursor: pointer; +} + +.list-item.active { + border-color: var(--accent); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); +} + +.list-item.drag-over { + border-color: var(--accent-hover); +} + +.list-item input[type="checkbox"] { + width: 16px; + height: 16px; +} + +.list-meta { + display: flex; + flex-direction: column; + gap: 4px; +} + +.list-name { + font-weight: 600; +} + +.list-stats { + font-size: 0.75rem; + opacity: 0.7; +} + +.list-badge { + font-size: 0.75rem; + padding: 4px 8px; + border-radius: 999px; + border: 1px solid var(--input-border); + opacity: 0.8; +} + +.details-panel { + display: flex; + flex-direction: column; + gap: 12px; +} + +.list-settings { + display: grid; + gap: 12px; + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: end; +} + +.list-settings label { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 0.85rem; +} + +.list-settings input[type="text"] { + width: 100%; +} + +.colors { + display: flex; + gap: 12px; + grid-column: span 2; + align-items: end; +} + +.colors label { + flex: 1; +} + + +.word-controls { + display: flex; + flex-direction: column; + gap: 8px; +} + +.add-words { + display: flex; + gap: 10px; + align-items: stretch; +} + +.add-words textarea { + flex: 1; + min-height: 64px; + resize: vertical; + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 10px; + padding: 8px 10px; +} + +.add-words button { + min-width: 120px; +} + +.word-controls .row { + display: flex; + gap: 8px; + flex-wrap: wrap; +} + +.word-controls input[type="text"], +.word-controls select { + flex: 1; + min-width: 160px; +} + +.word-list { + overflow-y: auto; + border: 1px solid var(--input-border); + border-radius: 12px; + padding: 8px; + background: #1a1511; + display: flex; + flex-direction: column; + gap: 6px; +} + +.empty { + padding: 12px; + text-align: center; + opacity: 0.7; + font-size: 0.9rem; +} + +.word-item { + display: grid; + grid-template-columns: auto 1fr auto auto auto; + gap: 8px; + align-items: center; + padding: 6px 8px; + border-radius: 10px; + background: #201915; +} + +.word-item.disabled { + opacity: 0.5; +} + +.word-item input[type="text"] { + width: 100%; +} + +button { + border: none; + border-radius: 10px; + padding: 8px 10px; + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + transition: background 0.2s ease; + font-size: 0.85rem; +} + +button:hover { + background: var(--button-hover); +} + +button.primary { + background: var(--accent); + color: var(--accent-text); +} + +button.primary:hover { + background: var(--accent-hover); +} + +button.danger { + background: rgba(248, 113, 113, 0.15); + color: var(--danger); + border: 1px solid rgba(248, 113, 113, 0.4); +} + +button.ghost { + background: transparent; + border: 1px solid var(--input-border); +} + +input[type="text"], +select, +input[type="color"] { + background: var(--input-bg); + color: var(--text-color); + border: 1px solid var(--input-border); + border-radius: 10px; + padding: 6px 8px; +} + +input[type="color"] { + padding: 0; + height: 36px; + border-radius: 10px; + border: 1px solid var(--input-border); + background: #1f1813; + cursor: pointer; +} + +input[type="color"]::-webkit-color-swatch-wrapper { + padding: 6px; +} + +input[type="color"]::-webkit-color-swatch { + border: none; + border-radius: 8px; + box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.08); +} + +.colors input[type="color"] { + width: 100%; +} + +.word-item input[type="color"] { + width: 38px; + height: 28px; + border-radius: 8px; +} + +.word-item input[type="color"]::-webkit-color-swatch-wrapper { + padding: 4px; +} + +.word-item input[type="color"]::-webkit-color-swatch { + border-radius: 6px; +} + +.stats { + font-size: 0.85rem; + opacity: 0.7; +} + +@media (max-width: 920px) { + body { + min-width: 100%; + } + + .layout { + grid-template-columns: 1fr; + height: auto; + } +} diff --git a/list-manager/list-manager.html b/list-manager/list-manager.html new file mode 100644 index 0000000..99ef70a --- /dev/null +++ b/list-manager/list-manager.html @@ -0,0 +1,98 @@ + + + + + + + Goose Highlighter - List Manager + + + + + + +
+
+
+ Goose Highlighter +
+
Goose Highlighter
+
List Manager
+
+
+
+ + +
+
+ +
+
+
+

Lists

+
+ + + +
+
+
+ + +
+
Drag lists to reorder
+
+
+ +
+
+

Selected List

+
0 words
+
+
+ +
+ + +
+ +
+ +
+
+ + +
+
+ + + + + +
+
+ + + + +
+
+ +
+
+
+
+ + + + + diff --git a/manifest.json b/manifest.json index 724d002..bfa2136 100644 --- a/manifest.json +++ b/manifest.json @@ -5,9 +5,11 @@ "version": "1.11.0", "default_locale": "en", "permissions": [ + "contextMenus", "scripting", "storage", - "tabs" + "tabs", + "windows" ], "host_permissions": [ "" @@ -24,4 +26,4 @@ "48": "icons/icon48.png", "128": "icons/icon128.png" } -} \ No newline at end of file +} diff --git a/popup/popup.html b/popup/popup.html index 3eab21c..cccacf1 100644 --- a/popup/popup.html +++ b/popup/popup.html @@ -55,6 +55,9 @@ +
+ +
@@ -158,4 +161,4 @@ - \ No newline at end of file + diff --git a/src/background.ts b/src/background.ts index bc942e4..ed43f63 100644 --- a/src/background.ts +++ b/src/background.ts @@ -8,6 +8,7 @@ class BackgroundService { private initialize(): void { this.setupTabUpdateListener(); this.setupInstallListener(); + this.setupContextMenu(); } private setupTabUpdateListener(): void { @@ -29,8 +30,33 @@ class BackgroundService { if (!data.exceptionsList) { await StorageService.update('exceptionsList', []); } + chrome.contextMenus.removeAll(() => { + chrome.contextMenus.create({ + id: 'manage-lists', + title: 'Manage Lists', + contexts: ['action'] + }); + }); + }); + } + + private setupContextMenu(): void { + chrome.contextMenus.onClicked.addListener((info) => { + if (info.menuItemId === 'manage-lists') { + this.openListManagerWindow(); + } + }); + } + + private openListManagerWindow(): void { + chrome.windows.create({ + url: chrome.runtime.getURL('list-manager/list-manager.html'), + type: 'popup', + width: 800, + height: 600, + focused: true }); } } -new BackgroundService(); \ No newline at end of file +new BackgroundService(); diff --git a/src/list-manager/ListManagerController.ts b/src/list-manager/ListManagerController.ts new file mode 100644 index 0000000..41510a3 --- /dev/null +++ b/src/list-manager/ListManagerController.ts @@ -0,0 +1,541 @@ +import { HighlightList, HighlightWord, ExportData } from '../types.js'; +import { StorageService } from '../services/StorageService.js'; +import { MessageService } from '../services/MessageService.js'; +import { DOMUtils } from '../utils/DOMUtils.js'; + +export class ListManagerController { + private lists: HighlightList[] = []; + private currentListIndex = 0; + private selectedLists = new Set(); + private selectedWords = new Set(); + private wordSearchQuery = ''; + private isReloading = false; + + async initialize(): Promise { + await this.loadData(); + this.setupEventListeners(); + this.render(); + this.setupStorageSync(); + } + + private async loadData(): Promise { + const data = await StorageService.get(); + this.lists = data.lists || []; + + if (this.lists.length === 0) { + this.lists.push({ + id: Date.now(), + name: chrome.i18n.getMessage('default_list_name') || 'Default List', + background: '#ffff00', + foreground: '#000000', + active: true, + words: [] + }); + } + + this.currentListIndex = Math.min(this.currentListIndex, this.lists.length - 1); + } + + private setupEventListeners(): void { + document.getElementById('newListBtn')?.addEventListener('click', () => this.createList()); + document.getElementById('duplicateListBtn')?.addEventListener('click', () => this.duplicateCurrentList()); + document.getElementById('mergeListsBtn')?.addEventListener('click', () => this.mergeSelectedLists()); + document.getElementById('deleteListsBtn')?.addEventListener('click', () => this.deleteSelectedLists()); + document.getElementById('activateListsBtn')?.addEventListener('click', () => this.setSelectedListsActive(true)); + document.getElementById('deactivateListsBtn')?.addEventListener('click', () => this.setSelectedListsActive(false)); + document.getElementById('applyListSettingsBtn')?.addEventListener('click', () => this.applyListSettings()); + document.getElementById('exportListBtn')?.addEventListener('click', () => this.exportCurrentList()); + + document.getElementById('selectAllWordsBtn')?.addEventListener('click', () => this.selectAllWords()); + document.getElementById('clearSelectedWordsBtn')?.addEventListener('click', () => this.clearSelectedWords()); + document.getElementById('enableWordsBtn')?.addEventListener('click', () => this.setSelectedWordsActive(true)); + document.getElementById('disableWordsBtn')?.addEventListener('click', () => this.setSelectedWordsActive(false)); + document.getElementById('deleteWordsBtn')?.addEventListener('click', () => this.deleteSelectedWords()); + document.getElementById('addWordsBtn')?.addEventListener('click', () => this.addWordsFromBulkInput()); + + document.getElementById('moveWordsBtn')?.addEventListener('click', () => this.moveOrCopySelectedWords(false)); + document.getElementById('copyWordsBtn')?.addEventListener('click', () => this.moveOrCopySelectedWords(true)); + + const wordSearch = document.getElementById('wordSearch') as HTMLInputElement; + wordSearch.addEventListener('input', (e) => { + this.wordSearchQuery = (e.target as HTMLInputElement).value; + this.renderWords(); + }); + + const listsContainer = document.getElementById('listsContainer'); + listsContainer?.addEventListener('click', (e) => this.handleListClick(e)); + listsContainer?.addEventListener('change', (e) => this.handleListCheckboxChange(e)); + listsContainer?.addEventListener('dragstart', (e) => this.handleDragStart(e)); + listsContainer?.addEventListener('dragover', (e) => this.handleDragOver(e)); + listsContainer?.addEventListener('drop', (e) => this.handleDrop(e)); + listsContainer?.addEventListener('dragend', () => this.clearDragState()); + + const wordList = document.getElementById('wordList'); + wordList?.addEventListener('change', (e) => this.handleWordListChange(e)); + wordList?.addEventListener('keydown', (e) => this.handleWordListKeydown(e)); + } + + private setupStorageSync(): void { + chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== 'local') return; + if (!changes.lists) return; + if (this.isReloading) return; + this.reloadFromStorage(); + }); + } + + private async reloadFromStorage(): Promise { + this.isReloading = true; + await this.loadData(); + this.selectedLists.clear(); + this.selectedWords.clear(); + this.render(); + this.isReloading = false; + } + + private createList(): void { + this.lists.push({ + id: Date.now(), + name: chrome.i18n.getMessage('new_list_name') || 'New List', + background: '#ffff00', + foreground: '#000000', + active: true, + words: [] + }); + this.currentListIndex = this.lists.length - 1; + this.selectedLists.clear(); + this.save(); + } + + private duplicateCurrentList(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + const newList: HighlightList = { + id: Date.now(), + name: `${list.name} (Copy)`, + background: list.background, + foreground: list.foreground, + active: list.active, + words: list.words.map(word => ({ ...word })) + }; + this.lists.splice(this.currentListIndex + 1, 0, newList); + this.currentListIndex = this.currentListIndex + 1; + this.selectedLists.clear(); + this.save(); + } + + private mergeSelectedLists(): void { + const selected = this.getSelectedListIndices(); + if (selected.length < 2) { + alert('Select at least two lists to merge.'); + return; + } + + const targetIndex = selected.includes(this.currentListIndex) ? this.currentListIndex : selected[0]; + const target = this.lists[targetIndex]; + if (!target) return; + + const confirmMessage = `Merge ${selected.length - 1} list(s) into "${target.name}"? Source lists will be removed.`; + if (!confirm(confirmMessage)) return; + + const sourceIndices = selected.filter(index => index !== targetIndex).sort((a, b) => b - a); + sourceIndices.forEach(index => { + const source = this.lists[index]; + if (source) { + target.words.push(...source.words.map(word => ({ ...word }))); + this.lists.splice(index, 1); + } + }); + + this.currentListIndex = Math.min(targetIndex, this.lists.length - 1); + this.selectedLists.clear(); + this.save(); + } + + private deleteSelectedLists(): void { + const selected = this.getSelectedListIndices(); + if (selected.length === 0) { + if (!this.lists[this.currentListIndex]) return; + if (!confirm('Delete current list?')) return; + this.lists.splice(this.currentListIndex, 1); + } else { + if (!confirm(`Delete ${selected.length} selected list(s)?`)) return; + selected.sort((a, b) => b - a).forEach(index => this.lists.splice(index, 1)); + } + + if (this.lists.length === 0) { + this.createList(); + return; + } + + this.currentListIndex = Math.min(this.currentListIndex, this.lists.length - 1); + this.selectedLists.clear(); + this.save(); + } + + private setSelectedListsActive(active: boolean): void { + const selected = this.getSelectedListIndices(); + if (selected.length === 0) { + const list = this.lists[this.currentListIndex]; + if (list) list.active = active; + } else { + selected.forEach(index => { + if (this.lists[index]) this.lists[index].active = active; + }); + } + this.save(); + } + + private applyListSettings(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const listName = document.getElementById('listName') as HTMLInputElement; + const listBg = document.getElementById('listBg') as HTMLInputElement; + const listFg = document.getElementById('listFg') as HTMLInputElement; + + list.name = listName.value; + list.background = listBg.value; + list.foreground = listFg.value; + + this.save(); + } + + private exportCurrentList(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const exportData: ExportData = { + lists: [list], + exceptionsList: [] + }; + + const blob = new Blob([JSON.stringify(exportData, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `highlight-list-${this.sanitizeFileName(list.name)}.json`; + a.click(); + URL.revokeObjectURL(url); + } + + private sanitizeFileName(name: string): string { + return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'list'; + } + + private selectAllWords(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + const entries = this.getFilteredWordEntries(list); + entries.forEach(entry => this.selectedWords.add(entry.index)); + this.renderWords(); + } + + private clearSelectedWords(): void { + this.selectedWords.clear(); + this.renderWords(); + } + + private setSelectedWordsActive(active: boolean): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + this.selectedWords.forEach(index => { + if (list.words[index]) list.words[index].active = active; + }); + this.save(); + } + + private deleteSelectedWords(): void { + const list = this.lists[this.currentListIndex]; + if (!list || this.selectedWords.size === 0) return; + if (!confirm(`Delete ${this.selectedWords.size} selected word(s)?`)) return; + + list.words = list.words.filter((_, i) => !this.selectedWords.has(i)); + this.selectedWords.clear(); + this.save(); + } + + private addWordsFromBulkInput(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const textarea = document.getElementById('bulkAddWords') as HTMLTextAreaElement | null; + if (!textarea) return; + + const words = textarea.value + .split(/\n+/) + .map(word => word.trim()) + .filter(Boolean); + + if (words.length === 0) return; + + words.forEach(word => { + list.words.push({ + wordStr: word, + background: '', + foreground: '', + active: true + }); + }); + + textarea.value = ''; + this.save(); + } + + private moveOrCopySelectedWords(copyOnly: boolean): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + if (this.selectedWords.size === 0) return; + + const targetSelect = document.getElementById('targetListSelect') as HTMLSelectElement; + const targetIndex = Number(targetSelect.value); + if (Number.isNaN(targetIndex) || targetIndex === this.currentListIndex) return; + + const targetList = this.lists[targetIndex]; + if (!targetList) return; + + const selectedIndices = Array.from(this.selectedWords).sort((a, b) => a - b); + const wordsToMove = selectedIndices.map(index => list.words[index]).filter(Boolean); + + if (copyOnly) { + targetList.words.push(...wordsToMove.map(word => ({ ...word }))); + } else { + targetList.words.push(...wordsToMove.map(word => ({ ...word }))); + list.words = list.words.filter((_, i) => !this.selectedWords.has(i)); + } + + this.selectedWords.clear(); + this.save(); + } + + private handleListClick(event: Event): void { + const target = event.target as HTMLElement; + const listItem = target.closest('.list-item') as HTMLElement | null; + if (!listItem) return; + + if (target.tagName === 'INPUT' || target.tagName === 'BUTTON') return; + + const index = Number(listItem.dataset.index); + if (Number.isNaN(index)) return; + + this.currentListIndex = index; + this.selectedWords.clear(); + this.render(); + } + + private handleListCheckboxChange(event: Event): void { + const target = event.target as HTMLInputElement; + if (!target.classList.contains('list-checkbox')) return; + const index = Number(target.dataset.index); + if (Number.isNaN(index)) return; + + if (target.checked) { + this.selectedLists.add(index); + } else { + this.selectedLists.delete(index); + } + } + + private handleDragStart(event: DragEvent): void { + const target = (event.target as HTMLElement).closest('.list-item') as HTMLElement | null; + if (!target) return; + const index = Number(target.dataset.index); + if (Number.isNaN(index)) return; + + event.dataTransfer?.setData('text/plain', index.toString()); + event.dataTransfer?.setDragImage(target, 10, 10); + } + + private handleDragOver(event: DragEvent): void { + event.preventDefault(); + const target = (event.target as HTMLElement).closest('.list-item') as HTMLElement | null; + if (!target) return; + target.classList.add('drag-over'); + } + + private handleDrop(event: DragEvent): void { + event.preventDefault(); + const target = (event.target as HTMLElement).closest('.list-item') as HTMLElement | null; + if (!target) return; + + const sourceIndex = Number(event.dataTransfer?.getData('text/plain')); + const targetIndex = Number(target.dataset.index); + if (Number.isNaN(sourceIndex) || Number.isNaN(targetIndex) || sourceIndex === targetIndex) { + this.clearDragState(); + return; + } + + const [moved] = this.lists.splice(sourceIndex, 1); + this.lists.splice(targetIndex, 0, moved); + + if (this.currentListIndex === sourceIndex) { + this.currentListIndex = targetIndex; + } else if (sourceIndex < this.currentListIndex && targetIndex >= this.currentListIndex) { + this.currentListIndex -= 1; + } else if (sourceIndex > this.currentListIndex && targetIndex <= this.currentListIndex) { + this.currentListIndex += 1; + } + + this.selectedLists.clear(); + this.save(); + } + + private clearDragState(): void { + document.querySelectorAll('.list-item.drag-over').forEach(item => item.classList.remove('drag-over')); + } + + private handleWordListChange(event: Event): void { + const target = event.target as HTMLInputElement; + const list = this.lists[this.currentListIndex]; + if (!list) return; + + if (target.classList.contains('word-checkbox') && target.dataset.index != null) { + const index = Number(target.dataset.index); + if (target.checked) { + this.selectedWords.add(index); + } else { + this.selectedWords.delete(index); + } + this.renderWords(); + return; + } + + const editIndex = Number(target.dataset.bgEdit ?? target.dataset.fgEdit ?? target.dataset.activeEdit ?? -1); + if (Number.isNaN(editIndex) || editIndex < 0) return; + + const word = list.words[editIndex]; + if (!word) return; + + if (target.dataset.bgEdit != null) word.background = target.value; + if (target.dataset.fgEdit != null) word.foreground = target.value; + if (target.dataset.activeEdit != null) word.active = target.checked; + + this.save(); + } + + private handleWordListKeydown(event: KeyboardEvent): void { + if (event.key !== 'Enter') return; + const target = event.target as HTMLInputElement; + const list = this.lists[this.currentListIndex]; + if (!list) return; + + const index = Number(target.dataset.wordEdit ?? -1); + if (Number.isNaN(index) || index < 0) return; + + const word = list.words[index]; + if (!word) return; + + word.wordStr = target.value; + this.save(); + } + + private getSelectedListIndices(): number[] { + return Array.from(this.selectedLists).filter(index => this.lists[index]); + } + + private getFilteredWordEntries(list: HighlightList): Array<{ word: HighlightWord; index: number }> { + const query = this.wordSearchQuery.trim().toLowerCase(); + const entries = list.words.map((word, index) => ({ word, index })); + if (!query) return entries; + return entries.filter(entry => entry.word.wordStr.toLowerCase().includes(query)); + } + + private render(): void { + this.renderLists(); + this.renderSelectedList(); + this.renderWords(); + } + + private renderLists(): void { + const container = document.getElementById('listsContainer'); + if (!container) return; + + container.innerHTML = ''; + + this.lists.forEach((list, index) => { + const total = list.words.length; + const activeCount = list.words.filter(word => word.active).length; + + const item = document.createElement('div'); + item.className = 'list-item'; + if (index === this.currentListIndex) item.classList.add('active'); + item.draggable = true; + item.dataset.index = index.toString(); + item.innerHTML = ` + +
+
${DOMUtils.escapeHtml(list.name)}
+
${total} words • ${activeCount} active
+
+
${list.active ? 'Active' : 'Paused'}
+ `; + container.appendChild(item); + }); + } + + private renderSelectedList(): void { + const list = this.lists[this.currentListIndex]; + if (!list) return; + + (document.getElementById('listName') as HTMLInputElement).value = list.name; + (document.getElementById('listBg') as HTMLInputElement).value = list.background; + (document.getElementById('listFg') as HTMLInputElement).value = list.foreground; + + const stats = document.getElementById('listStats'); + if (stats) { + const activeCount = list.words.filter(word => word.active).length; + const inactiveCount = list.words.length - activeCount; + stats.textContent = `${list.words.length} words • ${activeCount} active • ${inactiveCount} inactive`; + } + + this.renderTargetListOptions(); + } + + private renderTargetListOptions(): void { + const select = document.getElementById('targetListSelect') as HTMLSelectElement; + if (!select) return; + + const options = this.lists + .map((list, index) => ({ list, index })) + .filter(entry => entry.index !== this.currentListIndex) + .map(entry => ``); + + select.innerHTML = options.join(''); + select.disabled = options.length === 0; + } + + private renderWords(): void { + const list = this.lists[this.currentListIndex]; + const wordList = document.getElementById('wordList'); + if (!list || !wordList) return; + + const entries = this.getFilteredWordEntries(list); + + if (entries.length === 0) { + wordList.innerHTML = '
No words in this list.
'; + return; + } + + wordList.innerHTML = entries.map(entry => { + const word = entry.word; + const index = entry.index; + return ` +
+ + + + + +
+ `; + }).join(''); + } + + private async save(): Promise { + await StorageService.set({ + lists: this.lists + }); + this.render(); + MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' }); + } +} diff --git a/src/list-manager/list-manager.ts b/src/list-manager/list-manager.ts new file mode 100644 index 0000000..461d312 --- /dev/null +++ b/src/list-manager/list-manager.ts @@ -0,0 +1,6 @@ +import { ListManagerController } from './ListManagerController.js'; + +document.addEventListener('DOMContentLoaded', async () => { + const controller = new ListManagerController(); + await controller.initialize(); +}); diff --git a/src/popup/PopupController.ts b/src/popup/PopupController.ts index 630d411..af8bebb 100644 --- a/src/popup/PopupController.ts +++ b/src/popup/PopupController.ts @@ -116,6 +116,7 @@ export class PopupController { this.setupExceptions(); this.setupImportExport(); this.setupTheme(); + this.setupStorageSync(); } private setupTabs(): void { @@ -164,6 +165,10 @@ export class PopupController { this.save(); } }); + + document.getElementById('manageListsBtn')?.addEventListener('click', () => { + this.openListManagerWindow(); + }); } private setupWordManagement(): void { @@ -583,6 +588,48 @@ export class PopupController { MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' }); } + private openListManagerWindow(): void { + chrome.windows.create({ + url: chrome.runtime.getURL('list-manager/list-manager.html'), + type: 'popup', + width: 800, + height: 600, + focused: true + }); + } + + private setupStorageSync(): void { + chrome.storage.onChanged.addListener((changes, areaName) => { + if (areaName !== 'local') return; + if (changes.lists || changes.globalHighlightEnabled || changes.matchCaseEnabled || changes.matchWholeEnabled || changes.exceptionsList) { + this.reloadFromStorage(); + } + }); + } + + private async reloadFromStorage(): Promise { + const data = await StorageService.get(); + this.lists = data.lists || []; + this.globalHighlightEnabled = data.globalHighlightEnabled ?? true; + this.matchCaseEnabled = data.matchCaseEnabled ?? false; + this.matchWholeEnabled = data.matchWholeEnabled ?? false; + this.exceptionsList = data.exceptionsList || []; + + if (this.lists.length === 0) { + this.lists.push({ + id: Date.now(), + name: chrome.i18n.getMessage('default_list_name') || 'Default List', + background: '#ffff00', + foreground: '#000000', + active: true, + words: [] + }); + } + + this.currentListIndex = Math.min(this.currentListIndex, this.lists.length - 1); + this.render(); + } + @@ -721,4 +768,4 @@ export class PopupController { (document.getElementById('matchCase') as HTMLInputElement).checked = this.matchCaseEnabled; (document.getElementById('matchWhole') as HTMLInputElement).checked = this.matchWholeEnabled; } -} \ No newline at end of file +}