Browse Source

feat: make video ID clickable and add toggleable tags system

in the history view of the popup menu, make the id in parentheses the clickable link, instead of the video title (make the video title not clickable). under the video id, add tags or chips that can be activated or deactivated (default inactive). for now, only put a single one called "notable". when activated, modify the history in storage to include an array of the active tags (as string), like so: `{video-id: {url:..., history: [], tags: ["notable"]}}`.

```git-revs
ee9ee10  (Base revision)
38909df  Make video ID clickable instead of title, add tags section
58d3cef  Update video ID styling to be clickable and add tag chip styles
f3151cb  Add tag functionality and update history processing
bae5b13  Add tag toggle functionality and save to storage
e37016e  Add back formatActionName method
40d10d8  Add tag chip Alpine.js binding for CSP compliance
a5b5973  Create context file documenting the changes made
HEAD     Fix proxy clone error by creating plain JS object before storage
```

codemcp-id: 6-feat-make-video-id-clickable-and-add-toggleable-ta
Brandon Wong 1 year ago
parent
commit
0632289c39

+ 48 - 0
2025-05-29-codemcp-feat-make-video-id-clickable-and-add-toggleable-tags.md

@@ -0,0 +1,48 @@
+# 2025-05-29 - CodeMCP - Make Video ID Clickable and Add Toggleable Tags
+
+## User Request
+in the history view of the popup menu, make the id in parentheses the clickable link, instead of the video title (make the video title not clickable). under the video id, add tags or chips that can be activated or deactivated (default inactive). for now, only put a single one called "notable". when activated, modify the history in storage to include an array of the active tags (as string), like so: `{video-id: {url:..., history: [], tags: ["notable"]}}`.
+
+## Implementation Summary
+
+### Changes Made
+
+1. **HTML Changes (popup.html)**:
+   - Changed video title from clickable link to plain div
+   - Made video ID parentheses clickable with the video URL
+   - Added a tags section below the video ID with a "notable" tag chip
+
+2. **CSS Changes (popup.css)**:
+   - Updated `.history-video-id` to be clickable with hover effects
+   - Added styles for `.history-tags` container and `.tag-chip` elements
+   - Added active state styling for activated tags
+
+3. **JavaScript Changes (popup.js)**:
+   - Added `toggleTag(videoId, tag)` method to handle tag activation/deactivation
+   - Added `isTagActive(videoId, tag)` helper method to check tag state
+   - Modified `sortHistoryByRecentInteraction()` to ensure tags array exists
+   - Added CSP-compliant Alpine.js bindings for tag chips with click handlers and active state
+
+### Key Features
+
+- **Video ID Clickability**: The video ID in parentheses is now the clickable element that opens the video
+- **Tag System**: Each video history item has a "notable" tag chip that can be toggled
+- **Visual Feedback**: Active tags have different styling (blue background, white text)
+- **Data Persistence**: Tag state is saved to browser storage in the format: `{video-id: {url:..., history: [], tags: ["notable"]}}`
+- **CSP Compliance**: All Alpine.js code follows CSP requirements with no JavaScript expressions in HTML
+
+### Technical Implementation
+
+The tag system integrates with the existing history data structure by adding a `tags` array to each video's data. The implementation is fully CSP-compliant and maintains the existing functionality while adding the new tag features.
+
+The storage format now supports:
+```javascript
+{
+  "video-id": {
+    "url": "https://example.com/video",
+    "title": "Video Title",
+    "history": [...],
+    "tags": ["notable"] // New tags array
+  }
+}
+```

+ 44 - 1
popup/popup.css

