7 Incheckningar 8283bf214f ... 7f18c6c57b

Upphovsman SHA1 Meddelande Datum
  Brandon Wong 7f18c6c57b manually added channel categories; formatted code 1 år sedan
  Brandon Wong c42fa43d3c feat: add save channel functionality with curl command generation 1 år sedan
  Brandon Wong 1d54c30a54 feat: explore adding open tabs to playlist export 1 år sedan
  Brandon Wong 8fc3478533 feat: add reference, to rewatch, and to save tags to history view 1 år sedan
  Brandon Wong a217556e4d adjusted codemcp instructions; adjusted to-do list 1 år sedan
  Brandon Wong 57a28fb71d feat: add visual styling for non-contiguous done videos 1 år sedan
  Brandon Wong 6dc199914c feat: add done status toggle to video action menus 1 år sedan

+ 45 - 0
2025-05-31-codemcp-feat-add-done-status-toggle-to-video-action-menus.md

@@ -0,0 +1,45 @@
+# 2025-05-31 CodeMCP: Add Done Status Toggle to Video Action Menus
+
+## User Request
+in the popup menu, in the main view, each video has an actions menu. add an option to mark that video as "done" (if it's anything other than "done"), or to remove the status if it is "done". add this option to the sub-menus of the playlist-specific view as well.
+
+## Implementation Summary
+
+Added functionality to toggle the "done" status of videos in both the main playlist view and the individual playlist view. The implementation includes:
+
+### Changes Made
+
+1. **Added `toggleVideoDoneStatus` method** in `popup/popup.js`:
+   - Toggles video status between "done" and undefined (not done)
+   - Updates playlist data in browser storage
+   - Refreshes display after changes
+
+2. **Added `isVideoDone` helper method** in `popup/popup.js`:
+   - Checks if a specific video has "done" status
+
+3. **Added `toggleVideoDoneButton` handler** in `popup/popup.js`:
+   - Click handler that calls `toggleVideoDoneStatus` and closes menus
+   - CSP-compliant event binding
+
+4. **Updated `updatePlaylistsForDisplay` and `updateCurrentPlaylistVideos` methods**:
+   - Pre-compute `doneButtonText` property for CSP compliance
+   - Avoids JavaScript expressions in HTML templates
+
+5. **Updated HTML templates** in `popup/popup.html`:
+   - Added done toggle button to both main view and playlist-specific view more menus
+   - Used pre-computed `doneButtonText` property instead of JavaScript expressions
+   - Maintains CSP compliance by avoiding expressions like `video.status === 'done' ? ... : ...`
+
+### Key Features
+
+- **Toggle Functionality**: Videos can be marked as "done" or have their done status removed
+- **Dual View Support**: Available in both truncated main view and full playlist view
+- **CSP Compliance**: All Alpine.js code follows CSP restrictions with no JavaScript expressions in HTML
+- **Consistent UI**: Integrates seamlessly with existing more menu system
+- **Storage Integration**: Persists changes to browser storage and updates all views
+
+### Technical Details
+
+The done status is stored as a `status: "done"` property on video objects. When removing done status, the property is deleted rather than set to a different value, maintaining consistency with the existing codebase logic that checks for `video.status === "done"`.
+
+The implementation maintains full CSP compliance by pre-computing button text in JavaScript methods rather than using expressions in HTML attributes.

+ 36 - 0
2025-05-31-codemcp-feat-add-visual-styling-for-non-contiguous-done-vi.md

@@ -0,0 +1,36 @@
+# 2025-05-31 - Feature: Add visual styling for non-contiguous done videos
+
+## User Request
+> in the popup menu, there may be videos that are marked as "done" that come after videos that are not marked as done. even though they are not truncated with the contiguous "done" videos, they should be visually marked somehow. grey them out (very slightly) in the main view and in the individual playlist view. leave everything else the same.
+
+## Implementation Summary
+
+Added visual styling for videos that are marked as "done" but appear after non-done videos in the playlist. These non-contiguous done videos are now visually distinguished with subtle greying.
+
+### Changes Made
+
+1. **JavaScript Logic (`popup/popup.js`)**:
+   - Added `isNonContiguousDone(playlistName, videoIndex)` method to identify done videos that come after non-done videos
+   - Updated `updatePlaylistsForDisplay()` to include `isNonContiguousDone` property in visible videos
+   - Updated `updateCurrentPlaylistVideos()` to include `isNonContiguousDone` property in current playlist videos
+   - Modified `videoItemClass` binding to apply `non-contiguous-done-video` CSS class
+
+2. **CSS Styling (`popup/popup.css`)**:
+   - Added `.non-contiguous-done-video` class with subtle opacity reduction (0.7) and light grey background
+   - Added `.non-contiguous-done-video .video-title` with greyed text color (#666)
+
+### Technical Details
+
+The `isNonContiguousDone` method checks if:
+1. The video at the given index is marked as "done"
+2. There exists at least one non-done video before it in the playlist
+
+This ensures only truly non-contiguous done videos are styled, maintaining the existing behavior for contiguous done videos at the beginning of playlists.
+
+The styling is applied in both:
+- Main playlists view (truncated display)
+- Individual playlist view (full display)
+
+### Files Modified
+- `popup/popup.js` - Added logic for identifying non-contiguous done videos
+- `popup/popup.css` - Added subtle visual styling for non-contiguous done videos

+ 62 - 0
2025-06-01-codemcp-add-history-tags.md

@@ -0,0 +1,62 @@
+# Add History Tags Feature - 2025-06-01
+
+## User Request
+> in the history view of the popup menu, there are toggleable tags for each video. currently only "notable" is an option; add three more: "reference", "to rewatch", and "to save".
+
+## Implementation Summary
+
+Modified the history view in the Firefox extension popup to add three new toggleable tags alongside the existing "notable" tag.
+
+### Changes Made
+
+1. **popup/popup.html**: Added three new tag chip buttons in the history view:
+   - "reference" tag
+   - "to rewatch" tag
+   - "to save" tag
+
+The tags were added to the `.history-tags` div on lines 142-175, following the same pattern as the existing "notable" tag:
+
+```html
+<button
+  class="tag-chip"
+  :data-video-id="videoHistory.videoId"
+  data-tag="reference"
+  x-bind="tagChip"
+>
+  reference
+</button>
+<button
+  class="tag-chip"
+  :data-video-id="videoHistory.videoId"
+  data-tag="to rewatch"
+  x-bind="tagChip"
+>
+  to rewatch
+</button>
+<button
+  class="tag-chip"
+  :data-video-id="videoHistory.videoId"
+  data-tag="to save"
+  x-bind="tagChip"
+>
+  to save
+</button>
+```
+
+### Technical Notes
+
+- The existing tag functionality in `popup.js` handles all the logic:
+  - `toggleTag(videoId, tag)` method handles adding/removing tags from video history
+  - `isTagActive(videoId, tag)` method checks if a tag is active for a video
+  - `tagChip` binding handles click events and active state styling
+- CSS styles for `.tag-chip` are already implemented and work for all tags
+- Alpine.js CSP compliance maintained (no JavaScript expressions in HTML attributes)
+- All new tags use the same data binding pattern as the existing "notable" tag
+
+The implementation leverages the existing tag infrastructure, so no changes to the JavaScript logic were needed.
+
+### Files Modified
+- `popup/popup.html` - Added three new tag buttons to history view
+
+### Commits
+- eb406c5: Added three new tag buttons to history view: reference, to rewatch, and to save

+ 142 - 0
2025-06-06-codemcp-explore-adding-open-tabs-to-playlist-export.md

@@ -0,0 +1,142 @@
+# 2025-06-06 - CodeMCP: Explore Adding Open Tabs to Playlist Export
+
+## User Request
+the popup menu has a button to "export" the playlists. it currently puts the `playlists` and the `playbackHistory` into a json object along with the `exportDate`, and triggers the browser to download it. how feasible would it be to add `openTabs` to the export, containing a url and page title for each? don't make any code changes yet, just propose a plan for now.
+
+## Analysis
+
+### Current Export Implementation
+The export functionality is in `popup.js` lines 435-474 in the `exportPlaylists()` method:
+- Gets playlists from browser.storage.local
+- Gets playback history from browser.storage.local
+- Creates export data object with playlists, playbackHistory, and exportDate
+- Downloads as JSON file
+
+### Feasibility Assessment for Adding Open Tabs
+
+**HIGHLY FEASIBLE** - The implementation would be straightforward:
+
+#### Permissions ✅
+- Extension already has `"tabs"` permission in manifest.json
+- Firefox WebExtensions API `browser.tabs.query()` available to get all open tabs
+
+#### Technical Implementation ✅
+- Add `browser.tabs.query({})` call to get all open tabs
+- Extract URL and title from each tab
+- Add `openTabs` array to export data object
+- Minimal code changes (~10 lines)
+
+### Proposed Plan
+
+#### 1. Modify `exportPlaylists()` method
+```javascript
+// Add after getting history data:
+const tabsResult = await browser.tabs.query({});
+const openTabs = tabsResult
+  .filter(tab => tab.url && !tab.url.startsWith('moz-extension://') && !tab.url.startsWith('about:'))
+  .map(tab => ({
+    url: tab.url,
+    title: tab.title
+  }));
+
+// Add to export data:
+const exportData = {
+  playlists,
+  playbackHistory,
+  openTabs,
+  exportDate: new Date().toISOString(),
+};
+```
+
+#### 2. Error Handling
+- Wrap tabs query in try-catch
+- Gracefully handle permission or API issues
+- Provide fallback if tabs can't be accessed
+
+#### 3. Optional Filtering
+- Filter out extension internal pages
+- Filter out browser internal pages (about:, chrome://, etc.)
+- Consider deduplicating identical URLs
+
+### Benefits
+- **Minimal complexity**: Only ~10 lines of additional code
+- **No breaking changes**: Existing exports continue to work
+- **Useful data**: Open tabs represent current interests/research
+- **Consistent format**: Follows existing data structure pattern
+
+### Considerations
+- **Privacy**: Users should know open tabs are being exported
+- **Performance**: Large number of tabs (~100+) would create larger files
+- **Optional feature**: Could add checkbox to include/exclude open tabs
+
+### Recommendation
+**Proceed with implementation** - This is a low-risk, high-value addition that fits naturally with the existing export functionality.
+
+---
+
+## Implementation
+
+### Changes Made
+
+#### 1. Modified `exportPlaylists()` method in popup.js
+- Added `browser.tabs.query({})` call to retrieve all open tabs
+- Added filtering to exclude browser internal pages:
+  - `moz-extension://` URLs (Firefox extension pages)
+  - `about:` URLs (Firefox internal pages)
+  - `chrome://` URLs (Chrome internal pages)
+- Added error handling with try-catch around tabs query
+- Added `openTabs` array to export data structure
+
+#### 2. Updated TODO list
+- Marked "include currently open tabs in export" as completed (X)
+
+### Final Implementation
+```javascript
+// Get open tabs
+let openTabs = [];
+try {
+  const tabsResult = await browser.tabs.query({});
+  openTabs = tabsResult
+    .filter(tab => tab.url &&
+      !tab.url.startsWith('moz-extension://') &&
+      !tab.url.startsWith('about:') &&
+      !tab.url.startsWith('chrome://'))
+    .map(tab => ({
+      url: tab.url,
+      title: tab.title || 'Untitled'
+    }));
+} catch (error) {
+  console.warn("Could not retrieve open tabs:", error);
+  openTabs = [];
+}
+
+// Create export data object
+const exportData = {
+  playlists,
+  playbackHistory,
+  openTabs,
+  exportDate: new Date().toISOString(),
+};
+```
+
+### Export Data Structure
+The exported JSON now includes:
+```json
+{
+  "playlists": { /* existing playlist data */ },
+  "playbackHistory": { /* existing history data */ },
+  "openTabs": [
+    {
+      "url": "https://example.com",
+      "title": "Example Page Title"
+    }
+  ],
+  "exportDate": "2025-06-06T..."
+}
+```
+
+### Features
+- **Graceful degradation**: If tabs can't be accessed, exports still work with empty `openTabs` array
+- **Privacy-conscious filtering**: Excludes internal browser/extension pages
+- **Fallback titles**: Uses 'Untitled' if tab has no title
+- **Non-breaking**: Existing import functionality will ignore the new `openTabs` field

+ 125 - 0
2025-06-06-codemcp-feat-add-save-channel-functionality-with-curl-comm.md

@@ -0,0 +1,125 @@
+# Save Channel Functionality Implementation
+
+## User Request
+Add a "save channel" button at the bottom of the popup that switches to a new view. The view is only active if the currently open tab's URL contains `youtube.com.*/videos`. The view contains:
+
+1. A text-editable field with a copy button that copies the contents to clipboard
+2. An interval number input to modify the checkInterval field
+3. A list of categories that modify the field contents when clicked
+4. The field is populated with a curl command template
+
+Template string:
+```
+curl -X POST -H 'content-type: application/json' -d '{"query": "mutation add{ addChannel(details: {category: \"<category>\", checkInterval: <interval>, name: \"<title>\", url: \"<url>\"}){name} } "}' localhost:8543/data | jq '.'
+```
+
+Where:
+- `<category>` is the currently selected category (defaulting to "FOR BOTH")
+- `<interval>` is the check interval in days (defaulting to 14)
+- `<title>` is the page title with trailing " - YouTube" removed
+- `<url>` is the URL of the currently active tab
+
+Categories: "FOR BOTH", "CHANNELS", "CREATIVE"
+
+## Implementation Details
+
+### 1. Manifest Changes
+- Added `clipboardWrite` permission for copy functionality
+
+### 2. HTML Structure
+- Added new save channel header with back button
+- Added save channel container with:
+  - Curl command textarea (readonly)
+  - Copy button with active state styling
+  - Interval number input (defaulting to 14 days)
+  - Category selection buttons
+  - Notice for non-YouTube videos pages
+- Added "Save Channel" button to the export container
+
+### 3. JavaScript Functionality
+- Extended `currentView` to include "saveChannel" state
+- Added properties:
+  - `isCurrentTabChannelPage`: Detects YouTube /videos pages (renamed from `isCurrentTabYoutubeVideos`)
+  - `selectedCategory`: Tracks selected category (default "FOR BOTH")
+  - `availableCategories`: Array of category options
+  - `curlCommand`: Generated curl command string
+  - `checkInterval`: Check interval in days (default 14)
+
+- Added methods:
+  - `updateCurlCommand()`: Generates curl command based on current tab, category, and interval
+  - `copyCurlCommand()`: Copies command to clipboard with fallback
+  - `selectCategory(category)`: Updates selected category and regenerates command
+  - `updateCheckInterval(interval)`: Updates check interval with validation and regenerates command
+  - `showSaveChannel()`: Switches to save channel view
+
+- Added event bindings (CSP-compliant):
+  - `saveChannelButton`: Click handler for main save channel button
+  - `saveChannelContainer`: View visibility binding
+  - `saveChannelHeader`: Header visibility binding
+  - `copyCurlButton`: Copy button click handler with disabled state for non-channel pages
+  - `categoryButton`: Category selection with active state styling
+  - `intervalInput`: Two-way binding for interval number input
+  - `saveChannelNotice`: Notice visibility for non-YouTube videos pages
+
+### 4. CSS Styling
+- Save channel container with proper spacing and shadow
+- Monospace textarea for curl command display
+- Copy button with hover, active, and disabled states
+- Interval input section with label and validation styling
+- Category buttons with hover and active states
+- Notice styling for YouTube videos page requirement
+- Orange-themed save channel button
+
+### 5. YouTube Videos Page Detection
+The feature only activates when the current tab URL contains `/videos`, which typically corresponds to YouTube channel video listing pages (e.g., `https://www.youtube.com/@channel/videos`).
+
+### 6. Copy Functionality
+Implements both modern clipboard API and fallback for older browsers using document.execCommand. The copy button is disabled when not on a valid YouTube channel page.
+
+### 7. Interval Input
+- Number input with minimum value of 1 day
+- Default value of 14 days
+- Real-time updates to curl command as user types
+- Input validation with fallback to default value for invalid inputs
+
+## Files Modified
+- `manifest.json`: Added clipboardWrite permission
+- `popup/popup.html`: Added save channel view, button, and interval input
+- `popup/popup.js`: Added save channel functionality, interval handling, and bindings
+- `popup/popup.css`: Added styling for save channel components and interval input
+
+## Usage
+1. Navigate to a YouTube channel's videos page (URL contains /videos)
+2. Click "Save Channel" button in the extension popup
+3. Optionally adjust the check interval (defaults to 14 days)
+4. Select desired category from the buttons
+5. Click "Copy" to copy the generated curl command to clipboard
+6. Use the curl command to add the channel to your external system
+
+The feature gracefully handles non-YouTube pages by showing an informational notice and disabling the copy button.
+
+## Future Enhancement Consideration: Editable Textarea
+
+### Current State
+The textarea field is currently `readonly` and auto-generated based on user selections (category and interval).
+
+### Potential Enhancement
+Making the textarea user-editable would provide additional flexibility. Implementation considerations:
+
+#### Required Changes:
+1. **HTML**: Remove `readonly` attribute from textarea
+2. **JavaScript**: Switch from `x-text` to `x-model` for two-way binding
+3. **Update Logic**: Handle conflicts between manual edits and automatic updates
+
+#### Implementation Options:
+1. **Template-based**: Store template and regenerate only dynamic parts
+2. **Parsing approach**: Parse existing command to update specific fields
+3. **Hybrid**: Allow manual edits with "Reset to Template" button
+
+#### UX Considerations:
+- **Conflict resolution**: Behavior when user edits manually, then changes category/interval
+- **Validation**: Validate edited commands for JSON/GraphQL correctness
+- **Visual indicators**: Show when command is manually modified vs auto-generated
+- **Reset functionality**: Button to return to auto-generated version
+
+This enhancement would balance user flexibility with automatic generation convenience, but requires careful consideration of the user experience and conflict resolution strategies.

+ 9 - 0
codemcp.toml

@@ -1,11 +1,20 @@
+# Claude Desktop project's instructions (in "project knowledge"):
+# "Initialize codemcp with <home>/projects/experiments/playlist/"
+# (use absolute path for <home> because otherwise the LLM will hallucinate a home path)
+
 project_prompt = '''
 - this is a firefox browser extension which manages a set of playlists, where each video is located at a url
 - it also tracks video playback history
 - playlist and history data is stored using the browser's storage API
 - the extension popup code is in the popup/ folder, and is implemented in Alpine.js
   - all the Alpine code must be in "CSP" mode, which means there can be no javascript expression strings in the html attributes (no `===` equality checks, no function calls) (favor binding in Alpine.data instead)
+  - the popup menu has several "views" (or tabs, or pages):
+    - the main view of the popup shows all the playlists and their unviewed vidoes, and some extra action buttons
+    - the individual playlist view shows all the videos of a playlist, including past (viewed) videos
+    - the history view shows the videos that have been played in the past, as well as individual play/pause/finished events
 - background.js adds context menus (for adding new videos to the playlist), and manages automatic browser navigation and history tracking
 - content_scripts/content.js is injected to add video playback event listeners
+- the shared/ folder contains functions that may be used across different parts of the extension
 - under no circumstances will there be any need to read or write files outside the code directory
   - do not execute any command that reads, writes, or searches for files outside the code directory
 - at the end of each request, before committing in git, save a copy of the contents of the context window (including the user's original prompt) to a file in the code folder with the filename of "<date>-codemcp-<feature name>.md"

+ 1 - 0
manifest.json

@@ -10,6 +10,7 @@
     "contextMenus",
     "notifications",
     "webNavigation",
+    "clipboardWrite",
     "<all_urls>"
   ],
   "browser_action": {

+ 190 - 0
popup/popup.css

@@ -121,6 +121,15 @@ button:hover {
   text-decoration: line-through;
 }
 
+.non-contiguous-done-video {
+  opacity: 0.7;
+  background-color: rgba(128, 128, 128, 0.05);
+}
+
+.non-contiguous-done-video .video-title {
+  color: #666;
+}
+
 /* Truncated videos indicator */
 .truncated-videos {
   padding: 8px 0 12px 8px;
@@ -440,3 +449,184 @@ button:hover {
   transform: none;
   box-shadow: none;
 }
+
+/* Save Channel view styles */
+.save-channel-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 16px;
+  margin-bottom: 16px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  min-height: 300px;
+}
+
+.save-channel-content {
+  display: flex;
+  flex-direction: column;
+  gap: 20px;
+}
+
+.curl-command-section {
+  display: flex;
+  flex-direction: column;
+  gap: 12px;
+}
+
+.curl-field-container {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.curl-command-field {
+  width: 100%;
+  min-height: 120px;
+  padding: 12px;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  font-family: monospace;
+  font-size: 12px;
+  background-color: #f8f9fa;
+  resize: vertical;
+  line-height: 1.4;
+}
+
+.copy-curl-btn {
+  align-self: flex-start;
+  background-color: #4285f4;
+  color: white;
+  padding: 8px 16px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+}
+
+.copy-curl-btn:hover {
+  background-color: #3367d6;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 6px rgba(66, 133, 244, 0.3);
+}
+
+.copy-curl-btn:active {
+  background-color: #2d5aa0;
+  transform: translateY(0);
+  box-shadow: 0 1px 3px rgba(66, 133, 244, 0.4);
+  transition: all 0.1s ease;
+}
+
+.copy-curl-btn:disabled {
+  background-color: #ccc;
+  color: #888;
+  cursor: not-allowed;
+  transform: none;
+  box-shadow: none;
+}
+
+.copy-curl-btn:disabled:hover {
+  background-color: #ccc;
+  transform: none;
+  box-shadow: none;
+}
+
+.interval-section {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.interval-label {
+  font-size: 14px;
+  font-weight: 500;
+  color: #333;
+}
+
+.interval-input {
+  width: 100px;
+  padding: 8px 12px;
+  border: 1px solid #ddd;
+  border-radius: 6px;
+  font-size: 14px;
+  font-family: inherit;
+  background-color: #fff;
+  transition: border-color 0.2s ease, box-shadow 0.2s ease;
+}
+
+.interval-input:focus {
+  outline: none;
+  border-color: #4285f4;
+  box-shadow: 0 0 0 2px rgba(66, 133, 244, 0.2);
+}
+
+.interval-input:invalid {
+  border-color: #f44336;
+}
+
+.category-section h3 {
+  margin-bottom: 12px;
+  color: #333;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.category-buttons {
+  display: flex;
+  gap: 8px;
+  flex-wrap: wrap;
+}
+
+.category-btn {
+  padding: 8px 16px;
+  border: 2px solid #ddd;
+  border-radius: 6px;
+  background-color: #f8f9fa;
+  color: #666;
+  font-size: 14px;
+  font-weight: 500;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.category-btn:hover {
+  border-color: #4285f4;
+  background-color: #e8f0fe;
+  color: #4285f4;
+}
+
+.category-btn.active {
+  border-color: #4285f4;
+  background-color: #4285f4;
+  color: white;
+}
+
+.category-btn.active:hover {
+  border-color: #3367d6;
+  background-color: #3367d6;
+}
+
+.save-channel-notice {
+  background-color: #fff3cd;
+  border: 1px solid #ffeaa7;
+  border-radius: 6px;
+  padding: 12px;
+  color: #856404;
+  font-size: 14px;
+}
+
+.save-channel-notice p {
+  margin: 0;
+}
+
+/* Save Channel button styles */
+.save-channel-btn {
+  background-color: #ff6d01;
+  color: white;
+  padding: 8px 12px;
+  border-radius: 4px;
+  font-size: 14px;
+  font-weight: 500;
+}
+
+.save-channel-btn:hover {
+  background-color: #e55a00;
+}

+ 97 - 0
popup/popup.html

@@ -25,6 +25,11 @@
         <h1 x-text="currentPlaylistName"></h1>
       </header>
 
+      <header x-bind="saveChannelHeader">
+        <button class="back-btn-arrow" x-bind="backButton">←</button>
+        <h1>Save Channel</h1>
+      </header>
+
       <div class="playlists-container" x-bind="playlistsContainer">
         <template
           x-for="playlistData in playlistsForDisplay"
@@ -95,6 +100,14 @@
                         :data-playlist-index="video.originalIndex"
                         x-bind="moreMenu"
                       >
+                        <button
+                          :data-playlist-name="playlistData.name"
+                          :data-playlist-index="video.originalIndex"
+                          x-bind="toggleVideoDoneButton"
+                          class="menu-item done-item"
+                          x-text="video.doneButtonText"
+                        >
+                        </button>
                         <button
                           :data-playlist-name="playlistData.name"
                           :data-playlist-index="video.originalIndex"
@@ -140,6 +153,30 @@
                   >
                     notable
                   </button>
+                  <button
+                    class="tag-chip"
+                    :data-video-id="videoHistory.videoId"
+                    data-tag="reference"
+                    x-bind="tagChip"
+                  >
+                    reference
+                  </button>
+                  <button
+                    class="tag-chip"
+                    :data-video-id="videoHistory.videoId"
+                    data-tag="to rewatch"
+                    x-bind="tagChip"
+                  >
+                    to rewatch
+                  </button>
+                  <button
+                    class="tag-chip"
+                    :data-video-id="videoHistory.videoId"
+                    data-tag="to save"
+                    x-bind="tagChip"
+                  >
+                    to save
+                  </button>
                 </div>
               </div>
               <div class="history-events">
@@ -159,6 +196,55 @@
         </div>
       </div>
 
+      <!-- Save Channel view -->
+      <div class="save-channel-container" x-bind="saveChannelContainer">
+        <div class="save-channel-content">
+          <div class="curl-command-section">
+            <div class="curl-field-container">
+              <textarea
+                class="curl-command-field"
+                x-text="curlCommand"
+                readonly
+              ></textarea>
+              <button
+                class="copy-curl-btn"
+                x-bind="copyCurlButton"
+                title="Copy to clipboard"
+              >
+                Copy
+              </button>
+            </div>
+          </div>
+          <div class="interval-section">
+            <label for="check-interval-input" class="interval-label">Check Interval (days):</label>
+            <input
+              type="number"
+              id="check-interval-input"
+              class="interval-input"
+              min="1"
+              step="1"
+              x-bind="intervalInput"
+            />
+          </div>
+          <div class="category-section">
+            <h3>Categories</h3>
+            <div class="category-buttons">
+              <template x-for="category in availableCategories" :key="category">
+                <button
+                  class="category-btn"
+                  :data-category="category"
+                  x-bind="categoryButton"
+                  x-text="category"
+                ></button>
+              </template>
+            </div>
+          </div>
+          <div class="save-channel-notice" x-bind="saveChannelNotice">
+            <p>This feature is only available on YouTube channel pages (/videos).</p>
+          </div>
+        </div>
+      </div>
+
       <!-- Individual playlist view -->
       <div class="playlist-view-container" x-bind="playlistViewContainer">
         <div class="playlist-full-view">
@@ -211,6 +297,14 @@
                     :data-playlist-index="index"
                     x-bind="moreMenu"
                   >
+                    <button
+                      :data-playlist-name="currentPlaylistName"
+                      :data-playlist-index="index"
+                      x-bind="toggleVideoDoneButton"
+                      class="menu-item done-item"
+                      x-text="video.doneButtonText"
+                    >
+                    </button>
                     <button
                       :data-playlist-name="currentPlaylistName"
                       :data-playlist-index="index"
@@ -249,6 +343,9 @@
         <button class="history-btn" x-bind="historyButton">
           History
         </button>
+        <button class="save-channel-btn" x-bind="saveChannelButton">
+          Save Channel
+        </button>
         <input
           type="file"
           id="import-file-input"

+ 253 - 3
popup/popup.js

@@ -7,7 +7,7 @@ document.addEventListener("alpine:init", () => {
   //   X or move to an "old" category, and add a screen to see the list
   // - consider separating context menu items (rather than having a sub-menu)
   // X view playback history (?)
-  // - include currently open tabs in export
+  // X include currently open tabs in export
   // - support playlist management (add, remove, rename playlists)
   // - support input text box for adding to playlist (how to handle title?)
   //   - or raw json editing
@@ -16,20 +16,42 @@ document.addEventListener("alpine:init", () => {
   // - button to add channel to youtube page (copy gql mutation to clipboard) (like the automa version)
   // - long-term: replace youtube page? rss feeds? need server?
   // X add personal rating feature ("enjoyed", "this was important", etc)
+  // - option to move video (including status) to another playlist
   Alpine.data("playlistManager", () => ({
     playlists: {},
     currentIndices: {},
     history: {},
     sortedHistory: [],
     openMenus: new Set(), // Track which menus are open
-    currentView: "playlists", // Track current view: 'playlists', 'history', or 'playlist'
+    currentView: "playlists", // Track current view: 'playlists', 'history', 'playlist', or 'saveChannel'
     currentPlaylistName: "", // Track which playlist is being viewed
     playlistsForDisplay: [], // Computed array for display
     currentPlaylistVideos: [], // Videos for current playlist view
     currentTab: null, // Current active tab info
     isCurrentTabYoutube: false, // Whether current tab is YouTube
+    isCurrentTabChannelPage: false, // Whether current tab is YouTube videos page
     addCurrentPageButtonText: "Add Current Page", // Button text
 
+    // Save channel properties
+    selectedCategory: "FOR BOTH", // Currently selected category
+    availableCategories: [
+      "FOR BOTH",
+      "CHANNELS",
+      "SCIFI",
+      "POLI/REL",
+      "REAL ESTATE",
+      "HELICOPTER",
+      "SAILING",
+      "VAN LIFE",
+      "BLOCKCHAIN",
+      "MAKING",
+      "DESIGN",
+      "TECH CONF",
+      "CREATIVE",
+    ], // Available categories
+    curlCommand: "", // Generated curl command
+    checkInterval: 14, // Check interval in days
+
     init() {
       this.loadPlaylists();
       this.loadHistory();
@@ -87,6 +109,12 @@ document.addEventListener("alpine:init", () => {
             .map((video, index) => ({
               ...video,
               originalIndex: currentIndex + index,
+              doneButtonText:
+                video.status === "done" ? "Remove Done Status" : "Mark as Done",
+              isNonContiguousDone: this.isNonContiguousDone(
+                playlistName,
+                currentIndex + index,
+              ),
             }));
 
           return {
@@ -107,7 +135,17 @@ document.addEventListener("alpine:init", () => {
       ) {
         this.currentPlaylistVideos = [];
       } else {
-        this.currentPlaylistVideos = this.playlists[this.currentPlaylistName];
+        this.currentPlaylistVideos = this.playlists[
+          this.currentPlaylistName
+        ].map((video, index) => ({
+          ...video,
+          doneButtonText:
+            video.status === "done" ? "Remove Done Status" : "Mark as Done",
+          isNonContiguousDone: this.isNonContiguousDone(
+            this.currentPlaylistName,
+            index,
+          ),
+        }));
       }
     },
 
@@ -132,13 +170,18 @@ document.addEventListener("alpine:init", () => {
           this.currentTab = tabs[0];
           const url = new URL(this.currentTab.url);
           this.isCurrentTabYoutube = url.hostname === "www.youtube.com";
+          this.isCurrentTabChannelPage =
+            this.isCurrentTabYoutube && url.pathname.includes("/videos");
           this.updateAddCurrentPageButtonText();
+          this.updateCurlCommand();
         }
       } catch (error) {
         console.error("Error getting current tab:", error);
         this.currentTab = null;
         this.isCurrentTabYoutube = false;
+        this.isCurrentTabChannelPage = false;
         this.updateAddCurrentPageButtonText();
+        this.updateCurlCommand();
       }
     },
 
@@ -152,6 +195,61 @@ document.addEventListener("alpine:init", () => {
       }
     },
 
+    updateCurlCommand() {
+      if (!this.currentTab || !this.isCurrentTabChannelPage) {
+        this.curlCommand =
+          "This feature is only available on YouTube channel pages (/videos).";
+        return;
+      }
+
+      const title = this.currentTab.title.replace(" - YouTube", "");
+      const url = this.currentTab.url;
+      const category = this.selectedCategory;
+      const interval = this.checkInterval;
+
+      this.curlCommand = `curl -X POST -H 'content-type: application/json' -d '{"query": "mutation add{ addChannel(details: {category: \\"${category}\\", checkInterval: ${interval}, name: \\"${title}\\", url: \\"${url}\\"}){name} } "}' localhost:8543/data | jq '.'`;
+    },
+
+    async copyCurlCommand() {
+      try {
+        await navigator.clipboard.writeText(this.curlCommand);
+        console.log("Curl command copied to clipboard");
+        // Could add visual feedback here
+      } catch (error) {
+        console.error("Failed to copy to clipboard:", error);
+        // Fallback for older browsers
+        try {
+          const textArea = document.createElement("textarea");
+          textArea.value = this.curlCommand;
+          document.body.appendChild(textArea);
+          textArea.focus();
+          textArea.select();
+          document.execCommand("copy");
+          document.body.removeChild(textArea);
+          console.log("Curl command copied to clipboard (fallback)");
+        } catch (fallbackError) {
+          console.error("Fallback copy failed:", fallbackError);
+        }
+      }
+    },
+
+    selectCategory(category) {
+      this.selectedCategory = category;
+      this.updateCurlCommand();
+    },
+
+    updateCheckInterval(interval) {
+      // Ensure it's a positive integer, default to 14 if invalid
+      const parsedInterval = parseInt(interval);
+      this.checkInterval = parsedInterval > 0 ? parsedInterval : 14;
+      this.updateCurlCommand();
+    },
+
+    showSaveChannel() {
+      this.currentView = "saveChannel";
+      this.updateCurlCommand();
+    },
+
     sortHistoryByRecentInteraction() {
       // Convert history object to array with video ID and sort by most recent interaction
       this.sortedHistory = Object.entries(this.history)
@@ -361,6 +459,40 @@ document.addEventListener("alpine:init", () => {
       }
     },
 
+    async toggleVideoDoneStatus(playlistName, index) {
+      const playlists = JSON.parse(JSON.stringify(this.playlists));
+      const playlist = [...playlists[playlistName]];
+      const video = { ...playlist[index] };
+
+      // Toggle the done status
+      if (video.status === "done") {
+        // Remove status property (undefined status means not done)
+        delete video.status;
+      } else {
+        // Set status to done
+        video.status = "done";
+      }
+
+      // Update the video in the playlist
+      playlist[index] = video;
+
+      // Create an updated playlists object
+      const updatedPlaylists = {
+        ...playlists,
+        [playlistName]: playlist,
+      };
+
+      // Update the playlists in storage
+      try {
+        await browser.storage.local.set({ playlists: updatedPlaylists });
+        this.playlists = updatedPlaylists;
+        this.updatePlaylistsForDisplay();
+        this.updateCurrentPlaylistVideos();
+      } catch (error) {
+        console.error("Error toggling video done status:", error);
+      }
+    },
+
     async addCurrentPageToPlaylist() {
       if (
         !this.currentTab ||
@@ -401,10 +533,32 @@ document.addEventListener("alpine:init", () => {
         const historyResult = await browser.storage.local.get("history");
         const playbackHistory = historyResult.history || {};
 
+        // Get open tabs
+        let openTabs = [];
+        try {
+          const tabsResult = await browser.tabs.query({});
+          openTabs = tabsResult
+            .filter(
+              (tab) =>
+                tab.url &&
+                !tab.url.startsWith("moz-extension://") &&
+                !tab.url.startsWith("about:") &&
+                !tab.url.startsWith("chrome://"),
+            )
+            .map((tab) => ({
+              url: tab.url,
+              title: tab.title || "Untitled",
+            }));
+        } catch (error) {
+          console.warn("Could not retrieve open tabs:", error);
+          openTabs = [];
+        }
+
         // Create export data object
         const exportData = {
           playlists,
           playbackHistory,
+          openTabs,
           exportDate: new Date().toISOString(),
         };
 
@@ -444,6 +598,26 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    isNonContiguousDone(playlistName, videoIndex) {
+      const playlist = this.playlists[playlistName];
+      if (
+        !playlist ||
+        !playlist[videoIndex] ||
+        playlist[videoIndex].status !== "done"
+      ) {
+        return false;
+      }
+
+      // Check if there's any non-done video before this done video
+      for (let i = 0; i < videoIndex; i++) {
+        if (playlist[i].status !== "done") {
+          return true;
+        }
+      }
+
+      return false;
+    },
+
     isCurrentVideo(playlistName, index) {
       const currentIndex = this.currentIndices[playlistName];
       return currentIndex === index;
@@ -454,6 +628,12 @@ document.addEventListener("alpine:init", () => {
       return index < currentIndex;
     },
 
+    isVideoDone(playlistName, index) {
+      const video =
+        this.playlists[playlistName] && this.playlists[playlistName][index];
+      return video && video.status === "done";
+    },
+
     videoItemClass: {
       [":class"]() {
         const playlistName = this.$el.dataset.playlistName;
@@ -464,6 +644,10 @@ document.addEventListener("alpine:init", () => {
           "current-video": this.isCurrentVideo(playlistName, index),
           "done-video":
             this.isDoneVideo(playlistName, index) && video.status === "done",
+          "non-contiguous-done-video": this.isNonContiguousDone(
+            playlistName,
+            index,
+          ),
         };
       },
     },
@@ -478,6 +662,16 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    toggleVideoDoneButton: {
+      ["@click"]() {
+        this.toggleVideoDoneStatus(
+          this.$el.dataset.playlistName,
+          parseInt(this.$el.dataset.playlistIndex),
+        );
+        this.closeAllMenus();
+      },
+    },
+
     moveUpButton: {
       ["@click"]() {
         this.moveVideoUp(
@@ -719,6 +913,12 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    saveChannelButton: {
+      ["@click"]() {
+        this.showSaveChannel();
+      },
+    },
+
     backButton: {
       ["@click"]() {
         this.showPlaylists();
@@ -780,6 +980,12 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    saveChannelHeader: {
+      ["x-show"]() {
+        return this.currentView === "saveChannel";
+      },
+    },
+
     playlistsContainer: {
       ["x-show"]() {
         return this.currentView === "playlists";
@@ -798,6 +1004,12 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    saveChannelContainer: {
+      ["x-show"]() {
+        return this.currentView === "saveChannel";
+      },
+    },
+
     exportContainer: {
       ["x-show"]() {
         return this.currentView === "playlists";
@@ -834,6 +1046,44 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    // Save Channel specific bindings
+    copyCurlButton: {
+      ["@click"]() {
+        this.copyCurlCommand();
+      },
+      [":disabled"]() {
+        return !this.isCurrentTabChannelPage;
+      },
+    },
+
+    categoryButton: {
+      ["@click"]() {
+        const category = this.$el.dataset.category;
+        this.selectCategory(category);
+      },
+      [":class"]() {
+        const category = this.$el.dataset.category;
+        return {
+          active: this.selectedCategory === category,
+        };
+      },
+    },
+
+    saveChannelNotice: {
+      ["x-show"]() {
+        return !this.isCurrentTabChannelPage;
+      },
+    },
+
+    intervalInput: {
+      [":value"]() {
+        return this.checkInterval;
+      },
+      ["@input"]() {
+        this.updateCheckInterval(this.$el.value);
+      },
+    },
+
     // Add current page button binding
     addCurrentPageButton: {
       ["@click"]() {