Browse Source

feat: add full playlist view with truncated display

in the popup menu, the playlists display all the finished videos, but these are taking up too much space. add a "full playlist" view (similar to the "history" page/tab/view) which shows only the videos of the requested playlist. make it look the same as on the main page. on the main page, navigate to the playlist view by clicking on the playlist name. on the playlist page, have a back button to return to the main page. on the main page, truncate all the videos before the current one with a single line of text that says something like "(14 previous videos)". clicking the line of text also brings you to the playlist view.

```git-revs
88581e7  (Base revision)
9b2d384  Add header for playlist view
cbb0560  Make playlist names clickable and add truncated display
1fbbd8d  Fix video item data attributes to use correct indices
3af3fbd  Add individual playlist view container after history container
40f7b5e  Add new state variables for playlist view functionality
79eb910  Add methods for truncation and playlist navigation
64fb8ae  Add event handlers for playlist name clicks and truncation clicks
90c7ee6  Add view bindings for playlist view containers
f2f8b2c  Add CSS for clickable playlist names
a59d2c3  Add CSS styling for truncated videos indicator
d1a4cd7  Add CSS styling for playlist view container
3338209  Document the changes made for the full playlist view feature
cfe91c1  Fix CSP compliance for truncated videos display
88a17bf  Replace with CSP-compliant template structure using computed data
f34e076  Fix data attributes to use new playlist data structure
aa59f33  Fix individual playlist view to be CSP compliant
4b3069a  Add CSP-compliant computed properties for playlist display
295ea49  Add method to update computed playlist display data
00a42a7  Update showPlaylist method to use computed data
ba40f57  Update removeVideo to refresh computed data
76cb574  Update moveVideoUp to refresh computed data
400950d  Update moveVideoDown to refresh computed data
59132fa  Update updatePlaylists to refresh computed data
94aadd1  Add CSP-compliant bindings for truncated videos display
1c67c38  Add playlistViewEmptyState binding
c12e950  Update documentation with CSP compliance fixes
fca4d0f  Improve truncated videos indicator styling with more spacing, padding, and visual feedback
8482b88  Move back button next to page titles for history and playlist views
52962c6  Update header layout to display back button next to title
db261e2  Add styling for arrow back button positioned next to title
HEAD     Update documentation with all changes including CSP fixes and styling improvements
```

codemcp-id: 4-feat-add-full-playlist-view-with-truncated-display
Brandon Wong 1 year ago
parent
commit
7f17f1ca0b
4 changed files with 433 additions and 18 deletions
  1. 111 0
      2025-05-28-codemcp-feat-add-full-playlist-view-with-truncated-display.md
  2. 84 0
      popup/popup.css
  3. 106 17
      popup/popup.html
  4. 132 1
      popup/popup.js

File diff suppressed because it is too large
+ 111 - 0
2025-05-28-codemcp-feat-add-full-playlist-view-with-truncated-display.md


+ 84 - 0
popup/popup.css

