Browse Source

feat: add current page to playlist button in popup

in the individual playlist view of the popup menu (not the main view), add a button at the bottom that will add the current page (in the active tab) to the playlist. use the website's title (html tag) as the video title and the url as the url. disable the button (and grey it out) if the url's host is not youtube.com.

```git-revs
fec6507  (Base revision)
09b1ac3  Add button container to individual playlist view for adding current page
aa1c556  Snapshot before codemcp change
944a132  Add data properties for current tab info and button state
21ca1bb  Add getCurrentTab call to init method
ddab254  Add getCurrentTab method
8d41e67  Add addCurrentPageToPlaylist method
f4d7c03  Add addCurrentPageButton binding
8d1ef98  Add CSS styles for add current page button
7354577  Create documentation file for the current page addition feature
ac4242b  Create shared playlist utilities file
ab6d21d  Add shared playlist utils to background scripts
91189f1  Add shared playlist utils to popup HTML
31a3dd4  Remove duplicate functions from background.js and use shared utils
72e4acd  Update updateTracking function to use shared utils
03f9960  Update findNext function to use shared utils
f3589c4  Snapshot before codemcp change
91e7bbb  Update popup.js to use shared PlaylistUtils instead of custom method
HEAD     Update documentation file with shared utils changes
```

codemcp-id: 7-feat-add-current-page-to-playlist-button-in-popup
Brandon Wong 1 year ago
parent
commit
911e9789c1

+ 114 - 0
2025-05-29-codemcp-feat-add-current-page-to-playlist-button-in-popup.md

