mirror of
https://github.com/obsqrbtz/goose-highlighter.git
synced 2026-04-08 20:19:06 +03:00
WIP: list manager
This commit is contained in:
406
list-manager/list-manager.css
Normal file
406
list-manager/list-manager.css
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
98
list-manager/list-manager.html
Normal file
98
list-manager/list-manager.html
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Goose Highlighter - List Manager</title>
|
||||||
|
<link rel="stylesheet" href="list-manager.css" />
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.0/css/all.min.css" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="title">
|
||||||
|
<img src="../img/logo-outlined.png" alt="Goose Highlighter" />
|
||||||
|
<div>
|
||||||
|
<div class="name">Goose Highlighter</div>
|
||||||
|
<div class="subtitle">List Manager</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="topbar-actions">
|
||||||
|
<button id="exportListBtn" class="ghost"><i class="fa-solid fa-download"></i> Export List</button>
|
||||||
|
<button id="newListBtn" class="primary"><i class="fa-solid fa-plus"></i> New List</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="layout">
|
||||||
|
<section class="panel lists-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Lists</h2>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button id="duplicateListBtn"><i class="fa-solid fa-clone"></i> Duplicate</button>
|
||||||
|
<button id="mergeListsBtn"><i class="fa-solid fa-code-merge"></i> Merge</button>
|
||||||
|
<button id="deleteListsBtn" class="danger"><i class="fa-solid fa-trash"></i> Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions secondary">
|
||||||
|
<button id="activateListsBtn"><i class="fa-solid fa-circle-check"></i> Activate</button>
|
||||||
|
<button id="deactivateListsBtn"><i class="fa-solid fa-circle-xmark"></i> Deactivate</button>
|
||||||
|
</div>
|
||||||
|
<div class="list-hint">Drag lists to reorder</div>
|
||||||
|
<div id="listsContainer" class="lists"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel details-panel">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Selected List</h2>
|
||||||
|
<div class="stats" id="listStats">0 words</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-settings">
|
||||||
|
<label>
|
||||||
|
Name
|
||||||
|
<input type="text" id="listName" />
|
||||||
|
</label>
|
||||||
|
<div class="colors">
|
||||||
|
<label>
|
||||||
|
Background
|
||||||
|
<input type="color" id="listBg" />
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Foreground
|
||||||
|
<input type="color" id="listFg" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<button id="applyListSettingsBtn" class="primary"><i class="fa-solid fa-check"></i> Apply</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="word-controls">
|
||||||
|
<div class="add-words">
|
||||||
|
<textarea id="bulkAddWords" placeholder="Paste words here..."></textarea>
|
||||||
|
<button id="addWordsBtn" class="primary"><i class="fa-solid fa-plus"></i> Add Words</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="selectAllWordsBtn">Select All</button>
|
||||||
|
<button id="clearSelectedWordsBtn">Clear</button>
|
||||||
|
<button id="enableWordsBtn">Enable</button>
|
||||||
|
<button id="disableWordsBtn">Disable</button>
|
||||||
|
<button id="deleteWordsBtn" class="danger">Delete</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input type="text" id="wordSearch" placeholder="Search words..." />
|
||||||
|
<select id="targetListSelect"></select>
|
||||||
|
<button id="moveWordsBtn"><i class="fa-solid fa-arrow-right"></i> Move</button>
|
||||||
|
<button id="copyWordsBtn"><i class="fa-solid fa-copy"></i> Copy</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wordList" class="word-list"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="../dist/list-manager/list-manager.js"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -5,9 +5,11 @@
|
|||||||
"version": "1.11.0",
|
"version": "1.11.0",
|
||||||
"default_locale": "en",
|
"default_locale": "en",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
|
"contextMenus",
|
||||||
"scripting",
|
"scripting",
|
||||||
"storage",
|
"storage",
|
||||||
"tabs"
|
"tabs",
|
||||||
|
"windows"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"<all_urls>"
|
"<all_urls>"
|
||||||
@@ -24,4 +26,4 @@
|
|||||||
"48": "icons/icon48.png",
|
"48": "icons/icon48.png",
|
||||||
"128": "icons/icon128.png"
|
"128": "icons/icon128.png"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@
|
|||||||
<button id="deleteListBtn" class="danger"><i class="fa-solid fa-trash"></i> <span
|
<button id="deleteListBtn" class="danger"><i class="fa-solid fa-trash"></i> <span
|
||||||
data-i18n="delete_list">Delete</span></button>
|
data-i18n="delete_list">Delete</span></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="button-row">
|
||||||
|
<button id="manageListsBtn"><i class="fa-solid fa-layer-group"></i> Manage Lists</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section" data-section="settings">
|
<div class="section" data-section="settings">
|
||||||
@@ -158,4 +161,4 @@
|
|||||||
<script type="module" src="../dist/popup/popup.js"></script>
|
<script type="module" src="../dist/popup/popup.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ class BackgroundService {
|
|||||||
private initialize(): void {
|
private initialize(): void {
|
||||||
this.setupTabUpdateListener();
|
this.setupTabUpdateListener();
|
||||||
this.setupInstallListener();
|
this.setupInstallListener();
|
||||||
|
this.setupContextMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupTabUpdateListener(): void {
|
private setupTabUpdateListener(): void {
|
||||||
@@ -29,8 +30,33 @@ class BackgroundService {
|
|||||||
if (!data.exceptionsList) {
|
if (!data.exceptionsList) {
|
||||||
await StorageService.update('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();
|
new BackgroundService();
|
||||||
|
|||||||
541
src/list-manager/ListManagerController.ts
Normal file
541
src/list-manager/ListManagerController.ts
Normal file
@@ -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<number>();
|
||||||
|
private selectedWords = new Set<number>();
|
||||||
|
private wordSearchQuery = '';
|
||||||
|
private isReloading = false;
|
||||||
|
|
||||||
|
async initialize(): Promise<void> {
|
||||||
|
await this.loadData();
|
||||||
|
this.setupEventListeners();
|
||||||
|
this.render();
|
||||||
|
this.setupStorageSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadData(): Promise<void> {
|
||||||
|
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<void> {
|
||||||
|
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 = `
|
||||||
|
<input type="checkbox" class="list-checkbox" data-index="${index}" ${this.selectedLists.has(index) ? 'checked' : ''}>
|
||||||
|
<div class="list-meta">
|
||||||
|
<div class="list-name">${DOMUtils.escapeHtml(list.name)}</div>
|
||||||
|
<div class="list-stats">${total} words • ${activeCount} active</div>
|
||||||
|
</div>
|
||||||
|
<div class="list-badge">${list.active ? 'Active' : 'Paused'}</div>
|
||||||
|
`;
|
||||||
|
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 => `<option value="${entry.index}">${DOMUtils.escapeHtml(entry.list.name)}</option>`);
|
||||||
|
|
||||||
|
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 = '<div class="empty">No words in this list.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wordList.innerHTML = entries.map(entry => {
|
||||||
|
const word = entry.word;
|
||||||
|
const index = entry.index;
|
||||||
|
return `
|
||||||
|
<div class="word-item ${word.active ? '' : 'disabled'}">
|
||||||
|
<input type="checkbox" class="word-checkbox" data-index="${index}" ${this.selectedWords.has(index) ? 'checked' : ''}>
|
||||||
|
<input type="text" value="${DOMUtils.escapeHtml(word.wordStr)}" data-word-edit="${index}">
|
||||||
|
<input type="color" value="${word.background || list.background}" data-bg-edit="${index}">
|
||||||
|
<input type="color" value="${word.foreground || list.foreground}" data-fg-edit="${index}">
|
||||||
|
<input type="checkbox" data-active-edit="${index}" ${word.active ? 'checked' : ''} title="Active">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private async save(): Promise<void> {
|
||||||
|
await StorageService.set({
|
||||||
|
lists: this.lists
|
||||||
|
});
|
||||||
|
this.render();
|
||||||
|
MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' });
|
||||||
|
}
|
||||||
|
}
|
||||||
6
src/list-manager/list-manager.ts
Normal file
6
src/list-manager/list-manager.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { ListManagerController } from './ListManagerController.js';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
const controller = new ListManagerController();
|
||||||
|
await controller.initialize();
|
||||||
|
});
|
||||||
@@ -116,6 +116,7 @@ export class PopupController {
|
|||||||
this.setupExceptions();
|
this.setupExceptions();
|
||||||
this.setupImportExport();
|
this.setupImportExport();
|
||||||
this.setupTheme();
|
this.setupTheme();
|
||||||
|
this.setupStorageSync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupTabs(): void {
|
private setupTabs(): void {
|
||||||
@@ -164,6 +165,10 @@ export class PopupController {
|
|||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
document.getElementById('manageListsBtn')?.addEventListener('click', () => {
|
||||||
|
this.openListManagerWindow();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private setupWordManagement(): void {
|
private setupWordManagement(): void {
|
||||||
@@ -583,6 +588,48 @@ export class PopupController {
|
|||||||
MessageService.sendToAllTabs({ type: 'WORD_LIST_UPDATED' });
|
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<void> {
|
||||||
|
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('matchCase') as HTMLInputElement).checked = this.matchCaseEnabled;
|
||||||
(document.getElementById('matchWhole') as HTMLInputElement).checked = this.matchWholeEnabled;
|
(document.getElementById('matchWhole') as HTMLInputElement).checked = this.matchWholeEnabled;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user