@@ -20,6 +20,10 @@ body {
 header {
   margin-bottom: 16px;
   text-align: center;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  position: relative;
 }
 
 h1 {
@@ -34,6 +38,17 @@ h2 {
   border-bottom: 1px solid #ddd;
 }
 
+.playlist-name-clickable {
+  cursor: pointer;
+  color: #4285f4;
+  transition: color 0.2s ease;
+}
+
+.playlist-name-clickable:hover {
+  color: #3367d6;
+  text-decoration: underline;
+}
+
 .playlist {
   background: #fff;
   border-radius: 8px;
@@ -106,6 +121,23 @@ button:hover {
   text-decoration: line-through;
 }
 
+/* Truncated videos indicator */
+.truncated-videos {
+  padding: 8px 0 12px 8px;
+  margin-bottom: 12px;
+  border-bottom: 1px solid #eee;
+  cursor: pointer;
+  color: #888;
+  font-style: italic;
+  font-size: 14px;
+  transition: color 0.2s ease, background-color 0.2s ease;
+}
+
+.truncated-videos:hover {
+  color: #4285f4;
+  background-color: #f8f9fa;
+}
+
 /* More menu styles */
 .more-menu-container {
   position: relative;
@@ -171,6 +203,30 @@ button:hover {
   background-color: #3367d6;
 }
 
+/* Arrow back button styles */
+.back-btn-arrow {
+  position: absolute;
+  left: 0;
+  background: none;
+  border: none;
+  font-size: 20px;
+  color: #4285f4;
+  cursor: pointer;
+  padding: 8px 12px;
+  border-radius: 50%;
+  transition: background-color 0.2s ease, color 0.2s ease;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 36px;
+  height: 36px;
+}
+
+.back-btn-arrow:hover {
+  background-color: #e8f0fe;
+  color: #3367d6;
+}
+
 /* History view styles */
 .history-container {
   background: #fff;
@@ -273,3 +329,31 @@ button:hover {
 .history-btn:hover {
   background-color: #2d7d3a;
 }
+
+/* Playlist view styles */
+.playlist-view-container {
+  background: #fff;
+  border-radius: 8px;
+  padding: 12px;
+  margin-bottom: 16px;
+  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+  min-height: 200px;
+}
+
+.playlist-full-view {
+  display: flex;
+  flex-direction: column;
+  gap: 0;
+}
+
+.playlist-full-view .video-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 0;
+  border-bottom: 1px solid #eee;
+}
+
+.playlist-full-view .video-item:last-child {
+  border-bottom: none;
+}

+ 106 - 17
popup/popup.html

@@ -15,24 +15,42 @@
       </header>
 
       <header x-bind="historyHeader">
-        <button class="back-btn" x-bind="backButton">← Back</button>
+        <button class="back-btn-arrow" x-bind="backButton">←</button>
         <h1>History</h1>
       </header>
 
+      <header x-bind="playlistViewHeader">
+        <button class="back-btn-arrow" x-bind="backButton">←</button>
+        <h1 x-text="currentPlaylistName"></h1>
+      </header>
+
       <div class="playlists-container" x-bind="playlistsContainer">
         <template
-          x-for="(videos, playlistName) in playlists"
-          :key="playlistName"
+          x-for="playlistData in playlistsForDisplay"
+          :key="playlistData.name"
         >
           <div class="playlist">
-            <h2 x-text="playlistName"></h2>
+            <h2
+              class="playlist-name-clickable"
+              x-text="playlistData.name"
+              :data-playlist-name="playlistData.name"
+              x-bind="playlistNameClick"
+            ></h2>
             <div class="video-list">
-              <template x-for="(video, index) in videos" :key="index">
+              <!-- Truncated previous videos indicator -->
+              <div
+                class="truncated-videos"
+                :data-playlist-name="playlistData.name"
+                x-bind="truncatedVideosDisplay"
+              >
+                <span x-text="playlistData.truncationText"></span>
+              </div>
+              <template x-for="(video, index) in playlistData.visibleVideos" :key="index">
                 <div
                   class="video-item"
                   :title="video.title"
-                  :data-playlist-name="playlistName"
-                  :data-playlist-index="index"
+                  :data-playlist-name="playlistData.name"
+                  :data-playlist-index="video.originalIndex"
                   x-bind="videoItemClass"
                 >
                   <a
@@ -43,8 +61,8 @@
                   ></a>
                   <div class="video-actions">
                     <button
-                      :data-playlist-name="playlistName"
-                      :data-playlist-index="index"
+                      :data-playlist-name="playlistData.name"
+                      :data-playlist-index="video.originalIndex"
                       x-bind="moveUpButton"
                       class="move-up-btn"
                       title="Move up"
@@ -52,8 +70,8 @@
                     </button>
                     <button
-                      :data-playlist-name="playlistName"
-                      :data-playlist-index="index"
+                      :data-playlist-name="playlistData.name"
+                      :data-playlist-index="video.originalIndex"
                       x-bind="moveDownButton"
                       class="move-down-btn"
                       title="Move down"
@@ -62,8 +80,8 @@
                     </button>
                     <div class="more-menu-container">
                       <button
-                        :data-playlist-name="playlistName"
-                        :data-playlist-index="index"
+                        :data-playlist-name="playlistData.name"
+                        :data-playlist-index="video.originalIndex"
                         x-bind="moreMenuButton"
                         class="more-btn"
                         title="More actions"
@@ -72,13 +90,13 @@
                       </button>
                       <div
                         class="more-menu"
-                        :data-playlist-name="playlistName"
-                        :data-playlist-index="index"
+                        :data-playlist-name="playlistData.name"
+                        :data-playlist-index="video.originalIndex"
                         x-bind="moreMenu"
                       >
                         <button
-                          :data-playlist-name="playlistName"
-                          :data-playlist-index="index"
+                          :data-playlist-name="playlistData.name"
+                          :data-playlist-index="video.originalIndex"
                           x-bind="removeVideoButton"
                           class="menu-item remove-item"
                         >
@@ -130,6 +148,77 @@
         </div>
       </div>
 
+      <!-- Individual playlist view -->
+      <div class="playlist-view-container" x-bind="playlistViewContainer">
+        <div class="playlist-full-view">
+          <template x-for="(video, index) in currentPlaylistVideos" :key="index">
+            <div
+              class="video-item"
+              :title="video.title"
+              :data-playlist-name="currentPlaylistName"
+              :data-playlist-index="index"
+              x-bind="videoItemClass"
+            >
+              <a
+                class="video-title"
+                :href="video.url"
+                x-text="video.title"
+                x-bind="videoPlayLink"
+              ></a>
+              <div class="video-actions">
+                <button
+                  :data-playlist-name="currentPlaylistName"
+                  :data-playlist-index="index"
+                  x-bind="moveUpButton"
+                  class="move-up-btn"
+                  title="Move up"
+                >
+                  ↑
+                </button>
+                <button
+                  :data-playlist-name="currentPlaylistName"
+                  :data-playlist-index="index"
+                  x-bind="moveDownButton"
+                  class="move-down-btn"
+                  title="Move down"
+                >
+                  ↓
+                </button>
+                <div class="more-menu-container">
+                  <button
+                    :data-playlist-name="currentPlaylistName"
+                    :data-playlist-index="index"
+                    x-bind="moreMenuButton"
+                    class="more-btn"
+                    title="More actions"
+                  >
+                    ⋯
+                  </button>
+                  <div
+                    class="more-menu"
+                    :data-playlist-name="currentPlaylistName"
+                    :data-playlist-index="index"
+                    x-bind="moreMenu"
+                  >
+                    <button
+                      :data-playlist-name="currentPlaylistName"
+                      :data-playlist-index="index"
+                      x-bind="removeVideoButton"
+                      class="menu-item remove-item"
+                    >
+                      Remove
+                    </button>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </template>
+          <div x-bind="playlistViewEmptyState" class="empty-playlist">
+            No videos in this playlist
+          </div>
+        </div>
+      </div>
+
       <!-- Export/Import/History buttons -->
       <div class="export-container" x-bind="exportContainer">
         <button class="export-btn" x-bind="exportButton">

+ 132 - 1
popup/popup.js

@@ -20,7 +20,10 @@ document.addEventListener("alpine:init", () => {
     history: {},
     sortedHistory: [],
     openMenus: new Set(), // Track which menus are open
-    currentView: 'playlists', // Track current view: 'playlists' or 'history'
+    currentView: 'playlists', // Track current view: 'playlists', 'history', or 'playlist'
+    currentPlaylistName: '', // Track which playlist is being viewed
+    playlistsForDisplay: [], // Computed array for display
+    currentPlaylistVideos: [], // Videos for current playlist view
 
     init() {
       this.loadPlaylists();
@@ -60,6 +63,39 @@ document.addEventListener("alpine:init", () => {
       } catch (error) {
         console.error("Error loading playlists:", error);
       }
+      this.updatePlaylistsForDisplay();
+    },
+
+    updatePlaylistsForDisplay() {
+      this.playlistsForDisplay = Object.entries(this.playlists).map(([playlistName, videos]) => {
+        const currentIndex = this.currentIndices[playlistName] || 0;
+        const shouldShowTruncation = currentIndex > 0;
+        const truncationText = shouldShowTruncation
+          ? `(${currentIndex} previous video${currentIndex === 1 ? '' : 's'})`
+          : '';
+
+        // Get visible videos from current index onwards with original indices
+        const visibleVideos = videos.slice(currentIndex).map((video, index) => ({
+          ...video,
+          originalIndex: currentIndex + index
+        }));
+
+        return {
+          name: playlistName,
+          videos: videos,
+          visibleVideos: visibleVideos,
+          shouldShowTruncation: shouldShowTruncation,
+          truncationText: truncationText
+        };
+      });
+    },
+
+    updateCurrentPlaylistVideos() {
+      if (!this.currentPlaylistName || !this.playlists[this.currentPlaylistName]) {
+        this.currentPlaylistVideos = [];
+      } else {
+        this.currentPlaylistVideos = this.playlists[this.currentPlaylistName];
+      }
     },
 
     async loadHistory() {
@@ -167,6 +203,8 @@ document.addEventListener("alpine:init", () => {
       try {
         await browser.storage.local.set({ playlists: updatedPlaylists });
         this.playlists = updatedPlaylists;
+        this.updatePlaylistsForDisplay();
+        this.updateCurrentPlaylistVideos();
       } catch (error) {
         console.error("Error removing video:", error);
       }
@@ -197,6 +235,8 @@ document.addEventListener("alpine:init", () => {
       try {
         await browser.storage.local.set({ playlists: updatedPlaylists });
         this.playlists = updatedPlaylists;
+        this.updatePlaylistsForDisplay();
+        this.updateCurrentPlaylistVideos();
       } catch (error) {
         console.error("Error moving video up:", error);
       }
@@ -225,6 +265,8 @@ document.addEventListener("alpine:init", () => {
       try {
         await browser.storage.local.set({ playlists: updatedPlaylists });
         this.playlists = updatedPlaylists;
+        this.updatePlaylistsForDisplay();
+        this.updateCurrentPlaylistVideos();
       } catch (error) {
         console.error("Error moving video down:", error);
       }
@@ -448,6 +490,8 @@ document.addEventListener("alpine:init", () => {
       try {
         await browser.storage.local.set({ playlists });
         this.playlists = playlists;
+        this.updatePlaylistsForDisplay();
+        this.updateCurrentPlaylistVideos();
         alert("Playlists imported successfully!");
       } catch (error) {
         console.error("Error updating playlists:", error);
@@ -506,6 +550,40 @@ document.addEventListener("alpine:init", () => {
       this.currentView = 'playlists';
     },
 
+    showPlaylist(playlistName) {
+      this.currentView = 'playlist';
+      this.currentPlaylistName = playlistName;
+      this.updateCurrentPlaylistVideos();
+    },
+
+    // Methods for truncated display
+    shouldShowTruncation(playlistName) {
+      const currentIndex = this.currentIndices[playlistName] || 0;
+      return currentIndex > 0;
+    },
+
+    getTruncationText(playlistName) {
+      const currentIndex = this.currentIndices[playlistName] || 0;
+      const count = currentIndex;
+      return `(${count} previous video${count === 1 ? '' : 's'})`;
+    },
+
+    getVisibleVideos(playlistName, videos) {
+      const currentIndex = this.currentIndices[playlistName] || 0;
+      // Show from current video onwards, but keep original indices
+      return videos.slice(currentIndex).map((video, index) => ({
+        ...video,
+        originalIndex: currentIndex + index
+      }));
+    },
+
+    getCurrentPlaylistVideos() {
+      if (!this.currentPlaylistName || !this.playlists[this.currentPlaylistName]) {
+        return [];
+      }
+      return this.playlists[this.currentPlaylistName];
+    },
+
     // Button bindings for navigation
     historyButton: {
       ["@click"]() {
@@ -519,6 +597,40 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    // Event handlers for playlist navigation
+    playlistNameClick: {
+      ["@click"]() {
+        const playlistName = this.$el.dataset.playlistName;
+        this.showPlaylist(playlistName);
+      },
+    },
+
+    truncatedVideosClick: {
+      ["@click"]() {
+        const playlistName = this.$el.dataset.playlistName;
+        this.showPlaylist(playlistName);
+      },
+    },
+
+    truncatedVideosClick: {
+      ["@click"]() {
+        const playlistName = this.$el.dataset.playlistName;
+        this.showPlaylist(playlistName);
+      },
+    },
+
+    truncatedVideosDisplay: {
+      ["x-show"]() {
+        const playlistName = this.$el.dataset.playlistName;
+        const playlistData = this.playlistsForDisplay.find(p => p.name === playlistName);
+        return playlistData ? playlistData.shouldShowTruncation : false;
+      },
+      ["@click"]() {
+        const playlistName = this.$el.dataset.playlistName;
+        this.showPlaylist(playlistName);
+      },
+    },
+
     // CSP-compliant view bindings
     playlistsHeader: {
       ["x-show"]() {
@@ -532,6 +644,12 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    playlistViewHeader: {
+      ["x-show"]() {
+        return this.currentView === 'playlist';
+      },
+    },
+
     playlistsContainer: {
       ["x-show"]() {
         return this.currentView === 'playlists';
@@ -544,6 +662,12 @@ document.addEventListener("alpine:init", () => {
       },
     },
 
+    playlistViewContainer: {
+      ["x-show"]() {
+        return this.currentView === 'playlist';
+      },
+    },
+
     exportContainer: {
       ["x-show"]() {
         return this.currentView === 'playlists';
@@ -556,5 +680,12 @@ document.addEventListener("alpine:init", () => {
         return this.sortedHistory.length === 0;
       },
     },
+
+    // Playlist view-specific bindings for CSP compliance
+    playlistViewEmptyState: {
+      ["x-show"]() {
+        return this.currentPlaylistVideos.length === 0;
+      },
+    },
   }));
 });