@@ -0,0 +1,114 @@
+# 2025-05-29 - codemcp - Add Current Page to Playlist Button in Popup
+
+## User Request
+in the individual playlist view of the popup menu (not the main view), add a button at the bottom that will add the current page (in the active tab) to the playlist. use the website's title (html tag) as the video title and the url as the url. disable the button (and grey it out) if the url's host is not youtube.com.
+
+## Implementation
+
+### Files Modified
+
+#### popup/popup.html
+- Added a new button container at the bottom of the individual playlist view
+- Added `add-current-page-container` div with `add-current-page-btn` button
+- Button uses Alpine.js bindings for click handler, disabled state, and text content
+
+#### popup/popup.js
+- Added new data properties:
+  - `currentTab`: stores current active tab info
+  - `isCurrentTabYoutube`: boolean for YouTube detection
+  - `addCurrentPageButtonText`: dynamic button text
+- Added `getCurrentTab()` method to fetch current tab info and check if it's YouTube
+- Added `updateAddCurrentPageButtonText()` method to set appropriate button text
+- Added `addCurrentPageToPlaylist()` method to add current page to the selected playlist
+- Added Alpine.js binding `addCurrentPageButton` with click handler, disabled state, and CSS classes
+- Called `getCurrentTab()` in the `init()` method
+
+#### popup/popup.css
+- Added comprehensive styling for the new button:
+  - `.add-current-page-container`: container styling with border separator
+  - `.add-current-page-btn`: main button styling with hover effects
+  - Disabled state styling with grey colors and no-cursor
+
+### Key Features
+1. **YouTube Detection**: Button is only enabled when current tab is on youtube.com
+2. **Dynamic Text**: Button text changes based on current tab state:
+   - "Add Current Page to Playlist" (YouTube tab)
+   - "Add Current Page (YouTube only)" (non-YouTube tab)
+   - "Unable to get current page" (no tab access)
+3. **CSP Compliance**: All Alpine.js code follows CSP requirements with no JavaScript expressions in HTML
+4. **Visual Feedback**: Button is visually disabled (greyed out) when not usable
+5. **Integration**: Uses existing playlist management system and storage
+
+### Technical Details
+- Uses browser.tabs.query() to get current active tab
+- Checks URL hostname for YouTube detection
+- Creates video object with current tab's URL and title
+- Adds video to end of current playlist
+- Updates display after adding video
+- Respects existing Alpine.js patterns and CSP compliance
+
+# 2025-05-29 - codemcp - Add Current Page to Playlist Button in Popup + Shared Utils
+
+## User Request
+background.js has a function called addLinkToPlaylist, which adds a video to a playlist so long as it is not already present in a playlist. move that function to a shared file (like "playlist utils" or something) along with any of its dependent functions (like findPlaylist and findVideoInPlaylist). then have the new popup menu button call this function instead.
+
+## Implementation
+
+### New Files Created
+
+#### shared/playlist-utils.js
+- **PlaylistUtils** global object containing shared functions
+- **extractVideoId()**: Extract YouTube video ID from URL
+- **findVideoInPlaylists()**: Find video in all playlists, returns [playlistName, index, isLast] or false
+- **findPlaylist()**: Find which playlist contains a video (wrapper for findVideoInPlaylists)
+- **addVideoToPlaylist()**: Add video to playlist with duplicate checking
+- **addLinkToPlaylist()**: Legacy function for context menu compatibility
+
+### Files Modified
+
+#### manifest.json
+- Added `shared/playlist-utils.js` to background scripts array
+- Scripts load in order: shared utils first, then background.js
+
+#### popup/popup.html
+- Added `<script src="../shared/playlist-utils.js"></script>` before popup.js
+- Ensures shared utilities are available to popup code
+
+#### background.js
+- **Removed duplicate functions**: findVideoInPlaylists, findPlaylist, addLinkToPlaylist
+- **Updated function calls** to use PlaylistUtils:
+  - `updateTracking()` now uses `PlaylistUtils.findVideoInPlaylists()`
+  - `findNext()` now uses `PlaylistUtils.findVideoInPlaylists()`
+  - Context menu handler uses `PlaylistUtils.addLinkToPlaylist()`
+
+#### popup/popup.js
+- **Replaced custom addCurrentPageToPlaylist()** with call to shared utils
+- **Benefits of shared function**:
+  - Automatic duplicate checking (won't add if video exists in any playlist)
+  - Consistent video object structure
+  - Better error handling and logging
+  - Returns boolean indicating if video was actually added
+- **Improved user experience**:
+  - Only refreshes display if video was actually added
+  - Logs when video already exists (could show user notification later)
+
+### Key Improvements
+
+1. **Code Deduplication**: Removed ~65 lines of duplicate code from background.js
+2. **Consistent Logic**: Both popup and context menu use same duplicate-checking logic
+3. **Better UX**: Popup button now prevents adding duplicates (context menu already did this)
+4. **Maintainable**: Single source of truth for playlist operations
+5. **No Build Tools**: Pure vanilla JavaScript with global objects pattern
+
+### Technical Details
+- **Loading Order**: shared/playlist-utils.js loads before both popup.js and background.js
+- **Global Scope**: PlaylistUtils object available in both contexts
+- **Browser API Usage**: Uses browser.storage.local consistently
+- **Error Handling**: Graceful handling of malformed URLs and missing data
+- **Backward Compatibility**: Context menu functionality unchanged
+
+### Architecture Benefits
+- **Separation of Concerns**: UI logic vs business logic clearly separated
+- **Reusability**: Playlist functions can be used by any future components
+- **Testing**: Shared functions could be unit tested independently
+- **Consistency**: Same validation and storage patterns everywhere

+ 4 - 70
background.js

@@ -91,75 +91,9 @@ browser.runtime.onInstalled.addListener(() => {
   });
 });
 
