Browse Source

feat: populate history page with video interaction data

the newly-created history "page" (or view or tab) is currently empty. populate it with history (see background.js for details), ordered, with the most recently interacted video at the top. for each video, put the title, the "v" param (id) in parentheses, and the list of events. put human-readable timestamps.

```git-revs
8f68548  (Base revision)
d0dd647  Add history data properties and initialization
6c0db53  Add loadHistory method after loadPlaylists
bd4344d  Add history content to the history container
6a2cb9e  Add CSS styles for history items and events
a2c02b5  Update showHistory method to refresh history data
2a12a1b  Create documentation file for the history page feature
d7e812e  Add getReversedHistory method for CSP compliance
a2642b0  Update HTML template to use CSP-compliant method call
f665066  Add CSP-compliant bindings for history display
9ee046f  Fix CSP compliance for history template x-for loop
fef4cab  Replace CSP non-compliant bindings with proper binding objects
32f7684  Fix video ID display to use CSP-compliant binding
698fcba  Update sortHistoryByRecentInteraction to prepare CSP-compliant data
20e08a7  Update video ID display to use pre-processed data
4523f2b  Update events template to use pre-processed data for CSP compliance
621c99f  Remove unused CSP binding methods since we use pre-processed data
HEAD     Remove unused getReversedHistory method
```

codemcp-id: 3-feat-populate-history-page-with-video-interaction-
Brandon Wong 1 year ago
parent
commit
b2c92be0ba

+ 77 - 0
2025-05-28-codemcp-feat-populate-history-page-with-video-interaction.md

@@ -0,0 +1,77 @@
+# Populate History Page with Video Interaction Data
+
+## Context
+Date: 2025-05-28
+Feature: Populate the empty history page/view with video interaction data
+
+## User Request
+The newly-created history "page" (or view or tab) is currently empty. Populate it with history (see background.js for details), ordered, with the most recently interacted video at the top. For each video, put the title, the "v" param (id) in parentheses, and the list of events. Put human-readable timestamps.
+
+## Analysis
+From examining the codebase:
+
+1. **History Data Structure** (from background.js):
+   - History is stored in browser.storage.local with key "history"
+   - Each video is keyed by video ID (the "v" parameter from YouTube URL)
+   - Each video entry contains:
+     - `url`: Original video URL
+     - `title`: Video title
+     - `duration`: Video duration
+     - `history`: Array of interaction events
+   - Each event contains:
+     - `action`: Type of event ("play", "playing", "pause", "ended")
+     - `position`: Timestamp in video where event occurred
+     - `timestamp`: Unix timestamp when event was recorded
+
+2. **Existing UI Structure**:
+   - History view framework already exists in popup.html
+   - Navigation between playlists and history views implemented
+   - CSS container for history already present but empty
+
+## Implementation
+
+### 1. Added History Data Management (popup.js)
+- Added `history` and `sortedHistory` properties to Alpine.js data
+- Created `loadHistory()` method to fetch data from browser storage
+- Created `sortHistoryByRecentInteraction()` to sort videos by most recent interaction
+- Added utility methods:
+  - `formatTimestamp()`: Human-readable relative timestamps ("2 hours ago", etc.)
+  - `formatVideoPosition()`: Convert seconds to MM:SS format
+  - `formatActionName()`: Convert action types to human-readable names
+
+### 2. Updated HTML Structure (popup.html)
+- Populated empty history container with Alpine.js template
+- Added structured display for each video:
+  - Video title (clickable link)
+  - Video ID in parentheses
+  - List of events with action, position, and timestamp
+- Added empty state message
+
+### 3. Added CSS Styles (popup.css)
+- Comprehensive styling for history items
+- Event display with proper spacing and typography
+- Hover effects and visual hierarchy
+- Responsive layout considerations
+
+### 4. Enhanced Navigation
+- Updated `showHistory()` method to refresh data when switching views
+- Maintained existing navigation between playlists and history
+
+## Key Features Implemented
+1. **Chronological Ordering**: Videos sorted by most recent interaction
+2. **Comprehensive Event Display**: Each event shows action type, video position, and human-readable timestamp
+3. **Interactive Elements**: Video titles are clickable links that open in new tabs
+4. **Clean UI**: Well-structured layout with proper visual hierarchy
+5. **Empty State Handling**: Friendly message when no history exists
+
+## Files Modified
+- `popup/popup.js`: Added history data management and formatting methods
+- `popup/popup.html`: Populated history container with structured content
+- `popup/popup.css`: Added comprehensive styling for history display
+
+## Testing Considerations
+- Verify history loads correctly from browser storage
+- Test sorting by most recent interaction
+- Confirm human-readable timestamps display properly
+- Validate video links open correctly
+- Check empty state display when no history exists

+ 80 - 0
popup/popup.css

@@ -181,6 +181,86 @@ button:hover {
   min-height: 200px;
 }
 
