document.addEventListener("alpine:init", () => { //TODO // - preserve playlist order (sort) // - "play/resume playlist" button (pick up where you left off) // X track timestamp to truly resume where you left off // X periodically remove watched videos // 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 (?) // X include currently open tabs in export // - support playlist management (add, remove, rename playlists) // - support dynamic (on-demand) playlists // - support input text box for adding to playlist (how to handle title?) // - or raw json editing // X option (probably a button) to add current page (url, title) to playlist // X align with addLinkToPlaylist in background.js (no repeated videos) // X 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 // X button to copy current video to clipboard in wikitext format (for personal wiki list) Alpine.data("playlistManager", () => ({ playlists: {}, currentIndices: {}, history: {}, sortedHistory: [], openMenus: new Set(), // Track which menus are open currentView: "playlists", // Track current view: 'playlists', 'history', 'playlist', 'saveChannel', or 'wikiInsp' 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 // Wiki/insp properties wikitextCommand: "", // Generated wikitext isCurrentVideoYoutube: false, // Whether current video is YouTube hasVideoEnded: false, // Whether video has ended currentVideoTimestamp: "", // Current video timestamp includeTimestamp: true, // Whether to include timestamp line // Import properties importText: "", // Text area content for import 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 if (!e.target.closest(".more-menu-container")) { this.closeAllMenus(); } }); }, async loadPlaylists() { try { const result = await browser.storage.local.get("playlists"); console.log("LOAD RESULT", result.playlists); this.playlists = result.playlists || {}; if (result.playlists) { this.currentIndices = Object.keys(result.playlists).reduce( (acc, pln) => { const ind = result.playlists[pln].findIndex( (v) => v.status !== "done", ); if (ind === -1) { acc[pln] = result.playlists[pln].length; } else { acc[pln] = ind; } return acc; }, {}, ); } else { this.currentIndices = {}; } } 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, doneButtonText: video.status === "done" ? "Remove Done Status" : "Mark as Done", isNonContiguousDone: this.isNonContiguousDone( playlistName, 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 ].map((video, index) => ({ ...video, doneButtonText: video.status === "done" ? "Remove Done Status" : "Mark as Done", isNonContiguousDone: this.isNonContiguousDone( this.currentPlaylistName, index, ), })); } }, 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); } }, 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.isCurrentTabChannelPage = this.isCurrentTabYoutube && url.pathname.includes("/videos"); this.isCurrentVideoYoutube = this.isCurrentTabYoutube && url.pathname.includes("/watch"); // Get video timestamp if on YouTube video page if (this.isCurrentVideoYoutube) { await this.getVideoTimestamp(); } this.updateAddCurrentPageButtonText(); this.updateCurlCommand(); this.updateWikitextCommand(); } } catch (error) { console.error("Error getting current tab:", error); this.currentTab = null; this.isCurrentTabYoutube = false; this.isCurrentTabChannelPage = false; this.isCurrentVideoYoutube = false; this.updateAddCurrentPageButtonText(); this.updateCurlCommand(); this.updateWikitextCommand(); } }, async getVideoTimestamp() { try { // Execute script in the current tab to get video timestamp const results = await browser.tabs.executeScript(this.currentTab.id, { code: ` (function() { const video = document.querySelector('video'); if (video) { const currentTime = video.currentTime; const duration = video.duration; const ended = video.ended; const formatTime = (seconds) => { const hours = Math.floor(seconds / 3600); const minutes = Math.floor((seconds % 3600) / 60); const remainingSeconds = Math.floor(seconds % 60); if (hours > 0) { return hours + ':' + minutes.toString().padStart(2, '0') + ':' + remainingSeconds.toString().padStart(2, '0'); } else { return minutes + ':' + remainingSeconds.toString().padStart(2, '0'); } }; return { currentTime: currentTime, formattedTime: formatTime(currentTime), duration: duration, ended: ended, isLongVideo: duration > 3600 }; } return null; })(); ` }); if (results && results[0]) { const videoData = results[0]; this.currentVideoTimestamp = videoData.formattedTime; this.hasVideoEnded = videoData.ended; } else { this.currentVideoTimestamp = "0:00"; this.hasVideoEnded = false; } } catch (error) { console.error("Error getting video timestamp:", error); this.currentVideoTimestamp = "0:00"; this.hasVideoEnded = false; } }, 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"; } }, 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(); }, showWikiInsp() { this.currentView = "wikiInsp"; // Refresh timestamp when opening the view if (this.isCurrentVideoYoutube) { this.getVideoTimestamp().then(() => { this.updateWikitextCommand(); }); } else { this.updateWikitextCommand(); } }, showImport() { this.currentView = "import"; }, async refreshVideoTimestamp() { if (this.isCurrentVideoYoutube) { await this.getVideoTimestamp(); this.updateWikitextCommand(); } }, updateWikitextCommand() { if (!this.currentTab || !this.isCurrentVideoYoutube) { this.wikitextCommand = "This feature is only available on YouTube videos."; return; } const today = new Date(); const dateString = today.toISOString().split('T')[0]; // YYYY-MM-DD format const title = this.currentTab.title.replace(" - YouTube", "").replace(/\|/g, "-"); const url = this.currentTab.url; let wikitext = `!!! ${dateString}\n* [[${title}|${url}]] (yt)`; // Add timestamp if user has enabled it and video hasn't ended if (this.includeTimestamp && !this.hasVideoEnded) { wikitext += `\n** ${this.currentVideoTimestamp || "0:00"} - `; } this.wikitextCommand = wikitext; }, async copyWikitextCommand() { try { await navigator.clipboard.writeText(this.wikitextCommand); console.log("Wikitext 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.wikitextCommand; document.body.appendChild(textArea); textArea.focus(); textArea.select(); document.execCommand("copy"); document.body.removeChild(textArea); console.log("Wikitext copied to clipboard (fallback)"); } catch (fallbackError) { console.error("Fallback copy failed:", fallbackError); } } }, toggleTimestamp() { this.includeTimestamp = !this.includeTimestamp; this.updateWikitextCommand(); }, 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, tags: videoData.tags || [], // Ensure tags array exists }; }) .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")}`; }, 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", playing: "Playing", pause: "Paused", ended: "Finished", }; return actionMap[action] || action; }, formatPlaylistName(name) { // Convert "listening-1" to "Listening - 1" return name .split("-") .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) .join(" - "); }, openVideo(url) { browser.tabs.create({ url }); }, async removeVideo(playlistName, index) { const playlists = JSON.parse(JSON.stringify(this.playlists)); // Make a copy of the current playlist const playlist = [...playlists[playlistName]]; // Remove the video at the specified index playlist.splice(index, 1); // Create an updated playlists object with the remaining playlists unchanged 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 removing video:", error); } }, async moveVideoUp(playlistName, index) { // Can't move the first item up if (index <= 0) return; const playlists = JSON.parse(JSON.stringify(this.playlists)); // Make a copy of the current playlist const playlist = [...playlists[playlistName]]; // Swap the video with the one above it [playlist[index], playlist[index - 1]] = [ playlist[index - 1], playlist[index], ]; // 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 moving video up:", error); } }, async moveVideoDown(playlistName, index) { const playlists = JSON.parse(JSON.stringify(this.playlists)); const playlist = [...playlists[playlistName]]; // Can't move the last item down if (index >= playlist.length - 1) return; // Swap the video with the one below it [playlist[index], playlist[index + 1]] = [ playlist[index + 1], playlist[index], ]; // 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 moving video down:", error); } }, 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 || !this.isCurrentTabYoutube || !this.currentPlaylistName ) { return; } // Create video object using current tab info const video = { url: this.currentTab.url, title: this.currentTab.title, }; // 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 const playlistResult = await browser.storage.local.get("playlists"); const playlists = playlistResult.playlists || {}; // Get playback history 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(), }; // Convert to JSON const jsonString = JSON.stringify(exportData, null, 2); // Create download const blob = new Blob([jsonString], { type: "application/json" }); const url = URL.createObjectURL(blob); // Trigger download const a = document.createElement("a"); a.href = url; a.download = `playlists-export-${new Date().toISOString().split("T")[0]}.json`; document.body.appendChild(a); a.click(); // Clean up setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(url); }, 100); } catch (error) { console.error("Error exporting playlists:", error); } }, videotitle: { ["@click"]() { console.log("TITLE CLICK", this.$el); }, }, videoPlayLink: { ["@click.prevent"]() { browser.tabs.update({ url: this.$el.href }); }, }, 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; }, isDoneVideo(playlistName, index) { const currentIndex = this.currentIndices[playlistName]; 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; const index = parseInt(this.$el.dataset.playlistIndex); const video = this.playlists[playlistName][index]; return { "current-video": this.isCurrentVideo(playlistName, index), "done-video": this.isDoneVideo(playlistName, index) && video.status === "done", "non-contiguous-done-video": this.isNonContiguousDone( playlistName, index, ), }; }, }, removeVideoButton: { ["@click"]() { this.removeVideo( this.$el.dataset.playlistName, this.$el.dataset.playlistIndex, ); this.closeAllMenus(); }, }, toggleVideoDoneButton: { ["@click"]() { this.toggleVideoDoneStatus( this.$el.dataset.playlistName, parseInt(this.$el.dataset.playlistIndex), ); this.closeAllMenus(); }, }, moveUpButton: { ["@click"]() { this.moveVideoUp( this.$el.dataset.playlistName, parseInt(this.$el.dataset.playlistIndex), ); }, [":disabled"]() { return parseInt(this.$el.dataset.playlistIndex) === 0; }, }, moveDownButton: { ["@click"]() { this.moveVideoDown( this.$el.dataset.playlistName, parseInt(this.$el.dataset.playlistIndex), ); }, [":disabled"]() { return ( parseInt(this.$el.dataset.playlistIndex) === this.playlists[this.$el.dataset.playlistName].length - 1 ); }, }, exportButton: { ["@click"]() { this.exportPlaylists(); }, }, importButton: { ["@click"]() { this.showImport(); }, }, async handleImport() { if (!this.importText.trim()) { alert("Please paste your JSON export data"); return; } try { const importedData = JSON.parse(this.importText); this.validateAndImportPlaylists(importedData); this.importText = ""; } catch (error) { console.error("Error parsing JSON:", error); alert( "Invalid JSON format. Please paste valid playlist export data.", ); } }, validateAndImportPlaylists(data) { // Validate the playlists structure if (!data.playlists || typeof data.playlists !== "object") { alert("Invalid file format: Missing or invalid 'playlists' property"); return; } // Validate each playlist const validPlaylists = {}; let hasErrors = false; for (const [playlistName, videos] of Object.entries(data.playlists)) { // Check if videos is an array if (!Array.isArray(videos)) { console.error( `Playlist '${playlistName}' does not contain a valid array of videos`, ); hasErrors = true; continue; } // Validate each video in the playlist const validVideos = videos.filter((video) => { if (!video || typeof video !== "object") { console.error(`Invalid video object in '${playlistName}'`); return false; } if ( !video.url || typeof video.url !== "string" || !video.title || typeof video.title !== "string" ) { console.error( `Video in '${playlistName}' missing required properties (url, title)`, ); return false; } return true; }); // Add the validated playlist if it has valid videos if (validVideos.length > 0) { validPlaylists[playlistName] = validVideos; } } if (Object.keys(validPlaylists).length === 0) { alert("No valid playlists found in the import file"); return; } if (hasErrors) { const confirmImport = confirm( "Some playlists or videos were invalid and will be skipped. Do you want to continue with the import?", ); if (!confirmImport) return; } // Update storage and state this.updatePlaylists(validPlaylists); }, async updatePlaylists(playlists) { 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); alert("Error importing playlists: " + error.message); } }, getMenuId(playlistName, index) { return `${playlistName}-${index}`; }, isMenuOpen(playlistName, index) { return this.openMenus.has(this.getMenuId(playlistName, index)); }, toggleMenu(playlistName, index) { const menuId = this.getMenuId(playlistName, index); if (this.openMenus.has(menuId)) { this.openMenus.delete(menuId); } else { // Close all other menus first this.openMenus.clear(); this.openMenus.add(menuId); } }, closeAllMenus() { this.openMenus.clear(); }, moreMenuButton: { ["@click.stop"]() { this.toggleMenu( this.$el.dataset.playlistName, parseInt(this.$el.dataset.playlistIndex), ); }, }, moreMenu: { ["x-show"]() { return this.isMenuOpen( this.$el.dataset.playlistName, parseInt(this.$el.dataset.playlistIndex), ); }, }, // View navigation methods showHistory() { this.currentView = "history"; this.loadHistory(); // Refresh history data when switching to history view }, showPlaylists() { 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"]() { this.showHistory(); }, }, saveChannelButton: { ["@click"]() { this.showSaveChannel(); }, }, wikiInspButton: { ["@click"]() { this.showWikiInsp(); }, }, backButton: { ["@click"]() { this.showPlaylists(); }, }, // 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"]() { return this.currentView === "playlists"; }, }, historyHeader: { ["x-show"]() { return this.currentView === "history"; }, }, playlistViewHeader: { ["x-show"]() { return this.currentView === "playlist"; }, }, saveChannelHeader: { ["x-show"]() { return this.currentView === "saveChannel"; }, }, wikiInspHeader: { ["x-show"]() { return this.currentView === "wikiInsp"; }, }, importHeader: { ["x-show"]() { return this.currentView === "import"; }, }, playlistsContainer: { ["x-show"]() { return this.currentView === "playlists"; }, }, historyContainer: { ["x-show"]() { return this.currentView === "history"; }, }, playlistViewContainer: { ["x-show"]() { return this.currentView === "playlist"; }, }, saveChannelContainer: { ["x-show"]() { return this.currentView === "saveChannel"; }, }, wikiInspContainer: { ["x-show"]() { return this.currentView === "wikiInsp"; }, }, importContainer: { ["x-show"]() { return this.currentView === "import"; }, }, exportContainer: { ["x-show"]() { return this.currentView === "playlists"; }, }, // History-specific bindings for CSP compliance historyEmptyState: { ["x-show"]() { return this.sortedHistory.length === 0; }, }, // Playlist view-specific bindings for CSP compliance playlistViewEmptyState: { ["x-show"]() { 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), }; }, }, // Save Channel specific bindings copyCurlButton: { ["@click"]() { this.copyCurlCommand(); }, [":disabled"]() { return !this.isCurrentTabChannelPage; }, }, // Wiki/insp specific bindings copyWikitextButton: { ["@click"]() { this.copyWikitextCommand(); }, [":disabled"]() { return !this.isCurrentVideoYoutube; }, }, timestampCheckbox: { ["@change"]() { this.toggleTimestamp(); }, [":checked"]() { return this.includeTimestamp; }, }, refreshTimestampButton: { ["@click"]() { this.refreshVideoTimestamp(); }, [":disabled"]() { return !this.isCurrentVideoYoutube; }, }, 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; }, }, wikiInspNotice: { ["x-show"]() { return !this.isCurrentVideoYoutube; }, }, intervalInput: { [":value"]() { return this.checkInterval; }, ["@input"]() { this.updateCheckInterval(this.$el.value); }, }, // 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, }; }, }, // Import view bindings importTextarea: { [":value"]() { return this.importText; }, ["@input"]() { this.importText = this.$el.value; }, }, importSubmitButton: { ["@click"]() { this.handleImport(); }, }, })); });