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 // - periodically remove watched videos // - 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) // - view playback history (?) // - 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 // - 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) Alpine.data("playlistManager", () => ({ playlists: {}, currentIndices: {}, init() { this.loadPlaylists(); }, 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); } }, 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; } 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; } 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; } catch (error) { console.error("Error moving video down:", error); } }, 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 || {}; // Create export data object const exportData = { playlists, playbackHistory, 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 }); }, }, isCurrentVideo(playlistName, index) { const currentIndex = this.currentIndices[playlistName]; return currentIndex === index; }, isDoneVideo(playlistName, index) { const currentIndex = this.currentIndices[playlistName]; return index < currentIndex; }, 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", }; }, }, removeVideoButton: { ["@click"]() { this.removeVideo( this.$el.dataset.playlistName, this.$el.dataset.playlistIndex, ); }, }, 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"]() { document.getElementById("import-file-input").click(); }, }, importFile(event) { const file = event.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (e) => { try { const importedData = JSON.parse(e.target.result); this.validateAndImportPlaylists(importedData); } catch (error) { console.error("Error parsing JSON file:", error); alert( "Invalid JSON file. Please select a valid playlist export file.", ); } finally { // Reset the file input so the same file can be selected again event.target.value = ""; } }; reader.readAsText(file); }, 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; alert("Playlists imported successfully!"); } catch (error) { console.error("Error updating playlists:", error); alert("Error importing playlists: " + error.message); } }, })); });