+.history-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.history-item {
+  border-bottom: 1px solid #eee;
+  padding-bottom: 12px;
+}
+
+.history-item:last-child {
+  border-bottom: none;
+  padding-bottom: 0;
+}
+
+.history-video-info {
+  margin-bottom: 8px;
+}
+
+.history-video-title {
+  display: block;
+  font-weight: bold;
+  color: #333;
+  text-decoration: none;
+  margin-bottom: 4px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.history-video-title:hover {
+  color: #4285f4;
+}
+
+.history-video-id {
+  font-size: 12px;
+  color: #666;
+  font-family: monospace;
+}
+
+.history-events {
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+  margin-left: 12px;
+}
+
+.history-event {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  font-size: 12px;
+  color: #555;
+}
+
+.event-action {
+  font-weight: bold;
+  min-width: 60px;
+}
+
+.event-position {
+  color: #666;
+  font-family: monospace;
+  min-width: 70px;
+}
+
+.event-timestamp {
+  color: #888;
+  font-size: 11px;
+  margin-left: auto;
+}
+
+.empty-history {
+  text-align: center;
+  padding: 40px 0;
+  color: #888;
+  font-style: italic;
+}
+
 /* History button styles */
 .history-btn {
   background-color: #34a853;

+ 27 - 1
popup/popup.html

@@ -101,7 +101,33 @@
 
       <!-- History view -->
       <div class="history-container" x-bind="historyContainer">
-        <!-- History content will go here -->
+        <div class="history-list">
+          <template x-for="videoHistory in sortedHistory" :key="videoHistory.videoId">
+            <div class="history-item" :data-video-id="videoHistory.videoId">
+              <div class="history-video-info">
+                <a
+                  class="history-video-title"
+                  :href="videoHistory.url"
+                  x-text="videoHistory.title"
+                  x-bind="videoPlayLink"
+                ></a>
+                <div class="history-video-id" x-text="videoHistory.formattedVideoId"></div>
+              </div>
+              <div class="history-events">
+                <template x-for="event in videoHistory.processedEvents" :key="event.uniqueKey">
+                  <div class="history-event">
+                    <span class="event-action" x-text="event.formattedAction"></span>
+                    <span class="event-position" x-text="event.formattedPosition"></span>
+                    <span class="event-timestamp" x-text="event.formattedTimestamp"></span>
+                  </div>
+                </template>
+              </div>
+            </div>
+          </template>
+          <div x-bind="historyEmptyState" class="empty-history">
+            No video history yet
+          </div>
+        </div>
       </div>
 
       <!-- Export/Import/History buttons -->

+ 85 - 0
popup/popup.js

@@ -17,11 +17,14 @@ document.addEventListener("alpine:init", () => {
   Alpine.data("playlistManager", () => ({
     playlists: {},
     currentIndices: {},
+    history: {},
+    sortedHistory: [],
     openMenus: new Set(), // Track which menus are open
     currentView: 'playlists', // Track current view: 'playlists' or 'history'
 
     init() {
       this.loadPlaylists();
+      this.loadHistory();
       // Add document click handler to close menus
       document.addEventListener('click', (e) => {
         // If click is not on a more button or menu, close all menus
@@ -59,6 +62,80 @@ document.addEventListener("alpine:init", () => {
       }
     },
 
+    async loadHistory() {
+      try {
+        const result = await browser.storage.local.get("history");
+        console.log("LOAD HISTORY RESULT", result.history);
+        this.history = result.history || {};
+        this.sortHistoryByRecentInteraction();
+      } catch (error) {
+        console.error("Error loading history:", error);
+      }
+    },
+
+    sortHistoryByRecentInteraction() {
+      // Convert history object to array with video ID and sort by most recent interaction
+      this.sortedHistory = Object.entries(this.history)
+        .map(([videoId, videoData]) => {
+          const lastInteraction = videoData.history.length > 0
+            ? Math.max(...videoData.history.map(event => event.timestamp))
+            : 0;
+
+          // Pre-process events with formatted data for CSP compliance
+          const processedEvents = videoData.history
+            .slice()
+            .reverse()
+            .map((event, index) => ({
+              ...event,
+              formattedAction: this.formatActionName(event.action),
+              formattedPosition: `at ${this.formatVideoPosition(event.position)}`,
+              formattedTimestamp: this.formatTimestamp(event.timestamp),
+              uniqueKey: `${videoId}-${event.timestamp}-${index}`
+            }));
+
+          return {
+            videoId,
+            formattedVideoId: `(${videoId})`,
+            ...videoData,
+            lastInteraction,
+            processedEvents
+          };
+        })
+        .sort((a, b) => b.lastInteraction - a.lastInteraction);
+    },
+
+    formatTimestamp(timestamp) {
+      const date = new Date(timestamp);
+      const now = new Date();
+      const diffMs = now - date;
+      const diffMins = Math.floor(diffMs / (1000 * 60));
+      const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
+      const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
+
+      if (diffMins < 1) return 'Just now';
+      if (diffMins < 60) return `${diffMins} minute${diffMins > 1 ? 's' : ''} ago`;
+      if (diffHours < 24) return `${diffHours} hour${diffHours > 1 ? 's' : ''} ago`;
+      if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? 's' : ''} ago`;
+
+      return date.toLocaleDateString() + ' ' + date.toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
+    },
+
+    formatVideoPosition(seconds) {
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = Math.floor(seconds % 60);
+      return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}`;
+    },
+
+    formatActionName(action) {
+      const actionMap = {
+        'play': 'Started',
+        'playing': 'Playing',
+        'pause': 'Paused',
+        'ended': 'Finished'
+      };
+      return actionMap[action] || action;
+    },
+
     formatPlaylistName(name) {
       // Convert "listening-1" to "Listening - 1"
       return name
@@ -422,6 +499,7 @@ document.addEventListener("alpine:init", () => {
     // View navigation methods
     showHistory() {
       this.currentView = 'history';
+      this.loadHistory(); // Refresh history data when switching to history view
     },
 
     showPlaylists() {
@@ -471,5 +549,12 @@ document.addEventListener("alpine:init", () => {
         return this.currentView === 'playlists';
       },
     },
+
+    // History-specific bindings for CSP compliance
+    historyEmptyState: {
+      ["x-show"]() {
+        return this.sortedHistory.length === 0;
+      },
+    },
   }));
 });