diff --git a/apps/web-clipper-manifestv3/src/popup/index.html b/apps/web-clipper-manifestv3/src/popup/index.html
new file mode 100644
index 000000000..29e7840b6
--- /dev/null
+++ b/apps/web-clipper-manifestv3/src/popup/index.html
@@ -0,0 +1,212 @@
+
+
+
+
+
+ Trilium Web Clipper
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/apps/web-clipper-manifestv3/src/popup/popup.css b/apps/web-clipper-manifestv3/src/popup/popup.css
new file mode 100644
index 000000000..44a3bd4b4
--- /dev/null
+++ b/apps/web-clipper-manifestv3/src/popup/popup.css
@@ -0,0 +1,689 @@
+/* Modern Trilium Web Clipper Popup Styles with Theme Support */
+
+/* Import shared theme system */
+@import url('../shared/theme.css');
+
+/* Reset and base styles */
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.5;
+ color: var(--color-text-primary);
+ background: var(--color-bg-primary);
+ width: 380px;
+ min-height: 500px;
+ max-height: 600px;
+ transition: var(--theme-transition);
+}
+
+/* Popup container */
+.popup-container {
+ display: flex;
+ flex-direction: column;
+ height: 100%;
+}
+
+/* Header */
+.popup-header {
+ background: var(--color-primary);
+ color: var(--color-text-inverse);
+ padding: 16px;
+ text-align: center;
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.popup-title {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ font-size: 18px;
+ font-weight: 600;
+ margin: 0;
+}
+
+.persistent-connection-status {
+ position: absolute;
+ right: 16px;
+ top: 50%;
+ transform: translateY(-50%);
+}
+
+.persistent-status-dot {
+ width: 12px;
+ height: 12px;
+ border-radius: 50%;
+ display: inline-block;
+ cursor: pointer;
+}
+
+.persistent-status-dot.connected {
+ background-color: #22c55e;
+ box-shadow: 0 0 6px rgba(34, 197, 94, 0.5);
+}
+
+.persistent-status-dot.disconnected {
+ background-color: #ef4444;
+ box-shadow: 0 0 6px rgba(239, 68, 68, 0.5);
+}
+
+.persistent-status-dot.testing {
+ background-color: #f59e0b;
+ box-shadow: 0 0 6px rgba(245, 158, 11, 0.5);
+ animation: pulse 1.5s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.5; }
+}
+
+.popup-icon {
+ width: 24px;
+ height: 24px;
+}
+
+/* Main content */
+.popup-main {
+ flex: 1;
+ padding: 16px;
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+/* Action buttons */
+.action-buttons {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.action-btn {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 12px 16px;
+ border: 1px solid var(--color-border-primary);
+ border-radius: 8px;
+ background: var(--color-surface);
+ color: var(--color-text-primary);
+ font-size: 14px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: var(--theme-transition);
+ text-align: left;
+}
+
+.action-btn:hover {
+ background: var(--color-surface-hover);
+ border-color: var(--color-border-focus);
+ transform: translateY(-1px);
+ box-shadow: var(--shadow-md);
+}
+
+.action-btn:active {
+ transform: translateY(0);
+ box-shadow: var(--shadow-sm);
+}
+
+.action-btn:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+}
+
+.btn-icon {
+ font-size: 18px;
+ min-width: 18px;
+ color: var(--color-icon-secondary);
+}
+
+.action-btn:hover .btn-icon {
+ color: var(--color-primary);
+}
+
+.btn-text {
+ flex: 1;
+}
+
+/* Status section */
+.status-section {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.status-message {
+ padding: 12px;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-message--info {
+ background: var(--color-info-bg);
+ color: var(--color-info-text);
+ border: 1px solid var(--color-info-border);
+}
+
+.status-message--success {
+ background: var(--color-success-bg);
+ color: var(--color-success-text);
+ border: 1px solid var(--color-success-border);
+}
+
+.status-message--error {
+ background: var(--color-error-bg);
+ color: var(--color-error-text);
+ border: 1px solid var(--color-error-border);
+}
+
+.progress-bar {
+ height: 4px;
+ background: var(--color-border-primary);
+ border-radius: 2px;
+ overflow: hidden;
+}
+
+.progress-fill {
+ height: 100%;
+ background: var(--color-primary-gradient);
+ border-radius: 2px;
+ animation: progress-indeterminate 2s infinite;
+}
+
+@keyframes progress-indeterminate {
+ 0% {
+ transform: translateX(-100%);
+ }
+ 100% {
+ transform: translateX(400px);
+ }
+}
+
+.hidden {
+ display: none !important;
+}
+
+/* Info section */
+.info-section {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.info-section h3 {
+ font-size: 12px;
+ font-weight: 600;
+ color: var(--color-text-secondary);
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 8px;
+}
+
+.current-page {
+ padding: 12px;
+ background: var(--color-surface-secondary);
+ border-radius: 6px;
+ border: 1px solid var(--color-border-primary);
+}
+
+.page-title {
+ font-weight: 500;
+ color: var(--color-text-primary);
+ margin-bottom: 4px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.page-url {
+ font-size: 12px;
+ color: var(--color-text-secondary);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Already clipped indicator */
+.already-clipped {
+ margin-top: 12px;
+ padding: 10px 12px;
+ background: var(--color-success-bg);
+ border: 1px solid var(--color-success-border);
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+}
+
+.already-clipped.hidden {
+ display: none;
+}
+
+.clipped-label {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ flex: 1;
+}
+
+.clipped-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 18px;
+ height: 18px;
+ background: var(--color-success);
+ color: white;
+ border-radius: 50%;
+ font-size: 11px;
+ font-weight: bold;
+}
+
+.clipped-text {
+ font-size: 13px;
+ font-weight: 500;
+ color: var(--color-success);
+}
+
+.open-note-link {
+ font-size: 12px;
+ color: var(--color-primary);
+ text-decoration: none;
+ font-weight: 500;
+ white-space: nowrap;
+ transition: all 0.2s;
+}
+
+.open-note-link:hover {
+ color: var(--color-primary-hover);
+ text-decoration: underline;
+}
+
+.open-note-link:focus {
+ outline: 2px solid var(--color-primary);
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+
+.trilium-status {
+ padding: 12px;
+ background: var(--color-surface-secondary);
+ border-radius: 6px;
+ border: 1px solid var(--color-border-primary);
+}
+
+.connection-status {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+}
+
+.status-indicator {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background: var(--color-text-secondary);
+}
+
+.connection-status[data-status="connected"] .status-indicator {
+ background: var(--color-success);
+}
+
+.connection-status[data-status="disconnected"] .status-indicator {
+ background: var(--color-error);
+}
+
+.connection-status[data-status="checking"] .status-indicator {
+ background: var(--color-warning);
+ animation: pulse 2s infinite;
+}
+
+@keyframes pulse {
+ 0%, 100% {
+ opacity: 1;
+ }
+ 50% {
+ opacity: 0.5;
+ }
+}
+
+/* Footer */
+.popup-footer {
+ border-top: 1px solid var(--color-border-primary);
+ padding: 12px;
+ display: flex;
+ justify-content: space-between;
+ background: var(--color-surface-secondary);
+}
+
+.footer-btn {
+ display: flex;
+ align-items: center;
+ gap: 3px;
+ padding: 5px 8px;
+ border: none;
+ border-radius: 6px;
+ background: transparent;
+ color: var(--color-text-secondary);
+ font-size: 12px;
+ cursor: pointer;
+ transition: var(--theme-transition);
+}
+
+.footer-btn:hover {
+ background: var(--color-surface-hover);
+ color: var(--color-text-primary);
+}
+
+.footer-btn .btn-icon {
+ font-size: 14px;
+}
+
+/* Responsive adjustments */
+@media (max-width: 400px) {
+ body {
+ width: 320px;
+ }
+
+ .popup-main {
+ padding: 12px;
+ }
+
+ .action-btn {
+ padding: 10px 12px;
+ }
+}
+
+/* Theme toggle button styles */
+.theme-toggle {
+ background: var(--color-surface);
+ border: 1px solid var(--color-border-primary);
+ color: var(--color-text-secondary);
+}
+
+.theme-toggle:hover {
+ background: var(--color-surface-hover);
+ color: var(--color-text-primary);
+}
+
+/* Settings Panel Styles */
+.settings-panel {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: var(--color-bg-primary);
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+}
+
+.settings-panel.hidden {
+ display: none;
+}
+
+.settings-header {
+ background: var(--color-primary);
+ color: var(--color-text-inverse);
+ padding: 12px 16px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.back-btn {
+ background: transparent;
+ border: none;
+ color: var(--color-text-inverse);
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 14px;
+}
+
+.back-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+
+.settings-header h2 {
+ margin: 0;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.settings-content {
+ flex: 1;
+ padding: 16px;
+ overflow-y: auto;
+}
+
+.form-group {
+ margin-bottom: 16px;
+}
+
+.form-group label {
+ display: block;
+ margin-bottom: 4px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+}
+
+.form-group input[type="url"],
+.form-group input[type="text"],
+.form-group select {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid var(--color-border-primary);
+ border-radius: 6px;
+ background: var(--color-surface);
+ color: var(--color-text-primary);
+ font-size: 14px;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: var(--color-primary);
+ box-shadow: 0 0 0 3px rgba(0, 124, 186, 0.1);
+}
+
+.form-group small {
+ display: block;
+ margin-top: 4px;
+ color: var(--color-text-secondary);
+ font-size: 12px;
+}
+
+.checkbox-label {
+ display: flex !important;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ margin-bottom: 0 !important;
+}
+
+.checkbox-label input[type="checkbox"] {
+ width: auto;
+ margin: 0;
+}
+
+.theme-section {
+ margin-top: 20px;
+ padding-top: 16px;
+ border-top: 1px solid var(--color-border-primary);
+}
+
+.theme-section h3 {
+ margin: 0 0 12px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.theme-options {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.theme-option {
+ display: flex !important;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ border: 1px solid var(--color-border-primary);
+ border-radius: 6px;
+ cursor: pointer;
+ background: var(--color-surface);
+ margin-bottom: 0 !important;
+}
+
+.theme-option:hover {
+ background: var(--color-surface-hover);
+}
+
+.theme-option input[type="radio"] {
+ width: auto;
+ margin: 0;
+}
+
+.theme-option input[type="radio"]:checked + span {
+ color: var(--color-primary);
+ font-weight: 500;
+}
+
+.settings-actions {
+ display: flex;
+ gap: 8px;
+ margin-top: 20px;
+ padding-top: 16px;
+ border-top: 1px solid var(--color-border-primary);
+}
+
+.secondary-btn {
+ flex: 1;
+ padding: 8px 16px;
+ border: 1px solid var(--color-border-primary);
+ border-radius: 6px;
+ background: var(--color-surface);
+ color: var(--color-text-secondary);
+ cursor: pointer;
+ font-size: 12px;
+}
+
+.secondary-btn:hover {
+ background: var(--color-surface-hover);
+ color: var(--color-text-primary);
+}
+
+.primary-btn {
+ flex: 1;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 6px;
+ background: var(--color-primary);
+ color: var(--color-text-inverse);
+ cursor: pointer;
+ font-size: 12px;
+ font-weight: 500;
+}
+
+.primary-btn:hover {
+ background: var(--color-primary-dark);
+}
+
+/* Settings section styles */
+.connection-section,
+.content-section {
+ margin-bottom: 20px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid var(--color-border-primary);
+}
+
+.connection-section h3,
+.content-section h3 {
+ margin: 0 0 12px 0;
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.connection-subsection {
+ margin-bottom: 16px;
+ padding: 12px;
+ background: var(--color-surface);
+ border-radius: 6px;
+ border: 1px solid var(--color-border-primary);
+}
+
+.connection-subsection h4 {
+ margin: 0 0 8px 0;
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--color-text-primary);
+}
+
+.connection-subsection .form-group {
+ margin-bottom: 10px;
+}
+
+.connection-subsection .form-group:last-child {
+ margin-bottom: 0;
+}
+
+.connection-test {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-top: 12px;
+}
+
+.connection-result {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ font-size: 12px;
+}
+
+.connection-result.hidden {
+ display: none;
+}
+
+.connection-status-dot {
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ display: inline-block;
+}
+
+.connection-status-dot.connected {
+ background-color: #22c55e;
+}
+
+.connection-status-dot.disconnected {
+ background-color: #ef4444;
+}
+
+.connection-status-dot.testing {
+ background-color: #f59e0b;
+ animation: pulse 1.5s infinite;
+}
\ No newline at end of file
diff --git a/apps/web-clipper-manifestv3/src/popup/popup.ts b/apps/web-clipper-manifestv3/src/popup/popup.ts
new file mode 100644
index 000000000..44a4636a0
--- /dev/null
+++ b/apps/web-clipper-manifestv3/src/popup/popup.ts
@@ -0,0 +1,744 @@
+import { Logger, MessageUtils } from '@/shared/utils';
+import { ThemeManager } from '@/shared/theme';
+
+const logger = Logger.create('Popup', 'popup');
+
+/**
+ * Popup script for the Trilium Web Clipper extension
+ * Handles the popup interface and user interactions
+ */
+class PopupController {
+ private elements: { [key: string]: HTMLElement } = {};
+ private connectionCheckInterval?: number;
+
+ constructor() {
+ this.initialize();
+ }
+
+ private async initialize(): Promise {
+ try {
+ logger.info('Initializing popup...');
+
+ this.cacheElements();
+ this.setupEventHandlers();
+ await this.initializeTheme();
+ await this.loadCurrentPageInfo();
+ await this.checkTriliumConnection();
+ this.startPeriodicConnectionCheck();
+
+ logger.info('Popup initialized successfully');
+ } catch (error) {
+ logger.error('Failed to initialize popup', error as Error);
+ this.showError('Failed to initialize popup');
+ }
+ }
+
+ private cacheElements(): void {
+ const elementIds = [
+ 'save-selection',
+ 'save-page',
+ 'save-screenshot',
+ 'open-settings',
+ 'back-to-main',
+ 'view-logs',
+ 'help',
+ 'theme-toggle',
+ 'theme-text',
+ 'status-message',
+ 'status-text',
+ 'progress-bar',
+ 'page-title',
+ 'page-url',
+ 'connection-status',
+ 'connection-text',
+ 'settings-panel',
+ 'settings-form',
+ 'trilium-url',
+ 'enable-server',
+ 'desktop-port',
+ 'enable-desktop',
+ 'default-title',
+ 'auto-save',
+ 'enable-toasts',
+ 'screenshot-format',
+ 'test-connection',
+ 'persistent-connection-status',
+ 'connection-result',
+ 'connection-result-text'
+ ];
+
+ elementIds.forEach(id => {
+ const element = document.getElementById(id);
+ if (element) {
+ this.elements[id] = element;
+ } else {
+ logger.warn(`Element not found: ${id}`);
+ }
+ });
+ }
+
+ private setupEventHandlers(): void {
+ // Action buttons
+ this.elements['save-selection']?.addEventListener('click', this.handleSaveSelection.bind(this));
+ this.elements['save-page']?.addEventListener('click', this.handleSavePage.bind(this));
+ this.elements['save-screenshot']?.addEventListener('click', this.handleSaveScreenshot.bind(this));
+
+ // Footer buttons
+ this.elements['open-settings']?.addEventListener('click', this.handleOpenSettings.bind(this));
+ this.elements['back-to-main']?.addEventListener('click', this.handleBackToMain.bind(this));
+ this.elements['view-logs']?.addEventListener('click', this.handleViewLogs.bind(this));
+ this.elements['theme-toggle']?.addEventListener('click', this.handleThemeToggle.bind(this));
+ this.elements['help']?.addEventListener('click', this.handleHelp.bind(this));
+
+ // Settings form
+ this.elements['settings-form']?.addEventListener('submit', this.handleSaveSettings.bind(this));
+ this.elements['test-connection']?.addEventListener('click', this.handleTestConnection.bind(this));
+
+ // Theme radio buttons
+ const themeRadios = document.querySelectorAll('input[name="theme"]');
+ themeRadios.forEach(radio => {
+ radio.addEventListener('change', this.handleThemeRadioChange.bind(this));
+ });
+
+ // Keyboard shortcuts
+ document.addEventListener('keydown', this.handleKeyboardShortcuts.bind(this));
+ }
+
+ private handleKeyboardShortcuts(event: KeyboardEvent): void {
+ if (event.ctrlKey && event.shiftKey && event.key === 'S') {
+ event.preventDefault();
+ this.handleSaveSelection();
+ } else if (event.altKey && event.shiftKey && event.key === 'S') {
+ event.preventDefault();
+ this.handleSavePage();
+ } else if (event.ctrlKey && event.shiftKey && event.key === 'E') {
+ event.preventDefault();
+ this.handleSaveScreenshot();
+ }
+ }
+
+ private async handleSaveSelection(): Promise {
+ logger.info('Save selection requested');
+
+ try {
+ this.showProgress('Saving selection...');
+
+ const response = await MessageUtils.sendMessage({
+ type: 'SAVE_SELECTION'
+ });
+
+ this.showSuccess('Selection saved successfully!');
+ logger.info('Selection saved', { response });
+ } catch (error) {
+ this.showError('Failed to save selection');
+ logger.error('Failed to save selection', error as Error);
+ }
+ }
+
+ private async handleSavePage(): Promise {
+ logger.info('Save page requested');
+
+ try {
+ this.showProgress('Saving page...');
+
+ const response = await MessageUtils.sendMessage({
+ type: 'SAVE_PAGE'
+ });
+
+ this.showSuccess('Page saved successfully!');
+ logger.info('Page saved', { response });
+ } catch (error) {
+ this.showError('Failed to save page');
+ logger.error('Failed to save page', error as Error);
+ }
+ }
+
+ private async handleSaveScreenshot(): Promise {
+ logger.info('Save screenshot requested');
+
+ try {
+ this.showProgress('Capturing screenshot...');
+
+ const response = await MessageUtils.sendMessage({
+ type: 'SAVE_SCREENSHOT'
+ });
+
+ this.showSuccess('Screenshot saved successfully!');
+ logger.info('Screenshot saved', { response });
+ } catch (error) {
+ this.showError('Failed to save screenshot');
+ logger.error('Failed to save screenshot', error as Error);
+ }
+ }
+
+ private handleOpenSettings(): void {
+ try {
+ logger.info('Opening settings panel');
+ this.showSettingsPanel();
+ } catch (error) {
+ logger.error('Failed to open settings panel', error as Error);
+ }
+ }
+
+ private handleBackToMain(): void {
+ try {
+ logger.info('Returning to main panel');
+ this.hideSettingsPanel();
+ } catch (error) {
+ logger.error('Failed to return to main panel', error as Error);
+ }
+ }
+
+ private showSettingsPanel(): void {
+ const settingsPanel = this.elements['settings-panel'];
+ if (settingsPanel) {
+ settingsPanel.classList.remove('hidden');
+ this.loadSettingsData();
+ }
+ }
+
+ private hideSettingsPanel(): void {
+ const settingsPanel = this.elements['settings-panel'];
+ if (settingsPanel) {
+ settingsPanel.classList.add('hidden');
+ }
+ }
+
+ private async loadSettingsData(): Promise {
+ try {
+ const settings = await chrome.storage.sync.get([
+ 'triliumUrl',
+ 'enableServer',
+ 'desktopPort',
+ 'enableDesktop',
+ 'defaultTitle',
+ 'autoSave',
+ 'enableToasts',
+ 'screenshotFormat'
+ ]);
+
+ // Populate connection form fields
+ const urlInput = this.elements['trilium-url'] as HTMLInputElement;
+ const enableServerCheck = this.elements['enable-server'] as HTMLInputElement;
+ const desktopPortInput = this.elements['desktop-port'] as HTMLInputElement;
+ const enableDesktopCheck = this.elements['enable-desktop'] as HTMLInputElement;
+
+ // Populate content form fields
+ const titleInput = this.elements['default-title'] as HTMLInputElement;
+ const autoSaveCheck = this.elements['auto-save'] as HTMLInputElement;
+ const toastsCheck = this.elements['enable-toasts'] as HTMLInputElement;
+ const formatSelect = this.elements['screenshot-format'] as HTMLSelectElement;
+
+ // Set connection values
+ if (urlInput) urlInput.value = settings.triliumUrl || '';
+ if (enableServerCheck) enableServerCheck.checked = settings.enableServer !== false;
+ if (desktopPortInput) desktopPortInput.value = settings.desktopPort || '37840';
+ if (enableDesktopCheck) enableDesktopCheck.checked = settings.enableDesktop !== false;
+
+ // Set content values
+ if (titleInput) titleInput.value = settings.defaultTitle || 'Web Clip - {title}';
+ if (autoSaveCheck) autoSaveCheck.checked = settings.autoSave || false;
+ if (toastsCheck) toastsCheck.checked = settings.enableToasts !== false;
+ if (formatSelect) formatSelect.value = settings.screenshotFormat || 'png';
+
+ // Load theme settings
+ const themeConfig = await ThemeManager.getThemeConfig();
+ const themeMode = themeConfig.followSystem ? 'system' : themeConfig.mode;
+ const themeRadio = document.querySelector(`input[name="theme"][value="${themeMode}"]`) as HTMLInputElement;
+ if (themeRadio) themeRadio.checked = true;
+
+ } catch (error) {
+ logger.error('Failed to load settings data', error as Error);
+ }
+ }
+
+ private async handleSaveSettings(event: Event): Promise {
+ event.preventDefault();
+ try {
+ logger.info('Saving settings');
+
+ // Connection settings
+ const urlInput = this.elements['trilium-url'] as HTMLInputElement;
+ const enableServerCheck = this.elements['enable-server'] as HTMLInputElement;
+ const desktopPortInput = this.elements['desktop-port'] as HTMLInputElement;
+ const enableDesktopCheck = this.elements['enable-desktop'] as HTMLInputElement;
+
+ // Content settings
+ const titleInput = this.elements['default-title'] as HTMLInputElement;
+ const autoSaveCheck = this.elements['auto-save'] as HTMLInputElement;
+ const toastsCheck = this.elements['enable-toasts'] as HTMLInputElement;
+ const formatSelect = this.elements['screenshot-format'] as HTMLSelectElement;
+
+ const settings = {
+ triliumUrl: urlInput?.value || '',
+ enableServer: enableServerCheck?.checked !== false,
+ desktopPort: desktopPortInput?.value || '37840',
+ enableDesktop: enableDesktopCheck?.checked !== false,
+ defaultTitle: titleInput?.value || 'Web Clip - {title}',
+ autoSave: autoSaveCheck?.checked || false,
+ enableToasts: toastsCheck?.checked !== false,
+ screenshotFormat: formatSelect?.value || 'png'
+ };
+
+ await chrome.storage.sync.set(settings);
+ this.showSuccess('Settings saved successfully!');
+
+ // Auto-hide settings panel after saving
+ setTimeout(() => {
+ this.hideSettingsPanel();
+ }, 1500);
+
+ } catch (error) {
+ logger.error('Failed to save settings', error as Error);
+ this.showError('Failed to save settings');
+ }
+ }
+
+ private async handleTestConnection(): Promise {
+ try {
+ logger.info('Testing connection');
+
+ // Get connection settings from form
+ const urlInput = this.elements['trilium-url'] as HTMLInputElement;
+ const enableServerCheck = this.elements['enable-server'] as HTMLInputElement;
+ const desktopPortInput = this.elements['desktop-port'] as HTMLInputElement;
+ const enableDesktopCheck = this.elements['enable-desktop'] as HTMLInputElement;
+
+ const serverUrl = urlInput?.value?.trim();
+ const enableServer = enableServerCheck?.checked;
+ const desktopPort = desktopPortInput?.value?.trim() || '37840';
+ const enableDesktop = enableDesktopCheck?.checked;
+
+ if (!enableServer && !enableDesktop) {
+ this.showConnectionResult('Please enable at least one connection type', 'disconnected');
+ return;
+ }
+
+ this.showConnectionResult('Testing connections...', 'testing');
+ this.updatePersistentStatus('testing', 'Testing connections...');
+
+ // Use the background service to test connections
+ const response = await MessageUtils.sendMessage({
+ type: 'TEST_CONNECTION',
+ serverUrl: enableServer ? serverUrl : undefined,
+ authToken: enableServer ? (await this.getStoredAuthToken(serverUrl)) : undefined,
+ desktopPort: enableDesktop ? desktopPort : undefined
+ }) as { success: boolean; results: any; error?: string };
+
+ if (!response.success) {
+ this.showConnectionResult(`Connection test failed: ${response.error}`, 'disconnected');
+ this.updatePersistentStatus('disconnected', 'Connection test failed');
+ return;
+ }
+
+ const connectionResults = this.processConnectionResults(response.results, enableServer, enableDesktop);
+
+ if (connectionResults.hasConnection) {
+ this.showConnectionResult(connectionResults.message, 'connected');
+ this.updatePersistentStatus('connected', connectionResults.statusTooltip);
+
+ // Trigger a new connection search to update the background service
+ await MessageUtils.sendMessage({ type: 'TRIGGER_CONNECTION_SEARCH' });
+ } else {
+ this.showConnectionResult(connectionResults.message, 'disconnected');
+ this.updatePersistentStatus('disconnected', connectionResults.statusTooltip);
+ }
+
+ } catch (error) {
+ logger.error('Connection test failed', error as Error);
+ const errorText = 'Connection test failed - check settings';
+ this.showConnectionResult(errorText, 'disconnected');
+ this.updatePersistentStatus('disconnected', 'Connection test failed');
+ }
+ }
+
+ private async getStoredAuthToken(serverUrl?: string): Promise {
+ try {
+ if (!serverUrl) return undefined;
+
+ const data = await chrome.storage.sync.get('authToken');
+ return data.authToken;
+ } catch (error) {
+ logger.error('Failed to get stored auth token', error as Error);
+ return undefined;
+ }
+ }
+
+ private processConnectionResults(results: any, enableServer: boolean, enableDesktop: boolean) {
+ const connectedSources: string[] = [];
+ const failedSources: string[] = [];
+ const statusMessages: string[] = [];
+
+ if (enableServer && results.server) {
+ if (results.server.connected) {
+ connectedSources.push(`Server (${results.server.version || 'Unknown'})`);
+ statusMessages.push(`Server: Connected`);
+ } else {
+ failedSources.push('Server');
+ }
+ }
+
+ if (enableDesktop && results.desktop) {
+ if (results.desktop.connected) {
+ connectedSources.push(`Desktop Client (${results.desktop.version || 'Unknown'})`);
+ statusMessages.push(`Desktop: Connected`);
+ } else {
+ failedSources.push('Desktop Client');
+ }
+ }
+
+ const hasConnection = connectedSources.length > 0;
+ let message = '';
+ let statusTooltip = '';
+
+ if (hasConnection) {
+ message = `Connected to: ${connectedSources.join(', ')}`;
+ statusTooltip = statusMessages.join(' | ');
+ } else {
+ message = `Failed to connect to: ${failedSources.join(', ')}`;
+ statusTooltip = 'No connections available';
+ }
+
+ return { hasConnection, message, statusTooltip };
+ }
+
+ private showConnectionResult(message: string, status: 'connected' | 'disconnected' | 'testing'): void {
+ const resultElement = this.elements['connection-result'];
+ const textElement = this.elements['connection-result-text'];
+ const dotElement = resultElement?.querySelector('.connection-status-dot');
+
+ if (resultElement && textElement && dotElement) {
+ resultElement.classList.remove('hidden');
+ textElement.textContent = message;
+
+ // Update dot status
+ dotElement.classList.remove('connected', 'disconnected', 'testing');
+ dotElement.classList.add(status);
+ }
+ }
+
+ private updatePersistentStatus(status: 'connected' | 'disconnected' | 'testing', tooltip: string): void {
+ const persistentStatus = this.elements['persistent-connection-status'];
+ const dotElement = persistentStatus?.querySelector('.persistent-status-dot');
+
+ if (persistentStatus && dotElement) {
+ // Update dot status
+ dotElement.classList.remove('connected', 'disconnected', 'testing');
+ dotElement.classList.add(status);
+
+ // Update tooltip
+ persistentStatus.setAttribute('title', tooltip);
+ }
+ }
+
+ private startPeriodicConnectionCheck(): void {
+ // Check connection every 30 seconds
+ this.connectionCheckInterval = window.setInterval(async () => {
+ try {
+ await this.checkTriliumConnection();
+ } catch (error) {
+ logger.error('Periodic connection check failed', error as Error);
+ }
+ }, 30000);
+
+ // Clean up interval when popup closes
+ window.addEventListener('beforeunload', () => {
+ if (this.connectionCheckInterval) {
+ clearInterval(this.connectionCheckInterval);
+ }
+ });
+ }
+
+ private async handleThemeRadioChange(event: Event): Promise {
+ try {
+ const target = event.target as HTMLInputElement;
+ const mode = target.value as 'light' | 'dark' | 'system';
+
+ logger.info('Theme changed via radio button', { mode });
+
+ if (mode === 'system') {
+ await ThemeManager.setThemeConfig({ mode: 'system', followSystem: true });
+ } else {
+ await ThemeManager.setThemeConfig({ mode, followSystem: false });
+ }
+
+ await this.updateThemeButton();
+
+ } catch (error) {
+ logger.error('Failed to change theme via radio', error as Error);
+ }
+ }
+
+ private handleViewLogs(): void {
+ logger.info('Opening log viewer');
+ chrome.tabs.create({ url: chrome.runtime.getURL('logs.html') });
+ window.close();
+ }
+
+ private handleHelp(): void {
+ logger.info('Opening help');
+ const helpUrl = 'https://github.com/zadam/trilium/wiki/Web-clipper';
+ chrome.tabs.create({ url: helpUrl });
+ window.close();
+ }
+
+ private async initializeTheme(): Promise {
+ try {
+ await ThemeManager.initialize();
+ await this.updateThemeButton();
+ } catch (error) {
+ logger.error('Failed to initialize theme', error as Error);
+ }
+ }
+
+ private async handleThemeToggle(): Promise {
+ try {
+ logger.info('Theme toggle requested');
+ await ThemeManager.toggleTheme();
+ await this.updateThemeButton();
+ } catch (error) {
+ logger.error('Failed to toggle theme', error as Error);
+ }
+ }
+
+ private async updateThemeButton(): Promise {
+ try {
+ const config = await ThemeManager.getThemeConfig();
+ const themeText = this.elements['theme-text'];
+ const themeIcon = this.elements['theme-toggle']?.querySelector('.btn-icon');
+
+ if (themeText) {
+ // Show current theme mode
+ if (config.followSystem || config.mode === 'system') {
+ themeText.textContent = 'System';
+ } else if (config.mode === 'light') {
+ themeText.textContent = 'Light';
+ } else {
+ themeText.textContent = 'Dark';
+ }
+ }
+
+ if (themeIcon) {
+ // Show icon for current theme
+ if (config.followSystem || config.mode === 'system') {
+ themeIcon.textContent = '↻';
+ } else if (config.mode === 'light') {
+ themeIcon.textContent = '☀';
+ } else {
+ themeIcon.textContent = '☽';
+ }
+ }
+ } catch (error) {
+ logger.error('Failed to update theme button', error as Error);
+ }
+ }
+
+ private async loadCurrentPageInfo(): Promise {
+ try {
+ const tabs = await chrome.tabs.query({ active: true, currentWindow: true });
+ const activeTab = tabs[0];
+
+ if (activeTab) {
+ this.updatePageInfo(activeTab.title || 'Untitled', activeTab.url || '');
+ }
+ } catch (error) {
+ logger.error('Failed to load current page info', error as Error);
+ this.updatePageInfo('Error loading page info', '');
+ }
+ }
+
+ private async updatePageInfo(title: string, url: string): Promise {
+ if (this.elements['page-title']) {
+ this.elements['page-title'].textContent = title;
+ this.elements['page-title'].title = title;
+ }
+
+ if (this.elements['page-url']) {
+ this.elements['page-url'].textContent = this.shortenUrl(url);
+ this.elements['page-url'].title = url;
+ }
+
+ // Check for existing note and show indicator
+ await this.checkForExistingNote(url);
+ }
+
+ private async checkForExistingNote(url: string): Promise {
+ try {
+ logger.info('Starting check for existing note', { url });
+
+ // Only check if we have a valid URL
+ if (!url || url.startsWith('chrome://') || url.startsWith('about:')) {
+ logger.debug('Skipping check - invalid URL', { url });
+ this.hideAlreadyClippedIndicator();
+ return;
+ }
+
+ logger.debug('Sending CHECK_EXISTING_NOTE message to background', { url });
+
+ // Send message to background to check for existing note
+ const response = await MessageUtils.sendMessage({
+ type: 'CHECK_EXISTING_NOTE',
+ url
+ }) as { exists: boolean; noteId?: string };
+
+ logger.info('Received response from background', { response });
+
+ if (response && response.exists && response.noteId) {
+ logger.info('Note exists - showing indicator', { noteId: response.noteId });
+ this.showAlreadyClippedIndicator(response.noteId);
+ } else {
+ logger.debug('Note does not exist - hiding indicator', { response });
+ this.hideAlreadyClippedIndicator();
+ }
+ } catch (error) {
+ logger.error('Failed to check for existing note', error as Error);
+ this.hideAlreadyClippedIndicator();
+ }
+ }
+
+ private showAlreadyClippedIndicator(noteId: string): void {
+ logger.info('Showing already-clipped indicator', { noteId });
+
+ const indicator = document.getElementById('already-clipped');
+ const openLink = document.getElementById('open-note-link') as HTMLAnchorElement;
+
+ logger.debug('Indicator element found', {
+ indicatorExists: !!indicator,
+ linkExists: !!openLink
+ });
+
+ if (indicator) {
+ indicator.classList.remove('hidden');
+ logger.debug('Removed hidden class from indicator');
+ } else {
+ logger.error('Could not find already-clipped element in DOM!');
+ }
+
+ if (openLink) {
+ openLink.onclick = (e: MouseEvent) => {
+ e.preventDefault();
+ this.handleOpenNoteInTrilium(noteId);
+ };
+ }
+ }
+
+ private hideAlreadyClippedIndicator(): void {
+ const indicator = document.getElementById('already-clipped');
+ if (indicator) {
+ indicator.classList.add('hidden');
+ }
+ }
+
+ private async handleOpenNoteInTrilium(noteId: string): Promise {
+ try {
+ logger.info('Opening note in Trilium', { noteId });
+
+ await MessageUtils.sendMessage({
+ type: 'OPEN_NOTE',
+ noteId
+ });
+
+ // Close popup after opening note
+ window.close();
+ } catch (error) {
+ logger.error('Failed to open note in Trilium', error as Error);
+ this.showError('Failed to open note in Trilium');
+ }
+ }
+
+ private shortenUrl(url: string): string {
+ if (url.length <= 50) return url;
+
+ try {
+ const urlObj = new URL(url);
+ return `${urlObj.hostname}${urlObj.pathname.substring(0, 20)}...`;
+ } catch {
+ return url.substring(0, 50) + '...';
+ }
+ }
+
+ private async checkTriliumConnection(): Promise {
+ try {
+ // Get saved connection settings
+ // We don't need to check individual settings anymore since the background service handles this
+
+ // Get current connection status from background service
+ const statusResponse = await MessageUtils.sendMessage({
+ type: 'GET_CONNECTION_STATUS'
+ }) as any;
+
+ const status = statusResponse?.status || 'not-found';
+
+ if (status === 'found-desktop' || status === 'found-server') {
+ const connectionType = status === 'found-desktop' ? 'Desktop Client' : 'Server';
+ const url = statusResponse?.url || 'Unknown';
+ this.updateConnectionStatus('connected', `Connected to ${connectionType}`);
+ this.updatePersistentStatus('connected', `${connectionType}: ${url}`);
+ } else if (status === 'searching') {
+ this.updateConnectionStatus('checking', 'Checking connections...');
+ this.updatePersistentStatus('testing', 'Searching for Trilium...');
+ } else {
+ this.updateConnectionStatus('disconnected', 'No active connections');
+ this.updatePersistentStatus('disconnected', 'No connections available');
+ }
+
+ } catch (error) {
+ logger.error('Failed to check Trilium connection', error as Error);
+ this.updateConnectionStatus('disconnected', 'Connection check failed');
+ this.updatePersistentStatus('disconnected', 'Connection check failed');
+ }
+ }
+
+ private updateConnectionStatus(status: 'connected' | 'disconnected' | 'checking' | 'testing', message: string): void {
+ const statusElement = this.elements['connection-status'];
+ const textElement = this.elements['connection-text'];
+
+ if (statusElement && textElement) {
+ statusElement.setAttribute('data-status', status);
+ textElement.textContent = message;
+ }
+ }
+
+ private showProgress(message: string): void {
+ this.showStatus(message, 'info');
+ this.elements['progress-bar']?.classList.remove('hidden');
+ }
+
+ private showSuccess(message: string): void {
+ this.showStatus(message, 'success');
+ this.elements['progress-bar']?.classList.add('hidden');
+
+ // Auto-hide after 3 seconds
+ setTimeout(() => {
+ this.hideStatus();
+ }, 3000);
+ }
+
+ private showError(message: string): void {
+ this.showStatus(message, 'error');
+ this.elements['progress-bar']?.classList.add('hidden');
+ }
+
+ private showStatus(message: string, type: 'info' | 'success' | 'error'): void {
+ const statusElement = this.elements['status-message'];
+ const textElement = this.elements['status-text'];
+
+ if (statusElement && textElement) {
+ statusElement.className = `status-message status-message--${type}`;
+ textElement.textContent = message;
+ statusElement.classList.remove('hidden');
+ }
+ }
+
+ private hideStatus(): void {
+ this.elements['status-message']?.classList.add('hidden');
+ this.elements['progress-bar']?.classList.add('hidden');
+ }
+}
+
+// Initialize the popup when DOM is loaded
+if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', () => new PopupController());
+} else {
+ new PopupController();
+}