-function findVideoInPlaylists(playlists, url) {
-  // Extract the video id from URL query params
-  let v;
-  try {
-    const urlObj = new URL(url);
-    v = urlObj.searchParams.get("v");
-  } catch (e) {
-    return false; // Invalid URL
-  }
-
-  // If no video id found, return false
-  if (!v) {
-    return false;
-  }
-
-  // Check each playlist in the current set
-  for (const playlistName in playlists) {
-    const videos = playlists[playlistName];
-
-    // Check each video in the array
-    for (let i = 0; i < videos.length; i++) {
-      try {
-        const itemUrl = new URL(videos[i].url);
-        const itemVParam = itemUrl.searchParams.get("v");
-
-        // If the "v" parameters match, return playlist info
-        if (itemVParam === v) {
-          const isLastVideo = i === videos.length - 1;
-          return [playlistName, i, isLastVideo];
-        }
-      } catch (e) {
-        // Skip malformed URLs
-        continue;
-      }
-    }
-  }
-
-  // No match found
-  return false;
-}
-
-function findPlaylist(current, url) {
-  const result = findVideoInPlaylists(current, url);
-  // If found, return just the playlist name
-  return result ? result[0] : false;
-}
-
-async function addLinkToPlaylist(plName, item) {
-  const { playlists: currentPlaylists } =
-    await browser.storage.local.get("playlists");
-
-  const alreadyHave = findPlaylist(currentPlaylists, item.linkUrl);
-
-  if (alreadyHave) {
-    console.log("already have that link in", alreadyHave);
-  } else {
-    const { [plName]: playlist, ...others } = currentPlaylists;
-    await browser.storage.local.set({
-      playlists: {
-        [plName]: [...playlist, { url: item.linkUrl, title: item.linkText }],
-        ...others,
-      },
-    });
-  }
-}
-
 // Context menu click handler
 browser.contextMenus.onClicked.addListener(async (item, _tab) => {
-  addLinkToPlaylist(item.menuItemId, item);
+  PlaylistUtils.addLinkToPlaylist(item.menuItemId, item);
 });
 
 // Function to navigate a tab to a new URL