@@ -274,8 +274,51 @@ button:hover {
 
 .history-video-id {
   font-size: 12px;
-  color: #666;
+  color: #4285f4;
   font-family: monospace;
+  text-decoration: none;
+  cursor: pointer;
+  transition: color 0.2s ease;
+}
+
+.history-video-id:hover {
+  color: #3367d6;
+  text-decoration: underline;
+}
+
+.history-tags {
+  margin-top: 6px;
+  display: flex;
+  gap: 6px;
+  flex-wrap: wrap;
+}
+
+.tag-chip {
+  font-size: 10px;
+  padding: 2px 8px;
+  border: 1px solid #ddd;
+  border-radius: 12px;
+  background-color: #f8f9fa;
+  color: #666;
+  cursor: pointer;
+  transition: all 0.2s ease;
+}
+
+.tag-chip:hover {
+  background-color: #e8f0fe;
+  border-color: #4285f4;
+  color: #4285f4;
+}
+
+.tag-chip.active {
+  background-color: #4285f4;
+  border-color: #4285f4;
+  color: white;
+}
+
+.tag-chip.active:hover {
+  background-color: #3367d6;
+  border-color: #3367d6;
 }
 
 .history-events {

+ 13 - 3
popup/popup.html

@@ -123,13 +123,23 @@
           <template x-for="videoHistory in sortedHistory" :key="videoHistory.videoId">
             <div class="history-item" :data-video-id="videoHistory.videoId">
               <div class="history-video-info">
+                <div class="history-video-title" x-text="videoHistory.title"></div>
                 <a
-                  class="history-video-title"
+                  class="history-video-id"
                   :href="videoHistory.url"
-                  x-text="videoHistory.title"
+                  x-text="videoHistory.formattedVideoId"
                   x-bind="videoPlayLink"
                 ></a>
-                <div class="history-video-id" x-text="videoHistory.formattedVideoId"></div>
+                <div class="history-tags">
+                  <button
+                    class="tag-chip"
+                    :data-video-id="videoHistory.videoId"
+                    data-tag="notable"
+                    x-bind="tagChip"
+                  >
+                    notable
+                  </button>
+                </div>
               </div>
               <div class="history-events">
                 <template x-for="event in videoHistory.processedEvents" :key="event.uniqueKey">

+ 56 - 1
popup/popup.js

@@ -134,7 +134,8 @@ document.addEventListener("alpine:init", () => {
             formattedVideoId: `(${videoId})`,
             ...videoData,
             lastInteraction,
-            processedEvents
+            processedEvents,
+            tags: videoData.tags || [] // Ensure tags array exists
           };
         })
         .sort((a, b) => b.lastInteraction - a.lastInteraction);
@@ -162,6 +163,44 @@ document.addEventListener("alpine:init", () => {
       return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
     },
 
+    async toggleTag(videoId, tag) {
+      // Create a deep copy of history to avoid proxy issues
+      const history = JSON.parse(JSON.stringify(this.history));
+
+      if (!history[videoId]) {
+        console.error(`Video ${videoId} not found in history`);
+        return;
+      }
+
+      const videoData = history[videoId];
+      if (!videoData.tags) {
+        videoData.tags = [];
+      }
+
+      const tagIndex = videoData.tags.indexOf(tag);
+      if (tagIndex === -1) {
+        // Add tag
+        videoData.tags.push(tag);
+      } else {
+        // Remove tag
+        videoData.tags.splice(tagIndex, 1);
+      }
+
+      // Save to storage and update local state
+      try {
+        await browser.storage.local.set({ history: history });
+        this.history = history;
+        this.sortHistoryByRecentInteraction(); // Refresh display
+      } catch (error) {
+        console.error("Error saving tag changes:", error);
+      }
+    },
+
+    isTagActive(videoId, tag) {
+      const videoData = this.history[videoId];
+      return videoData && videoData.tags && videoData.tags.includes(tag);
+    },
+
     formatActionName(action) {
       const actionMap = {
         'play': 'Started',
@@ -693,5 +732,21 @@ document.addEventListener("alpine:init", () => {
         return this.currentPlaylistVideos.length === 0;
       },
     },
+
+    // Tag chip bindings for CSP compliance
+    tagChip: {
+      ["@click"]() {
+        const videoId = this.$el.dataset.videoId;
+        const tag = this.$el.dataset.tag;
+        this.toggleTag(videoId, tag);
+      },
+      [":class"]() {
+        const videoId = this.$el.dataset.videoId;
+        const tag = this.$el.dataset.tag;
+        return {
+          'active': this.isTagActive(videoId, tag)
+        };
+      },
+    },
   }));
 });