mirror of
https://github.com/zadam/trilium.git
synced 2025-12-06 23:44:25 +01:00
feat: implement popup interface
Features: - Quick action buttons (Selection, Page, Link, Screenshot, Image) - Connection status indicator with real-time updates - Theme toggle (system/light/dark) with visual feedback - Navigation to Settings and Logs pages - Keyboard shortcuts display - Full theme system integration Entry point for most user interactions. Initializes theme on load and persists preference. Uses centralized logging for debugging.
This commit is contained in:
parent
b51f83555b
commit
90c58142ce
212
apps/web-clipper-manifestv3/src/popup/index.html
Normal file
212
apps/web-clipper-manifestv3/src/popup/index.html
Normal file
@ -0,0 +1,212 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Trilium Web Clipper</title>
|
||||
<link rel="stylesheet" href="popup.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="popup-container">
|
||||
<header class="popup-header">
|
||||
<h1 class="popup-title">
|
||||
<img src="../icons/icon-32.png" alt="Trilium" class="popup-icon">
|
||||
Trilium Web Clipper
|
||||
</h1>
|
||||
<div id="persistent-connection-status" class="persistent-connection-status" title="Connection status">
|
||||
<span class="persistent-status-dot disconnected"></span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="popup-main">
|
||||
<div class="action-buttons">
|
||||
<button id="save-selection" class="action-btn" title="Ctrl+Shift+S">
|
||||
<span class="btn-icon">✂</span>
|
||||
<span class="btn-text">Save Selection</span>
|
||||
</button>
|
||||
|
||||
<button id="save-page" class="action-btn" title="Alt+Shift+S">
|
||||
<span class="btn-icon">□</span>
|
||||
<span class="btn-text">Save Full Page</span>
|
||||
</button>
|
||||
|
||||
<button id="save-screenshot" class="action-btn" title="Ctrl+Shift+E">
|
||||
<span class="btn-icon">⊞</span>
|
||||
<span class="btn-text">Save Screenshot</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="status-section">
|
||||
<div id="status-message" class="status-message hidden">
|
||||
<span id="status-text"></span>
|
||||
</div>
|
||||
|
||||
<div id="progress-bar" class="progress-bar hidden">
|
||||
<div class="progress-fill"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="info-section">
|
||||
<div class="current-page">
|
||||
<h3>Current Page</h3>
|
||||
<p id="page-title" class="page-title">Loading...</p>
|
||||
<p id="page-url" class="page-url">Loading...</p>
|
||||
|
||||
<!-- Already clipped indicator -->
|
||||
<div id="already-clipped" class="already-clipped hidden">
|
||||
<div class="clipped-label">
|
||||
<span class="clipped-icon">✓</span>
|
||||
<span class="clipped-text">Already saved today</span>
|
||||
</div>
|
||||
<a id="open-note-link" class="open-note-link" href="#" title="Open this note in Trilium">
|
||||
Open in Trilium →
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="trilium-status">
|
||||
<h3>Trilium Connection</h3>
|
||||
<div id="connection-status" class="connection-status">
|
||||
<span class="status-indicator"></span>
|
||||
<span id="connection-text">Checking...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- Settings Panel (hidden by default) -->
|
||||
<div id="settings-panel" class="settings-panel hidden">
|
||||
<div class="settings-header">
|
||||
<button id="back-to-main" class="back-btn">
|
||||
<span class="btn-icon">←</span>
|
||||
Back
|
||||
</button>
|
||||
<h2>Settings</h2>
|
||||
</div>
|
||||
|
||||
<div class="settings-content">
|
||||
<form id="settings-form">
|
||||
<div class="connection-section">
|
||||
<h3>Connection Settings</h3>
|
||||
|
||||
<div class="connection-subsection">
|
||||
<h4>Trilium Server</h4>
|
||||
<div class="form-group">
|
||||
<label for="trilium-url">Server URL:</label>
|
||||
<input type="url" id="trilium-url" placeholder="http://localhost:8080">
|
||||
<small>Enter the URL of your Trilium server (optional)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="enable-server" checked>
|
||||
<span>Enable server connection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-subsection">
|
||||
<h4>Trilium Desktop Client</h4>
|
||||
<div class="form-group">
|
||||
<label for="desktop-port">Desktop Client Port:</label>
|
||||
<input type="number" id="desktop-port" placeholder="37840" min="1" max="65535">
|
||||
<small>Port number for local Trilium desktop client (optional)</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="enable-desktop" checked>
|
||||
<span>Enable desktop client connection</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="connection-test">
|
||||
<button type="button" id="test-connection" class="secondary-btn">Test Connections</button>
|
||||
<div id="connection-result" class="connection-result hidden">
|
||||
<span class="connection-status-dot"></span>
|
||||
<span id="connection-result-text">Not tested</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-section">
|
||||
<h3>Content Settings</h3>
|
||||
<div class="form-group">
|
||||
<label for="default-title">Note Title Template:</label>
|
||||
<input type="text" id="default-title" placeholder="Web Clip - {title}" required>
|
||||
<small>Use {title}, {url}, {date}</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="screenshot-format">Screenshot Format:</label>
|
||||
<select id="screenshot-format">
|
||||
<option value="png">PNG (Higher Quality)</option>
|
||||
<option value="jpeg">JPEG (Smaller Size)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="auto-save">
|
||||
<span>Enable auto-save for selections</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="checkbox-label">
|
||||
<input type="checkbox" id="enable-toasts" checked>
|
||||
<span>Show toast notifications</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="theme-section">
|
||||
<h3>Theme Settings</h3>
|
||||
<div class="theme-options">
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="light">
|
||||
<span>Light</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="dark">
|
||||
<span>Dark</span>
|
||||
</label>
|
||||
<label class="theme-option">
|
||||
<input type="radio" name="theme" value="system">
|
||||
<span>System</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="settings-actions">
|
||||
<button type="submit" class="primary-btn">Save Settings</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="popup-footer">
|
||||
<button id="open-settings" class="footer-btn">
|
||||
<span class="btn-icon">⚙</span>
|
||||
Settings
|
||||
</button>
|
||||
|
||||
<button id="view-logs" class="footer-btn">
|
||||
<span class="btn-icon">≡</span>
|
||||
Logs
|
||||
</button>
|
||||
|
||||
<button id="theme-toggle" class="footer-btn theme-toggle" title="Toggle theme">
|
||||
<span class="btn-icon">☽</span>
|
||||
<span id="theme-text">Dark</span>
|
||||
</button>
|
||||
|
||||
<button id="help" class="footer-btn">
|
||||
<span class="btn-icon">?</span>
|
||||
Help
|
||||
</button>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script type="module" src="popup.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
689
apps/web-clipper-manifestv3/src/popup/popup.css
Normal file
689
apps/web-clipper-manifestv3/src/popup/popup.css
Normal file
@ -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;
|
||||
}
|
||||
744
apps/web-clipper-manifestv3/src/popup/popup.ts
Normal file
744
apps/web-clipper-manifestv3/src/popup/popup.ts
Normal file
@ -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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<string | undefined> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
try {
|
||||
await ThemeManager.initialize();
|
||||
await this.updateThemeButton();
|
||||
} catch (error) {
|
||||
logger.error('Failed to initialize theme', error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
private async handleThemeToggle(): Promise<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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<void> {
|
||||
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();
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user