@@ -171,8 +105,8 @@ async function updateTracking(message) {
   // Get current playlists from storage
   const { playlists } = await browser.storage.local.get("playlists");
 
-  // Find the video in playlists
-  const result = findVideoInPlaylists(playlists, message.url);
+  // Find the video in playlists using shared utils
+  const result = PlaylistUtils.findVideoInPlaylists(playlists, message.url);
 
   // If the video is not present in any playlist, do nothing
   if (!result) {
@@ -267,7 +201,7 @@ async function updateHistory(message) {
 }
 
 function findNext(playlists, url) {
-  const result = findVideoInPlaylists(playlists, url);
+  const result = PlaylistUtils.findVideoInPlaylists(playlists, url);
 
   if (!result) {
     return false; // Not found in any playlist

+ 1 - 0
codemcp.toml

@@ -9,6 +9,7 @@ project_prompt = '''
 - 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"
+  - if you have to update the file (due to a subsequent user request), append to it, do not replace it
 
 ALPINE.JS CSP COMPLIANCE: All Alpine.js code must be CSP-compliant, meaning:
 - NO JavaScript expressions in HTML attributes (x-show="variable === 0", x-text="method()", etc.)

+ 1 - 1
manifest.json

@@ -17,7 +17,7 @@
     "default_popup": "popup/popup.html"
   },
   "background": {
-    "scripts": ["background.js"]
+    "scripts": ["shared/playlist-utils.js", "background.js"]
   },
   "content_scripts": [
     {

+ 40 - 0
popup/popup.css

@@ -400,3 +400,43 @@ button:hover {
 .playlist-full-view .video-item:last-child {
   border-bottom: none;
 }
+
+/* Add current page button styles */
+.add-current-page-container {
+  margin-top: 16px;
+  padding-top: 12px;
+  border-top: 1px solid #eee;
+}
+
+.add-current-page-btn {
+  width: 100%;
+  background-color: #4285f4;
+  color: white;
+  padding: 10px 16px;
+  border-radius: 6px;
+  font-size: 14px;
+  font-weight: 500;
+  transition: all 0.2s ease;
+}
+
+.add-current-page-btn:hover:not(:disabled) {
+  background-color: #3367d6;
+  transform: translateY(-1px);
+  box-shadow: 0 2px 8px rgba(66, 133, 244, 0.3);
+}
+
+.add-current-page-btn:disabled,
+.add-current-page-btn.disabled {
+  background-color: #ccc;
+  color: #888;
+  cursor: not-allowed;
+  transform: none;
+  box-shadow: none;
+}
+
+.add-current-page-btn:disabled:hover,
+.add-current-page-btn.disabled:hover {
+  background-color: #ccc;
+  transform: none;
+  box-shadow: none;
+}

+ 9 - 0
popup/popup.html

@@ -6,6 +6,7 @@
     <title>My Playlists</title>
     <link rel="stylesheet" href="popup.css" />
     <script src="alpine.min.js" defer></script>
+    <script src="../shared/playlist-utils.js"></script>
     <script src="popup.js"></script>
   </head>
   <body>
@@ -227,6 +228,14 @@
             No videos in this playlist
           </div>
         </div>
+        <!-- Add current page button -->
+        <div class="add-current-page-container">
+          <button
+            class="add-current-page-btn"
+            x-bind="addCurrentPageButton"
+            x-text="addCurrentPageButtonText"
+          ></button>
+        </div>
       </div>
 
       <!-- Export/Import/History buttons -->

+ 73 - 2
popup/popup.js

@@ -10,10 +10,11 @@ document.addEventListener("alpine:init", () => {
   // - 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?)
-  // - option (probably a button) to add current page (url, title) to playlist
+  // X option (probably a button) to add current page (url, title) to playlist
+  //   X align with addLinkToPlaylist in background.js (no repeated videos)
   // - 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?
-  // - add personal rating feature ("enjoyed", "this was important", etc)
+  // X add personal rating feature ("enjoyed", "this was important", etc)
   Alpine.data("playlistManager", () => ({
     playlists: {},
     currentIndices: {},
@@ -24,10 +25,14 @@ document.addEventListener("alpine:init", () => {
     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
+    addCurrentPageButtonText: 'Add Current Page', // Button text
 
     init() {
       this.loadPlaylists();
       this.loadHistory();
+      this.getCurrentTab();
       // Add document click handler to close menus
       document.addEventListener('click', (e) => {
         // If click is not on a more button or menu, close all menus
@@ -109,6 +114,33 @@ document.addEventListener("alpine:init", () => {
       }
     },
 
+    async getCurrentTab() {
+      try {
+        const tabs = await browser.tabs.query({ active: true, currentWindow: true });
+        if (tabs.length > 0) {
+          this.currentTab = tabs[0];
+          const url = new URL(this.currentTab.url);
+          this.isCurrentTabYoutube = url.hostname === 'www.youtube.com';
+          this.updateAddCurrentPageButtonText();
+        }
+      } catch (error) {
+        console.error("Error getting current tab:", error);
+        this.currentTab = null;
+        this.isCurrentTabYoutube = false;
+        this.updateAddCurrentPageButtonText();
+      }
+    },
+
+    updateAddCurrentPageButtonText() {
+      if (!this.currentTab) {
+        this.addCurrentPageButtonText = 'Unable to get current page';
+      } else if (!this.isCurrentTabYoutube) {
+        this.addCurrentPageButtonText = 'Add Current Page (YouTube only)';
+      } else {
+        this.addCurrentPageButtonText = 'Add Current Page to Playlist';
+      }
+    },
+
     sortHistoryByRecentInteraction() {
       // Convert history object to array with video ID and sort by most recent interaction
       this.sortedHistory = Object.entries(this.history)
@@ -311,6 +343,30 @@ document.addEventListener("alpine:init", () => {
       }
     },
 
+    async addCurrentPageToPlaylist() {
+      if (!this.currentTab || !this.isCurrentTabYoutube || !this.currentPlaylistName) {
+        return;
+      }
+
+      // Create video object using current tab info
+      const video = {
+        url: this.currentTab.url,
+        title: this.currentTab.title,
+        status: "new"
+      };
+
+      // Use shared utility function to add video (handles duplicate checking)
+      const wasAdded = await PlaylistUtils.addVideoToPlaylist(this.currentPlaylistName, video);
+
+      if (wasAdded) {
+        // Refresh displays only if video was actually added
+        this.loadPlaylists(); // This will update both display arrays
+      } else {
+        // Could show user feedback that video already exists
+        console.log("Video already exists in a playlist");
+      }
+    },
+
     async exportPlaylists() {
       try {
         // Get current playlists
@@ -748,5 +804,20 @@ document.addEventListener("alpine:init", () => {
         };
       },
     },
+
+    // Add current page button binding
+    addCurrentPageButton: {
+      ["@click"]() {
+        this.addCurrentPageToPlaylist();
+      },
+      [":disabled"]() {
+        return !this.currentTab || !this.isCurrentTabYoutube || !this.currentPlaylistName;
+      },
+      [":class"]() {
+        return {
+          'disabled': !this.currentTab || !this.isCurrentTabYoutube || !this.currentPlaylistName
+        };
+      },
+    },
   }));
 });

+ 105 - 0
shared/playlist-utils.js

@@ -0,0 +1,105 @@
+// Shared playlist utility functions
+// Used by both popup.js and background.js
+
+const PlaylistUtils = {
+  /**
+   * Extract video ID from YouTube URL
+   * @param {string} url - YouTube URL
+   * @returns {string|null} - Video ID or null if not found
+   */
+  extractVideoId(url) {
+    try {
+      const urlObj = new URL(url);
+      return urlObj.searchParams.get("v");
+    } catch (e) {
+      return null;
+    }
+  },
+
+  /**
+   * Find a video in all playlists by URL
+   * @param {Object} playlists - All playlists object
+   * @param {string} url - Video URL to search for
+   * @returns {Array|false} - [playlistName, videoIndex, isLastVideo] or false if not found
+   */
+  findVideoInPlaylists(playlists, url) {
+    const videoId = this.extractVideoId(url);
+
+    if (!videoId) {
+      return false;
+    }
+
+    // Check each playlist
+    for (const playlistName in playlists) {
+      const videos = playlists[playlistName];
+
+      // Check each video in the playlist
+      for (let i = 0; i < videos.length; i++) {
+        const itemVideoId = this.extractVideoId(videos[i].url);
+
+        // If the video IDs match, return playlist info
+        if (itemVideoId === videoId) {
+          const isLastVideo = i === videos.length - 1;
+          return [playlistName, i, isLastVideo];
+        }
+      }
+    }
+
+    return false;
+  },
+
+  /**
+   * Find which playlist contains a video by URL
+   * @param {Object} playlists - All playlists object
+   * @param {string} url - Video URL to search for
+   * @returns {string|false} - Playlist name or false if not found
+   */
+  findPlaylist(playlists, url) {
+    const result = this.findVideoInPlaylists(playlists, url);
+    return result ? result[0] : false;
+  },
+
+  /**
+   * Add a video to a playlist if it's not already present in any playlist
+   * @param {string} playlistName - Name of playlist to add to
+   * @param {Object} video - Video object with url and title properties
+   * @returns {Promise<boolean>} - True if added, false if already exists
+   */
+  async addVideoToPlaylist(playlistName, video) {
+    const { playlists: currentPlaylists } = await browser.storage.local.get("playlists");
+
+    const alreadyExists = this.findPlaylist(currentPlaylists, video.url);
+
+    if (alreadyExists) {
+      console.log("Video already exists in playlist:", alreadyExists);
+      return false;
+    }
+
+    // Add video to the specified playlist
+    const updatedPlaylist = [...currentPlaylists[playlistName], video];
+    const updatedPlaylists = {
+      ...currentPlaylists,
+      [playlistName]: updatedPlaylist
+    };
+
+    await browser.storage.local.set({ playlists: updatedPlaylists });
+    console.log("Added video to playlist:", playlistName);
+    return true;
+  },
+
+  /**
+   * Legacy function for backward compatibility with context menu
+   * @param {string} playlistName - Name of playlist to add to
+   * @param {Object} item - Context menu item with linkUrl and linkText
+   * @returns {Promise<boolean>} - True if added, false if already exists
+   */
+  async addLinkToPlaylist(playlistName, item) {
+    const video = {
+      url: item.linkUrl,
+      title: item.linkText,
+      status: "new"
+    };
+
+    return await this.addVideoToPlaylist(playlistName, video);
+  }
+};