feat: major feature updates and cleanup

- Add Redis architecture implementation
- Update browser extension functionality
- Clean up deprecated files and documentation
- Enhance backend handlers for auth, messages, search
- Add new configuration options and settings
- Update Docker and deployment configurations
This commit is contained in:
Tomas Dvorak
2026-03-03 11:03:37 +01:00
parent 446bc7acfb
commit 083373a24f
241 changed files with 46662 additions and 24880 deletions
+288
View File
@@ -0,0 +1,288 @@
# Trackeep Saver Browser Extension
This folder contains a WebExtension (Manifest v3) that lets you:
- **Save the current page or video** as a **Trackeep bookmark**.
- **Upload a local file** directly to Trackeep.
- **Rightclick** any page, link, selection, image, or video and choose **“Save to Trackeep”** from the context menu.
It is designed to work in **Chrome**, **Microsoft Edge**, and **Firefox** (Manifest v3 where available).
---
## Folder structure
- `manifest.json` WebExtension manifest (v3) with background service worker and context menu.
- `popup.html` / `popup.js` Popup UI and logic to save bookmarks and upload files.
- `options.html` / `options.js` Options page to configure API URL and auth token.
- `background.js` Service worker that creates and handles the context menu.
- `icons/` Placeholder icon files (copied from the repo favicon).
- `README.md` This documentation.
> Note: For store publishing you will likely want custom icon PNG files. See the publishing section.
---
## What the extension does
### Popup (toolbar icon)
- Reads the **active tabs title and URL** and lets you save it as a Trackeep bookmark.
- Lets you add an optional **description**, **tags** (comma-separated), and mark a bookmark as **public**.
- Lets you pick a local **file** and upload it to Trackeep with an optional description.
### Rightclick context menu
- Rightclick any page, link, selection, image, or video and choose **“Save to Trackeep”**.
- The popup opens with:
- URL prefilled (link URL, image/video source, or current page URL).
- Title prefilled (tab title).
- Description prefilled with the selected text (if any).
- Works even if you rightclick a link on another site; the popup will open with that links details.
### Autodetect Trackeep domain
- When you open the popup or options page on a Trackeep domain (e.g. `https://app.trackeep.example`), the extension automatically:
- Prefills the **API base URL** to `https://app.trackeep.example/api/v1`.
- Falls back to `http://localhost:8080/api/v1` if nothing is set and youre not on a Trackeep domain.
- This reduces manual setup for most users.
---
## Configuration (Options page)
1. **Open the extension options**
- After loading the extension (see below), right-click its icon → **Options**.
- Or click **Open Options** in the popup.
2. **Set the API base URL**
- Usually autodetected if youre on a Trackeep domain.
- Example for local dev:
- `http://localhost:8080/api/v1`
- Example for production:
- `https://your-trackeep-domain.example.com/api/v1`
3. **Get your Trackeep auth token**
- Log into Trackeep in your browser.
- Open **DevTools → Application → Local Storage**.
- Select your Trackeep origin (e.g. `http://localhost:5173` or your production domain).
- Find the key `trackeep_token` and copy its **value**.
- Paste this value into the **Auth token** field in the options page.
4. **Save settings**
- Click **Save settings**.
- The popup will now use these values to call the API.
> Keep your auth token private. Treat it like a password.
---
## Loading the extension during development
### Chrome (and Brave, Vivaldi, etc.)
1. Open `chrome://extensions/`.
2. Enable **Developer mode** (top-right toggle).
3. Click **Load unpacked**.
4. Select the `browser-extension` folder from this repository.
5. The extension should appear with the name **Trackeep Saver**.
### Microsoft Edge
1. Open `edge://extensions/`.
2. Enable **Developer mode**.
3. Click **Load unpacked**.
4. Select the `browser-extension` folder.
### Firefox (Manifest v3)
Firefox support for Manifest v3 is still evolving, but this extension uses only basic APIs:
1. Open `about:debugging#/runtime/this-firefox`.
2. Click **Load Temporary Add-on…**.
3. Select the `manifest.json` file inside the `browser-extension` folder.
4. The extension will be installed temporarily (until you restart Firefox).
If you hit MV3-specific issues in Firefox, you can either:
- Switch to a Firefox version with MV3 enabled, or
- Port this to a MV2 manifest (same JS/HTML, different `manifest.json`).
---
## Using the extension
### Popup (toolbar icon)
1. Make sure you configured **API base URL** and **auth token** in the options.
2. Navigate to any page or video (e.g. a YouTube video, article, docs page).
3. Click the **Trackeep Saver** icon in the toolbar.
4. In the popup:
- Adjust **Title**, **URL**, **Description**, and **Tags** as needed.
- Optionally tick **Public**.
- Click **Save bookmark** to create a Trackeep bookmark.
5. To upload a file:
- Use the **Upload file to Trackeep** section.
- Pick a file, optionally add a description, then click **Upload file**.
### Context menu (rightclick)
1. Rightclick any page, link, selection, image, or video.
2. Choose **“Save to Trackeep”**.
3. The popup opens with the relevant data prefilled.
4. Edit as desired and click **Save bookmark**.
If anything fails, an error message from the Trackeep API is shown in the popup.
---
## CORS and backend configuration
The backend uses a CORS middleware that primarily targets browser frontends.
Because this is a browser extension, requests are made from an extension context and usually do **not** require the same CORS headers as a regular web page.
If you run into network errors:
- Make sure your Trackeep backend is reachable at the URL you configured.
- Check the browser extension console (in `chrome://extensions`**Inspect views****Service worker / popup**).
- If needed, relax or adjust the `CORS_ALLOWED_ORIGINS` env variable on the backend to include your frontend origin for normal web use. The extension itself generally should not require changes.
---
## Publishing to browser stores
The high-level process is similar across Chrome, Edge, and Firefox:
### 1. Prepare assets
- Make sure `manifest.json`, `popup.*`, `options.*`, and `background.js` are all present and working.
- Add icon PNG files (required for store publishing):
- The `icons/` folder contains placeholder files copied from the repo favicon.
- For production, replace them with custom icons at sizes 16, 32, 48, and 128 pixels.
- Update `manifest.json` with an `icons` section (already present).
### 2. Chrome Web Store (Chrome and most Chromium browsers)
1. Go to the **Chrome Web Store Developer Dashboard**.
2. Create a new item.
3. Zip the **contents of `browser-extension/`** (do not zip the parent folder twice).
4. Upload the ZIP.
5. Fill out listing details (name, description, screenshots, categories, privacy policy).
6. Submit for review.
Once published, Chrome, Brave, and other Chromium-based browsers can install it from the store.
### 3. Microsoft Edge Add-ons
1. Go to the **Microsoft Edge Add-ons** developer dashboard.
2. You can often upload the same ZIP you used for Chrome.
3. Fill in the listing information and submit.
Edge is also Chromium-based, so Manifest v3 and the `chrome.*` APIs are supported.
### 4. Firefox Add-ons (AMO)
1. Go to **https://addons.mozilla.org/developers/**.
2. Create a new add-on and upload the ZIP built from `browser-extension/`.
3. If Firefox flags MV3-specific issues, follow its guidance usually this involves:
- Ensuring the manifest is valid for the current Firefox MV3 implementation.
- Optionally adding `browser_specific_settings` in `manifest.json` with a Firefox-specific `gecko` ID.
Example snippet you may add for Firefox (Chrome will ignore this block):
```json
"browser_specific_settings": {
"gecko": {
"id": "trackeep-saver@example.com",
"strict_min_version": "120.0"
}
}
```
Use your own ID and version constraints as recommended by Mozilla.
---
## How to publish to extension stores
### Quick checklist before you publish
- [ ] Test the extension locally in Chrome/Edge/Firefox.
- [ ] Ensure the API URL and auth token work with your Trackeep backend.
- [ ] Replace placeholder icons with production assets (optional; `trackeepfavi_bg.png` is already used).
- [ ] Write a short description and prepare screenshots for the store listings.
- [ ] Decide on a publisher name and privacy policy URL (required by most stores).
### Stepbystep publishing
#### Chrome Web Store (and Chromium browsers)
1. **Prepare a ZIP**
- Zip the **contents of `browser-extension/`** (not the folder itself).
- Ensure `manifest.json`, `popup.*`, `options.*`, `background.js`, and `icons/` are included.
2. **Developer Dashboard**
- Go to the [Chrome Web Store Developer Dashboard](https://chrome.google.com/webstore/developer/dashboard).
- Click **Add new item**.
- Upload the ZIP.
3. **Listing details**
- **Name**: Trackeep Saver
- **Description**: Save pages, videos, and files to your Trackeep account.
- **Category**: Productivity
- **Screenshots**: 1280x800 or 640x400 PNGs.
- **Icon**: 128x128 PNG (already in `icons/icon128.png`).
- **Privacy policy**: Required; you can host a simple page on GitHub Pages or your site.
4. **Permissions review**
- The manifest requests `storage`, `tabs`, `activeTab`, `contextMenus`, and `<all_urls>` host permissions.
- Be prepared to explain why each is needed (bookmarking, uploading files, rightclick menu).
5. **Submit**
- Review and submit. Google will review for compliance and security.
#### Microsoft Edge Add-ons
1. Go to the [Microsoft Edge Add-ons Developer Dashboard](https://partner.microsoft.com/dashboard/microsoftedge).
2. Upload the same ZIP you used for Chrome.
3. Fill out the listing (similar to Chrome).
4. Submit. Edges review is usually fast.
#### Firefox Add-ons (AMO)
1. Go to the [Firefox Add-on Developer Hub](https://addons.mozilla.org/developers/).
2. Click **Submit a New Addon** and upload the ZIP.
3. Firefox may ask for a `browser_specific_settings.gecko.id` in `manifest.json`. If you want a fixed ID, add:
```json
"browser_specific_settings": {
"gecko": {
"id": "trackeep-saver@example.com",
"strict_min_version": "120.0"
}
}
```
Replace `example.com` with your own domain.
4. Provide listing details and privacy policy.
5. Submit. Mozillas review focuses on privacy and security.
---
## What you can do next
- **Test thoroughly**:
- Save bookmarks from different sites (articles, YouTube videos, GitHub repos).
- Upload various file types (PDFs, images, docs).
- Try the rightclick context menu on links, images, and selected text.
- **Improve UX**:
- Autotag YouTube videos as `video`.
- Add a keyboard shortcut to quicksave the current page.
- Sync the auth token automatically from the Trackeep web app.
- **Prepare for stores**:
- Write a concise privacy policy and host it publicly.
- Take clean screenshots of the popup and options page.
- Consider a custom icon set if you want a distinct brand look.
- **Maintain**:
- Keep the extension compatible with Trackeep API changes.
- Update the manifest version when you release updates.
For now, the extension is fully functional for bookmarking pages/videos and uploading files to Trackeep, with a convenient rightclick menu and smart domain autodetection.
+217
View File
@@ -0,0 +1,217 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// Handle keyboard commands
browser.commands.onCommand.addListener((command) => {
if (command === 'quick-save') {
// Get current tab and trigger quick save
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (tab) {
browser.storage.local.set({
contextMenuData: {
url: tab.url,
title: tab.title,
selection: '',
timestamp: Date.now(),
isQuickSave: true
}
}, () => {
browser.action.openPopup();
});
}
});
}
});
// Handle first-time install
browser.runtime.onInstalled.addListener((details) => {
if (details.reason === 'install') {
// Set up first-time install flag
browser.storage.sync.set({
isFirstInstall: true,
installDate: new Date().toISOString()
}, () => {
// Open options page for first-time setup
browser.runtime.openOptionsPage();
});
}
// Create context menus
browser.contextMenus.create({
id: 'save-to-trackeep',
title: 'Save to Trackeep',
contexts: ['page', 'link', 'selection', 'image', 'video']
});
// Quick save menu
browser.contextMenus.create({
id: 'quick-save-to-trackeep',
title: 'Quick Save to Trackeep',
contexts: ['page']
});
});
// Handle context menu click
browser.contextMenus.onClicked.addListener(async (info, tab) => {
if (info.menuItemId !== 'save-to-trackeep' && info.menuItemId !== 'quick-save-to-trackeep') return;
// Detect content type and get smart data
const smartData = await detectContentType(info, tab);
// Open popup with pre-filled data based on context
const url = info.linkUrl || info.srcUrl || tab?.url || '';
const title = tab?.title || '';
const selection = info.selectionText || '';
// Store temporary data for popup to read
browser.storage.local.set({
contextMenuData: {
url,
title,
selection,
timestamp: Date.now(),
isQuickSave: info.menuItemId === 'quick-save-to-trackeep',
smartData
}
}, () => {
// Open popup (or focus it if already open)
browser.action.openPopup();
});
});
// Smart content detection
async function detectContentType(info, tab) {
const url = info.linkUrl || info.srcUrl || tab?.url || '';
const title = tab?.title || '';
try {
const urlObj = new URL(url);
const domain = urlObj.hostname.toLowerCase();
// Video detection
if (url.includes('youtube.com/watch') || url.includes('youtu.be/')) {
return {
type: 'video',
platform: 'youtube',
suggestedTags: ['video', 'youtube', 'educational'],
autoTitle: extractYouTubeTitle(url) || title
};
}
if (url.includes('vimeo.com') || url.includes('dailymotion.com')) {
return {
type: 'video',
platform: domain.replace('.com', ''),
suggestedTags: ['video', domain.replace('.com', '')]
};
}
// Social media detection
if (domain.includes('twitter.com') || domain.includes('x.com')) {
return {
type: 'social',
platform: 'twitter',
suggestedTags: ['social', 'twitter', 'tweet']
};
}
if (domain.includes('linkedin.com')) {
return {
type: 'social',
platform: 'linkedin',
suggestedTags: ['social', 'linkedin', 'professional']
};
}
if (domain.includes('reddit.com')) {
return {
type: 'social',
platform: 'reddit',
suggestedTags: ['social', 'reddit', 'discussion']
};
}
// Development platforms
if (domain.includes('github.com')) {
return {
type: 'code',
platform: 'github',
suggestedTags: ['code', 'github', 'development', 'repository']
};
}
if (domain.includes('stackoverflow.com')) {
return {
type: 'code',
platform: 'stackoverflow',
suggestedTags: ['code', 'stackoverflow', 'programming', 'qa']
};
}
if (domain.includes('medium.com')) {
return {
type: 'article',
platform: 'medium',
suggestedTags: ['article', 'blog', 'medium']
};
}
// Documentation
if (domain.includes('docs.') || domain.includes('documentation')) {
return {
type: 'documentation',
suggestedTags: ['documentation', 'docs', 'reference']
};
}
// News sites
if (domain.includes('news.') || domain.includes('cnn.com') || domain.includes('bbc.com') ||
domain.includes('reuters.com') || domain.includes('washingtonpost.com')) {
return {
type: 'news',
suggestedTags: ['news', 'article', 'current-events']
};
}
// E-commerce
if (domain.includes('amazon.com') || domain.includes('ebay.com') ||
domain.includes('shopify.com') || domain.includes('etsy.com')) {
return {
type: 'shopping',
suggestedTags: ['shopping', 'product', 'ecommerce']
};
}
// Default detection
return {
type: 'general',
suggestedTags: ['bookmark', 'webpage']
};
} catch (e) {
return {
type: 'general',
suggestedTags: ['bookmark', 'webpage']
};
}
}
// Extract YouTube video title
function extractYouTubeTitle(url) {
try {
const urlObj = new URL(url);
const videoId = urlObj.searchParams.get('v');
if (videoId) {
// In a real implementation, you might fetch YouTube API
// For now, return null and let the page title be used
return null;
}
} catch (e) {
return null;
}
}
+11
View File
@@ -0,0 +1,11 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// Export the browser object for use in other scripts
if (typeof module !== 'undefined' && module.exports) {
module.exports = browser;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 769 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 346 B

+39
View File
@@ -0,0 +1,39 @@
{
"manifest_version": 3,
"name": "Trackeep Saver",
"version": "0.2.0",
"description": "Smart content detection and quick saving for Trackeep with auto-tagging and recommendations.",
"action": {
"default_popup": "popup.html",
"default_title": "Save to Trackeep"
},
"options_page": "options.html",
"background": {
"service_worker": "background.js"
},
"permissions": [
"storage",
"tabs",
"activeTab",
"contextMenus",
"scripting"
],
"host_permissions": [
"<all_urls>"
],
"commands": {
"quick-save": {
"suggested_key": {
"default": "Ctrl+Shift+S",
"mac": "Command+Shift+S"
},
"description": "Quick save current page to Trackeep"
}
},
"icons": {
"16": "icons/icon16.png",
"32": "icons/icon32.png",
"48": "icons/icon48.png",
"128": "icons/icon128.png"
}
}
+903
View File
@@ -0,0 +1,903 @@
<!DOCTYPE html>
<html lang="en" data-kb-theme="dark">
<head>
<meta charset="UTF-8" />
<title>Trackeep Saver Options</title>
<style>
/* Modern Inter Font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');
/* Modern CSS Variables - Proton Pass Inspired */
:root {
--bg-primary: #0f0f0f;
--bg-secondary: #1a1a1a;
--bg-tertiary: #262626;
--bg-hover: #2a2a2a;
--bg-active: #333333;
--border-primary: #2a2a2a;
--border-secondary: #333333;
--text-primary: #ffffff;
--text-secondary: #a3a3a3;
--text-tertiary: #737373;
--accent-primary: #3b82f6;
--accent-hover: #2563eb;
--success: #10b981;
--warning: #f59e0b;
--error: #ef4444;
--gradient-primary: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
--gradient-secondary: linear-gradient(135deg, #1a1a1a 0%, #262626 100%);
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.3);
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.4);
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.5);
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 12px;
--radius-xl: 16px;
}
* {
box-sizing: border-box;
}
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
margin: 0;
padding: 0;
min-height: 100vh;
background: var(--bg-primary);
color: var(--text-primary);
line-height: 1.6;
font-size: 14px;
color-scheme: dark;
}
/* Container */
.container {
max-width: 800px;
margin: 0 auto;
padding: 40px;
background: var(--bg-primary);
min-height: 100vh;
}
/* Header */
.header {
background: var(--gradient-secondary);
padding: 32px 20px 20px;
border-bottom: 1px solid var(--border-primary);
position: relative;
overflow: hidden;
}
.header::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: var(--gradient-primary);
}
.header-content {
max-width: 800px;
margin: 0 auto;
display: flex;
align-items: center;
gap: 16px;
}
.logo-container {
display: flex;
align-items: center;
gap: 16px;
}
.logo {
width: 48px;
height: 48px;
border-radius: var(--radius-lg);
background: var(--gradient-primary);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 700;
font-size: 20px;
box-shadow: var(--shadow-lg);
position: relative;
overflow: hidden;
}
.logo::after {
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: linear-gradient(45deg, transparent, rgba(255,255,255,0.1), transparent);
transform: rotate(45deg);
animation: shimmer 3s infinite;
}
@keyframes shimmer {
0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
}
.title-section {
flex: 1;
}
.title {
font-size: 28px;
font-weight: 700;
color: var(--text-primary);
margin: 0 0 4px 0;
}
.subtitle {
font-size: 14px;
color: var(--text-secondary);
margin: 0;
}
/* Main Content */
.main-content {
max-width: 800px;
margin: 0 auto;
}
/* Section */
.section {
background: var(--bg-secondary);
border-radius: var(--radius-lg);
padding: 32px;
margin-bottom: 24px;
border: 1px solid var(--border-primary);
box-shadow: var(--shadow-lg);
}
.section-header {
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-primary);
}
.section-icon {
width: 48px;
height: 48px;
background: var(--gradient-primary);
border-radius: var(--radius-md);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 16px;
}
.section-title {
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
margin: 0;
}
.section-description {
font-size: 14px;
color: var(--text-secondary);
margin: 4px 0 0 0;
}
/* Form Elements */
.form {
max-width: 100%;
}
.form-group {
margin-bottom: 20px;
}
.form-group:last-child {
margin-bottom: 0;
}
label {
display: block;
font-size: 13px;
font-weight: 500;
margin-bottom: 8px;
color: var(--text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* Install Welcome Styles */
.install-welcome {
padding: 40px 20px;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.install-card {
background: var(--bg-secondary);
border-radius: var(--radius-xl);
border: 1px solid var(--border-primary);
box-shadow: var(--shadow-lg);
max-width: 600px;
width: 100%;
overflow: hidden;
}
.install-header {
background: var(--gradient-primary);
padding: 32px;
text-align: center;
color: white;
}
.install-icon {
font-size: 48px;
margin-bottom: 16px;
}
.install-header h2 {
margin: 0 0 8px 0;
font-size: 24px;
font-weight: 700;
}
.install-header p {
margin: 0;
opacity: 0.9;
font-size: 16px;
}
.setup-steps {
padding: 32px;
}
.step {
display: flex;
gap: 20px;
margin-bottom: 32px;
align-items: flex-start;
}
.step-number {
background: var(--accent-primary);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 700;
font-size: 16px;
flex-shrink: 0;
}
.step-content h3 {
margin: 0 0 8px 0;
font-size: 18px;
font-weight: 600;
color: var(--text-primary);
}
.step-content p {
margin: 0 0 12px 0;
color: var(--text-secondary);
line-height: 1.5;
}
.security-note {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: var(--radius-md);
padding: 12px;
margin-top: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.security-note .icon {
color: var(--success);
flex-shrink: 0;
}
.security-note strong {
color: var(--success);
}
.main-options {
display: none;
}
/* Setup Form Styles */
.setup-form {
margin-top: 16px;
padding: 16px;
background: var(--bg-tertiary);
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
}
.setup-form .form-group {
margin-bottom: 16px;
}
.setup-form .form-group:last-child {
margin-bottom: 0;
}
.setup-form label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
margin-bottom: 6px;
}
.setup-form .form-input {
width: 100%;
padding: 12px 14px;
border-radius: var(--radius-sm);
border: 1px solid var(--border-primary);
background: var(--bg-secondary);
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
transition: all 0.2s ease;
outline: none;
}
.setup-form .form-input:focus {
border-color: var(--accent-primary);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.setup-form .input-help {
font-size: 12px;
color: var(--text-secondary);
margin-top: 4px;
}
.setup-actions {
display: flex;
gap: 12px;
margin-top: 24px;
width: 100%;
}
.setup-actions .btn {
flex: 1;
}
/* Button Groups */
.btn-group {
display: flex;
gap: 12px;
margin-top: 20px;
width: 100%;
}
.btn-group .btn {
flex: 1;
}
/* Full Width Elements */
.btn-block {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
/* Instructions */
.instructions {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
border: 1px solid var(--border-primary);
margin-top: 16px;
width: 100%;
}
input[type="url"],
input[type="password"] {
width: 100%;
padding: 14px 16px;
border-radius: var(--radius-md);
border: 1px solid var(--border-primary);
background: var(--bg-tertiary);
color: var(--text-primary);
font-size: 14px;
font-family: 'Inter', sans-serif;
font-weight: 400;
transition: all 0.2s ease;
outline: none;
}
input[type="text"]:focus,
input[type="url"]:focus,
input[type="password"]:focus {
border-color: var(--accent-primary);
background: var(--bg-hover);
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
/* Instructions */
.instructions {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
border: 1px solid var(--border-primary);
margin-top: 16px;
}
.instructions-title {
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 6px;
}
.instructions-list {
font-size: 13px;
color: var(--text-secondary);
margin: 0;
padding-left: 16px;
line-height: 1.6;
}
.instructions-list li {
margin-bottom: 4px;
}
.instructions-list li:last-child {
margin-bottom: 0;
}
/* Buttons */
.btn {
padding: 14px 24px;
border-radius: var(--radius-md);
border: none;
font-size: 14px;
font-weight: 500;
font-family: 'Inter', sans-serif;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
outline: none;
position: relative;
overflow: hidden;
}
.btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent);
transition: left 0.5s;
}
.btn:hover::before {
left: 100%;
}
.btn-primary {
background: var(--gradient-primary);
color: white;
box-shadow: var(--shadow-sm);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.btn-primary:active {
transform: translateY(0);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
/* Status Messages */
.status-message {
padding: 16px 20px;
border-radius: var(--radius-md);
font-size: 13px;
font-weight: 500;
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
animation: slideUp 0.3s ease;
}
@keyframes slideUp {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.status-message.success {
background: rgba(16, 185, 129, 0.1);
color: var(--success);
border: 1px solid rgba(16, 185, 129, 0.2);
}
.security-badge {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: var(--radius-sm);
padding: 6px 10px;
font-size: 11px;
font-weight: 600;
color: var(--success);
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 8px;
}
.security-badge .icon {
flex-shrink: 0;
}
.connection-status {
background: var(--bg-tertiary);
border-radius: var(--radius-md);
padding: 16px;
margin-top: 20px;
border: 1px solid var(--border-primary);
}
.connection-status.connected {
border-color: var(--success);
background: rgba(16, 185, 129, 0.05);
}
.connection-status.error {
border-color: var(--error);
background: rgba(239, 68, 68, 0.05);
}
.status-message.info {
background: rgba(59, 130, 246, 0.1);
color: var(--accent-primary);
border: 1px solid rgba(59, 130, 246, 0.2);
}
/* Code styling */
code {
background: var(--bg-tertiary);
padding: 3px 8px;
border-radius: var(--radius-sm);
font-size: 12px;
color: var(--text-primary);
border: 1px solid var(--border-primary);
font-family: 'Fira Code', 'Monaco', 'Consolas', monospace;
}
/* Icon System */
.icon {
width: 16px;
height: 16px;
display: inline-block;
vertical-align: middle;
transition: all 0.2s ease;
fill: white;
stroke: white;
}
.icon-sm {
width: 12px;
height: 12px;
fill: white;
stroke: white;
}
.icon-lg {
width: 20px;
height: 20px;
fill: white;
stroke: white;
}
.icon-xl {
width: 24px;
height: 24px;
fill: white;
stroke: white;
}
/* External SVG Icons */
img.icon {
filter: brightness(0) invert(1);
}
img.icon-sm {
filter: brightness(0) invert(1);
}
img.icon-lg {
filter: brightness(0) invert(1);
}
img.icon-xl {
filter: brightness(0) invert(1);
}
/* Icon animations */
.icon-spin {
animation: spin 1s linear infinite;
}
.icon-pulse {
animation: pulse 2s ease-in-out infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.8; transform: scale(1.05); }
}
/* Enhanced button icons */
.btn .icon {
transition: transform 0.2s ease;
}
.btn:hover .icon {
transform: scale(1.1);
}
.btn:active .icon {
transform: scale(0.95);
}
/* Section icon enhancements */
.section-icon {
transition: all 0.3s ease;
}
.section:hover .section-icon {
transform: scale(1.05) rotate(5deg);
box-shadow: var(--shadow-md);
}
/* Responsive */
@media (max-width: 640px) {
.container {
padding: 20px 16px;
}
.section {
padding: 20px;
}
.header {
padding: 24px 16px 16px;
}
.title {
font-size: 24px;
}
}
</style>
</head>
<body>
<!-- First-time Install Welcome -->
<div id="installWelcome" class="install-welcome" style="display: none;">
<div class="install-card">
<div class="install-header">
<div class="install-icon">🎉</div>
<h2>Welcome to Trackeep Saver!</h2>
<p>Let's set up your connection to get started.</p>
</div>
<div class="setup-steps">
<div class="step">
<div class="step-number">1</div>
<div class="step-content">
<h3>Get Your API Key</h3>
<p>Log into your Trackeep account and generate an API key in Settings → Security.</p>
<div class="security-note">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<strong>Secure:</strong> API keys are safer than JWT tokens and can be revoked anytime.
</div>
</div>
</div>
<div class="step">
<div class="step-number">2</div>
<div class="step-content">
<h3>Configure Connection</h3>
<p>Enter your Trackeep URL and API key below.</p>
<div class="setup-form">
<div class="form-group">
<label for="setupApiUrl">Trackeep URL</label>
<input type="url" id="setupApiUrl" placeholder="https://your-trackeep.com/api/v1" class="form-input" />
<div class="input-help">Your Trackeep instance API URL</div>
</div>
<div class="form-group">
<label for="setupApiKey">API Key</label>
<input type="password" id="setupApiKey" placeholder="tk_..." class="form-input" />
<div class="input-help">
<div class="security-badge">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<span>Secure API Key</span>
</div>
More secure than JWT tokens, revocable anytime
</div>
</div>
</div>
</div>
</div>
<div class="step">
<div class="step-number">3</div>
<div class="step-content">
<h3>
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Test" class="icon" style="width: 20px; height: 20px; margin-right: 8px;" />
Test Connection
</h3>
<p>Verify your connection works before saving bookmarks.</p>
<div id="setupConnectionStatus" class="connection-status" style="display: none;">
<div class="status-content">
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Status" class="icon" style="width: 16px; height: 16px;" />
<div>
<strong id="setupStatusTitle">Testing Connection...</strong>
<p id="setupStatusMessage">Please wait</p>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="setup-actions">
<button id="testSetupConnectionBtn" class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 8l-3-3-6-6z"></path>
<path d="M21 12v.01M5 12c0-4.41 3.58-8 8s8 3.58 8 8-3.58 8-8-8z"></path>
</svg>
Test Connection
</button>
<button id="completeSetupBtn" class="btn btn-secondary">
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Complete" class="icon" style="width: 16px; height: 16px;" />
Complete Setup
</button>
</div>
</div>
</div>
</div>
<!-- Main Options -->
<div id="mainOptions" class="container">
<header class="header">
<div class="header-content">
<div class="logo-container">
<div class="logo">
<img src="https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/trackeep.svg" alt="Trackeep" class="icon-xl" style="width: 24px; height: 24px; fill: white;" />
</div>
<div>
<h1 class="title">Trackeep Saver</h1>
<p class="subtitle">Browser Extension Settings</p>
</div>
</div>
</div>
</header>
<main class="main-content">
<section class="section">
<div class="section-header">
<div class="section-icon">
<img src="https://www.svgrepo.com/show/505495/settings.svg" alt="Settings" class="icon-xl" style="width: 24px; height: 24px;" />
</div>
<h2 class="section-title">Connection Settings</h2>
<p class="section-description">Configure your Trackeep connection and API key</p>
</div>
<form id="optionsForm" class="form">
<div class="form-group">
<label for="trackeepApiUrl">Trackeep URL</label>
<input type="url" id="trackeepApiUrl" placeholder="https://your-trackeep.com/api/v1" class="form-input" />
<div class="input-help">Your Trackeep instance API URL</div>
</div>
<div class="form-group">
<label for="trackeepApiKey">API Key</label>
<input type="password" id="trackeepApiKey" placeholder="tk_..." class="form-input" />
<div class="input-help">
<div class="security-badge">
<img src="https://www.svgrepo.com/show/381193/secure-shield-password-protect-safe.svg" alt="Security" class="icon" style="width: 16px; height: 16px;" />
<span>Secure API Key</span>
</div>
More secure than JWT tokens, revocable anytime
</div>
</div>
<div id="connectionStatus" class="connection-status" style="display: none;">
<div class="status-content">
<img src="https://www.svgrepo.com/show/448375/connection-gateway.svg" alt="Status" class="icon" style="width: 16px; height: 16px;" />
<div>
<strong id="statusTitle">Testing Connection...</strong>
<p id="statusMessage">Please wait</p>
</div>
</div>
</div>
</form>
<div class="btn-group">
<button id="testConnectionBtn" class="btn btn-primary">
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 11l3 3L22 8l-3-3-6-6z"></path>
<path d="M21 12v.01M5 12c0-4.41 3.58-8 8s8 3.58 8 8-3.58 8-8-8z"></path>
</svg>
Test Connection
</button>
<button id="generateKeyBtn" class="btn btn-secondary">
<img src="https://www.svgrepo.com/show/532805/file-shredder.svg" alt="Generate" class="icon" style="width: 16px; height: 16px;" />
Generate API Key
</button>
</div>
<div class="instructions">
<div class="instructions-title">
<img src="https://www.svgrepo.com/show/447845/website-click.svg" alt="Instructions" class="icon" style="width: 16px; height: 16px;" />
<span>How to get your API key:</span>
</div>
<ol class="instructions-list">
<li>Log into your Trackeep account</li>
<li>Go to Settings → Security</li>
<li>Click "Generate New API Key"</li>
<li>Copy the generated key (starts with <code>tk_</code>)</li>
<li>Paste the key in the field above</li>
<li><strong>API keys are more secure than JWT tokens</strong> - they can be revoked anytime and have limited permissions</li>
</ol>
</div>
<button class="btn btn-primary" id="saveBtn" style="margin-top: 24px;">
<img src="https://www.svgrepo.com/show/521819/save.svg" alt="Save" class="icon" style="width: 16px; height: 16px;" />
<span>Save Settings</span>
</button>
<div id="statusMessage" class="status-message" style="display: none;"></div>
</section>
</main>
</div>
<script src="options.js"></script>
</body>
</html>
+374
View File
@@ -0,0 +1,374 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
const apiBaseUrlInput = document.getElementById('trackeepApiUrl');
const apiKeyInput = document.getElementById('trackeepApiKey');
const testConnectionBtn = document.getElementById('testConnectionBtn');
const generateKeyBtn = document.getElementById('generateKeyBtn');
const saveBtn = document.getElementById('saveBtn');
const statusMessageEl = document.getElementById('statusMessage');
const connectionStatusEl = document.getElementById('connectionStatus');
const statusTitleEl = document.getElementById('statusTitle');
const statusTextEl = document.getElementById('statusMessage');
const installWelcomeEl = document.getElementById('installWelcome');
const mainOptionsEl = document.getElementById('mainOptions');
function showMessage(message, type = 'info', duration = 5000) {
statusMessageEl.textContent = message;
statusMessageEl.className = `status-message ${type}`;
statusMessageEl.style.display = 'flex';
if (duration > 0) {
setTimeout(() => {
statusMessageEl.style.display = 'none';
}, duration);
}
}
function hideMessage() {
statusMessageEl.style.display = 'none';
}
function showConnectionStatus(title, message, type = 'info') {
connectionStatusEl.style.display = 'block';
statusTitleEl.textContent = title;
statusTextEl.textContent = message;
connectionStatusEl.className = `connection-status ${type}`;
}
function hideConnectionStatus() {
connectionStatusEl.style.display = 'none';
}
function setButtonLoading(button, loading = true) {
if (loading) {
button.disabled = true;
const originalContent = button.innerHTML;
button.dataset.originalContent = originalContent;
button.innerHTML = `
<svg class="icon icon-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>Saving...</span>
`;
} else {
button.disabled = false;
if (button.dataset.originalContent) {
button.innerHTML = button.dataset.originalContent;
delete button.dataset.originalContent;
}
}
}
function detectAndPrefillApiBaseUrl(callback) {
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (!tab || !tab.url) {
if (callback) callback();
return;
}
try {
const url = new URL(tab.url);
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
if (isTrackeepDomain && (url.protocol === 'https:' || url.protocol === 'http:')) {
const candidate = `${url.origin}/api/v1`;
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = candidate;
}
if (callback) callback();
});
} else {
// Fallback to localhost if nothing set
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = 'http://localhost:8080/api/v1';
}
if (callback) callback();
});
}
} catch (e) {
if (callback) callback();
}
});
}
function loadSettings() {
browser.storage.sync.get(['trackeepApiBaseUrl', 'trackeepApiKey', 'isFirstInstall'], (items) => {
// Handle first-time install
if (items.isFirstInstall) {
installWelcomeEl.style.display = 'flex';
mainOptionsEl.style.display = 'none';
} else {
installWelcomeEl.style.display = 'none';
mainOptionsEl.style.display = 'block';
}
// Load saved settings
if (items.trackeepApiBaseUrl) {
apiBaseUrlInput.value = items.trackeepApiBaseUrl;
}
if (items.trackeepApiKey) {
apiKeyInput.value = items.trackeepApiKey;
}
// Auto-detect API URL if empty
if (!items.trackeepApiBaseUrl) {
detectAndPrefillApiBaseUrl();
}
});
}
function saveSettings() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
const apiKey = apiKeyInput.value.trim();
if (!apiBaseUrl) {
showMessage('API base URL is required.', 'error');
return;
}
if (!apiKey) {
showMessage('API key is required.', 'error');
return;
}
if (!apiKey.startsWith('tk_')) {
showMessage('API key should start with "tk_"', 'error');
return;
}
setButtonLoading(saveBtn, true);
showMessage('Saving settings...', 'info', 0);
browser.storage.sync.set({
trackeepApiBaseUrl: apiBaseUrl,
trackeepApiKey: apiKey,
isFirstInstall: false
}, () => {
setButtonLoading(saveBtn, false);
showMessage('Settings saved successfully!', 'success');
});
}
async function testConnection() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
const apiKey = apiKeyInput.value.trim();
if (!apiBaseUrl || !apiKey) {
showConnectionStatus('Connection Failed', 'Please enter both URL and API key', 'error');
return;
}
showConnectionStatus('Testing Connection', 'Connecting to your Trackeep instance...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/me`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
showConnectionStatus('Connection Successful', `Connected as ${data.username || 'user'}. API key is valid!`, 'success');
// Hide success message after 3 seconds
setTimeout(() => {
hideConnectionStatus();
}, 3000);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showConnectionStatus('Connection Failed', `Error: ${error.message}`, 'error');
}
}
async function generateApiKey() {
const apiBaseUrl = apiBaseUrlInput.value.trim();
if (!apiBaseUrl) {
showMessage('Please enter API URL first', 'error');
return;
}
showConnectionStatus('Generating API Key', 'Opening Trackeep to generate new API key...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/generate-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
if (data.api_key) {
apiKeyInput.value = data.api_key;
showConnectionStatus('API Key Generated', 'New API key generated and copied to clipboard!', 'success');
// Copy to clipboard
navigator.clipboard.writeText(data.api_key);
// Hide success message after 3 seconds
setTimeout(() => {
hideConnectionStatus();
}, 3000);
} else {
throw new Error('No API key in response');
}
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showConnectionStatus('Generation Failed', `Error: ${error.message}`, 'error');
}
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
detectAndPrefillApiBaseUrl(() => {
loadSettings();
});
// Event listeners for main options
saveBtn.addEventListener('click', (e) => {
e.preventDefault();
saveSettings();
});
testConnectionBtn.addEventListener('click', (e) => {
e.preventDefault();
testConnection();
});
generateKeyBtn.addEventListener('click', (e) => {
e.preventDefault();
generateApiKey();
});
// Event listeners for setup form
const testSetupConnectionBtn = document.getElementById('testSetupConnectionBtn');
const completeSetupBtn = document.getElementById('completeSetupBtn');
const getStartedBtn = document.getElementById('getStartedBtn');
if (testSetupConnectionBtn) {
testSetupConnectionBtn.addEventListener('click', (e) => {
e.preventDefault();
testSetupConnection();
});
}
if (completeSetupBtn) {
completeSetupBtn.addEventListener('click', (e) => {
e.preventDefault();
completeSetup();
});
}
if (getStartedBtn) {
getStartedBtn.addEventListener('click', (e) => {
e.preventDefault();
// Hide welcome and show main options with setup form
document.getElementById('installWelcome').style.display = 'none';
document.getElementById('mainOptions').style.display = 'block';
});
}
});
// Test connection from setup form
async function testSetupConnection() {
const apiBaseUrl = document.getElementById('setupApiUrl').value.trim();
const apiKey = document.getElementById('setupApiKey').value.trim();
if (!apiBaseUrl || !apiKey) {
showSetupConnectionStatus('Connection Failed', 'Please enter both URL and API key', 'error');
return;
}
showSetupConnectionStatus('Testing Connection', 'Connecting to your Trackeep instance...', 'info');
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/auth/me`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
}
});
if (response.ok) {
const data = await response.json();
showSetupConnectionStatus('Connection Successful', `Connected as ${data.username || 'user'}. API key is valid!`, 'success');
// Copy values to main form
document.getElementById('trackeepApiUrl').value = apiBaseUrl;
document.getElementById('trackeepApiKey').value = apiKey;
// Hide success message after 3 seconds
setTimeout(() => {
hideSetupConnectionStatus();
}, 3000);
} else {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (error) {
showSetupConnectionStatus('Connection Failed', `Error: ${error.message}`, 'error');
}
}
// Complete setup
function completeSetup() {
const apiBaseUrl = document.getElementById('setupApiUrl').value.trim();
const apiKey = document.getElementById('setupApiKey').value.trim();
if (!apiBaseUrl || !apiKey) {
showMessage('Please fill in both URL and API key', 'error');
return;
}
// Save settings
browser.storage.sync.set({
trackeepApiBaseUrl: apiBaseUrl,
trackeepApiKey: apiKey,
isFirstInstall: false
}, () => {
showMessage('Setup completed successfully!', 'success');
// Switch to main options view
document.getElementById('installWelcome').style.display = 'none';
document.getElementById('mainOptions').style.display = 'block';
// Load settings in main form
document.getElementById('trackeepApiUrl').value = apiBaseUrl;
document.getElementById('trackeepApiKey').value = apiKey;
});
}
// Setup connection status functions
function showSetupConnectionStatus(title, message, type = 'info') {
const statusEl = document.getElementById('setupConnectionStatus');
const titleEl = document.getElementById('setupStatusTitle');
const messageEl = document.getElementById('setupStatusMessage');
statusEl.style.display = 'block';
titleEl.textContent = title;
messageEl.textContent = message;
statusEl.className = `connection-status ${type}`;
}
function hideSetupConnectionStatus() {
document.getElementById('setupConnectionStatus').style.display = 'none';
}
File diff suppressed because it is too large Load Diff
+515
View File
@@ -0,0 +1,515 @@
/* global chrome, browser */
// Browser compatibility polyfill
if (typeof browser === 'undefined' && typeof chrome !== 'undefined') {
browser = chrome;
}
// DOM Elements
const statusIndicatorEl = document.getElementById('statusIndicator');
const statusTextEl = document.getElementById('statusText');
const statusMessageEl = document.getElementById('statusMessage');
const openOptionsBtn = document.getElementById('openOptions');
// Tab elements
const tabBtns = document.querySelectorAll('.tab');
const tabContents = document.querySelectorAll('.tab-content');
// Bookmark elements
const bookmarkTitleInput = document.getElementById('bookmarkTitle');
const bookmarkUrlInput = document.getElementById('bookmarkUrl');
const bookmarkDescriptionInput = document.getElementById('bookmarkDescription');
const bookmarkTagsInput = document.getElementById('bookmarkTags');
const bookmarkPublicInput = document.getElementById('bookmarkPublic');
const saveBookmarkBtn = document.getElementById('saveBookmarkBtn');
// File elements
const fileInput = document.getElementById('fileInput');
const fileDescriptionInput = document.getElementById('fileDescription');
const uploadFileBtn = document.getElementById('uploadFileBtn');
// Smart suggestion elements
const suggestedTagsContainer = document.getElementById('suggestedTags');
const contentTypeIndicator = document.getElementById('contentTypeIndicator');
const quickSaveBtn = document.getElementById('quickSaveBtn');
let trackeepConfig = {
apiBaseUrl: '',
authToken: ''
};
let smartData = null;
let isQuickSaveMode = false;
// Tab switching functionality
function initTabs() {
tabBtns.forEach(btn => {
btn.addEventListener('click', () => {
const targetTab = btn.dataset.tab;
// Update button states
tabBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
// Update content visibility
tabContents.forEach(content => {
content.classList.remove('active');
if (content.id === `${targetTab}-tab`) {
content.classList.add('active');
}
});
});
});
}
// Status management
function updateStatus(text, type = 'info') {
statusTextEl.textContent = text;
statusIndicatorEl.className = 'status-indicator';
if (type === 'success') {
statusIndicatorEl.classList.add('connected');
} else if (type === 'error') {
statusIndicatorEl.classList.add('error');
}
}
function showMessage(message, type = 'info', duration = 5000) {
statusMessageEl.textContent = message;
statusMessageEl.className = `status-message ${type}`;
statusMessageEl.style.display = 'flex';
// Auto-hide after duration
if (duration > 0) {
setTimeout(() => {
statusMessageEl.style.display = 'none';
}, duration);
}
}
function hideMessage() {
statusMessageEl.style.display = 'none';
}
// Loading states
function setButtonLoading(button, loading = true) {
if (loading) {
button.disabled = true;
const originalContent = button.innerHTML;
button.dataset.originalContent = originalContent;
button.innerHTML = `
<svg class="icon icon-spin" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 12a9 9 0 1 1-6.219-8.56"/>
</svg>
<span>Processing...</span>
`;
} else {
button.disabled = false;
if (button.dataset.originalContent) {
button.innerHTML = button.dataset.originalContent;
delete button.dataset.originalContent;
}
}
}
function disableForms(disabled) {
const elements = [
bookmarkTitleInput, bookmarkUrlInput, bookmarkDescriptionInput,
bookmarkTagsInput, bookmarkPublicInput, saveBookmarkBtn,
fileInput, fileDescriptionInput, uploadFileBtn
];
elements.forEach(el => {
if (el) el.disabled = disabled;
});
}
function loadConfig(callback) {
browser.storage.sync.get(['trackeepApiBaseUrl', 'trackeepAuthToken'], (items) => {
const apiBaseUrl = (items.trackeepApiBaseUrl || '').trim();
const authToken = (items.trackeepAuthToken || '').trim();
trackeepConfig = { apiBaseUrl, authToken };
if (!apiBaseUrl || !authToken) {
updateStatus('Configuration required', 'error');
showMessage('Configure API URL and token in Options to enable saving.', 'error');
disableForms(true);
} else {
updateStatus(`Connected to ${apiBaseUrl}`, 'success');
hideMessage();
disableForms(false);
}
if (typeof callback === 'function') {
callback();
}
});
}
function detectTrackeepDomain(callback) {
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (!tab || !tab.url) {
if (callback) callback();
return;
}
try {
const url = new URL(tab.url);
const isTrackeepDomain = url.hostname.includes('trackeep') || url.hostname === 'localhost';
if (isTrackeepDomain && url.protocol === 'https:') {
const candidate = `${url.origin}/api/v1`;
browser.storage.sync.get(['trackeepApiBaseUrl'], (items) => {
if (!items.trackeepApiBaseUrl) {
browser.storage.sync.set({ trackeepApiBaseUrl: candidate }, () => {
console.log('Auto-detected Trackeep API URL:', candidate);
if (callback) callback();
});
} else {
if (callback) callback();
}
});
} else {
if (callback) callback();
}
} catch (e) {
if (callback) callback();
}
});
}
function initActiveTab() {
browser.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const tab = tabs && tabs[0];
if (!tab) return;
browser.storage.local.get(['contextMenuData'], (items) => {
const ctx = items.contextMenuData;
if (ctx && ctx.timestamp && Date.now() - ctx.timestamp < 5000) {
// Use context menu data
smartData = ctx.smartData || null;
isQuickSaveMode = ctx.isQuickSave || false;
if (ctx.url && !bookmarkUrlInput.value) {
bookmarkUrlInput.value = ctx.url;
}
if (ctx.title && !bookmarkTitleInput.value) {
bookmarkTitleInput.value = ctx.title;
}
if (ctx.selection && !bookmarkDescriptionInput.value) {
bookmarkDescriptionInput.value = ctx.selection;
}
// Apply smart suggestions
if (smartData) {
applySmartSuggestions(smartData);
}
// Handle quick save mode
if (isQuickSaveMode) {
handleQuickSave();
}
browser.storage.local.remove(['contextMenuData']);
} else {
// Regular tab detection
detectAndApplySmartData(tab);
if (tab.title && !bookmarkTitleInput.value) {
bookmarkTitleInput.value = tab.title;
}
if (tab.url && !bookmarkUrlInput.value) {
bookmarkUrlInput.value = tab.url;
}
}
});
});
}
// Smart data detection for regular tab
async function detectAndApplySmartData(tab) {
try {
const info = { linkUrl: tab.url, srcUrl: tab.url };
smartData = await detectContentType(info, tab);
if (smartData) {
applySmartSuggestions(smartData);
}
} catch (e) {
console.error('Smart detection failed:', e);
}
}
// Apply smart suggestions to UI
function applySmartSuggestions(data) {
// Show content type indicator
if (contentTypeIndicator) {
const typeColors = {
video: '#ff0000',
social: '#1da1f2',
code: '#0969da',
article: '#ff6900',
documentation: '#6f42c1',
news: '#ff4500',
shopping: '#ff9500',
general: '#6b7280'
};
const typeIcons = {
video: '🎥',
social: '💬',
code: '💻',
article: '📝',
documentation: '📚',
news: '📰',
shopping: '🛒',
general: '🔗'
};
contentTypeIndicator.innerHTML = `
<span style="color: ${typeColors[data.type] || typeColors.general}; font-weight: 600;">
${typeIcons[data.type] || typeIcons.general} ${data.type.charAt(0).toUpperCase() + data.type.slice(1)}
</span>
${data.platform ? `<span style="color: #6b7280; font-size: 0.85em; margin-left: 8px;">• ${data.platform}</span>` : ''}
`;
contentTypeIndicator.style.display = 'inline-block';
}
// Show suggested tags
if (suggestedTagsContainer && data.suggestedTags) {
suggestedTagsContainer.innerHTML = '';
data.suggestedTags.forEach(tag => {
const tagEl = document.createElement('span');
tagEl.className = 'suggested-tag';
tagEl.textContent = tag;
tagEl.onclick = () => addSuggestedTag(tag);
suggestedTagsContainer.appendChild(tagEl);
});
suggestedTagsContainer.style.display = 'flex';
}
}
// Add suggested tag to input
function addSuggestedTag(tag) {
const currentTags = bookmarkTagsInput.value
.split(',')
.map(t => t.trim())
.filter(t => t);
if (!currentTags.includes(tag)) {
currentTags.push(tag);
bookmarkTagsInput.value = currentTags.join(', ');
}
}
// Handle quick save
function handleQuickSave() {
if (isQuickSaveMode && smartData) {
// Auto-fill with smart data and save immediately
if (smartData.suggestedTags && !bookmarkTagsInput.value) {
bookmarkTagsInput.value = smartData.suggestedTags.join(', ');
}
// Auto-save after a short delay
setTimeout(() => {
if (bookmarkUrlInput.value && bookmarkTitleInput.value) {
saveBookmark(new Event('submit'));
}
}, 500);
}
}
async function saveBookmark(event) {
event.preventDefault();
hideMessage();
const { apiBaseUrl, authToken } = trackeepConfig;
if (!apiBaseUrl || !authToken) {
showMessage('Missing API URL or auth token. Open options first.', 'error');
return;
}
const url = bookmarkUrlInput.value.trim();
if (!url) {
showMessage('URL is required.', 'error');
return;
}
const title = bookmarkTitleInput.value.trim() || url;
const description = bookmarkDescriptionInput.value.trim();
const tagsRaw = bookmarkTagsInput.value.trim();
const isPublic = !!bookmarkPublicInput.checked;
const tags = tagsRaw
? tagsRaw.split(',').map((t) => t.trim()).filter(Boolean)
: [];
const payload = {
title,
url,
description,
tags,
is_public: isPublic
};
setButtonLoading(saveBookmarkBtn, true);
showMessage('Saving bookmark...', 'info', 0);
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/bookmarks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(payload)
});
if (!response.ok) {
let errorMessage = `Failed to save bookmark (status ${response.status})`;
try {
const data = await response.json();
if (data && data.error) {
errorMessage = data.error;
}
} catch (_) {
// ignore JSON parse errors
}
throw new Error(errorMessage);
}
showMessage(`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"/>
</svg>
Bookmark saved successfully!
`, 'success');
// Clear form after successful save
setTimeout(() => {
bookmarkDescriptionInput.value = '';
bookmarkTagsInput.value = '';
bookmarkPublicInput.checked = false;
}, 2000);
} catch (err) {
console.error('Error saving bookmark', err);
showMessage(err && err.message ? err.message : 'Failed to save bookmark.', 'error');
} finally {
setButtonLoading(saveBookmarkBtn, false);
}
}
async function uploadFile(event) {
event.preventDefault();
hideMessage();
const { apiBaseUrl, authToken } = trackeepConfig;
if (!apiBaseUrl || !authToken) {
showMessage('Missing API URL or auth token. Open options first.', 'error');
return;
}
const file = fileInput.files && fileInput.files[0];
if (!file) {
showMessage('Please choose a file to upload.', 'error');
return;
}
const description = fileDescriptionInput.value.trim();
const formData = new FormData();
formData.append('file', file, file.name);
if (description) {
formData.append('description', description);
}
setButtonLoading(uploadFileBtn, true);
showMessage('Uploading file...', 'info', 0);
try {
const base = apiBaseUrl.replace(/\/$/, '');
const response = await fetch(`${base}/files/upload`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`
},
body: formData
});
if (!response.ok) {
let errorMessage = `Failed to upload file (status ${response.status})`;
try {
const data = await response.json();
if (data && data.error) {
errorMessage = data.error;
}
} catch (_) {
// ignore JSON parse errors
}
throw new Error(errorMessage);
}
showMessage(`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="20,6 9,17 4,12"/>
</svg>
File uploaded successfully!
`, 'success');
// Clear form after successful upload
setTimeout(() => {
fileInput.value = '';
fileDescriptionInput.value = '';
}, 2000);
} catch (err) {
console.error('Error uploading file', err);
showMessage(err && err.message ? err.message : 'Failed to upload file.', 'error');
} finally {
setButtonLoading(uploadFileBtn, false);
}
}
function openOptions() {
if (browser.runtime.openOptionsPage) {
browser.runtime.openOptionsPage();
} else {
window.open(browser.runtime.getURL('options.html'));
}
}
// Initialize everything when DOM is loaded
document.addEventListener('DOMContentLoaded', () => {
// Initialize tabs
initTabs();
// Event listeners
openOptionsBtn.addEventListener('click', openOptions);
quickSaveBtn.addEventListener('click', handleQuickSave);
saveBookmarkBtn.addEventListener('click', (e) => {
e.preventDefault();
saveBookmark(e);
});
uploadFileBtn.addEventListener('click', (e) => {
e.preventDefault();
uploadFile(e);
});
// Keyboard shortcut for quick save
document.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.shiftKey && e.key === 'S') {
e.preventDefault();
handleQuickSave();
}
});
// Initialize configuration and active tab
detectTrackeepDomain(() => {
loadConfig(() => {
initActiveTab();
});
});
});