|
@@ -9,21 +9,23 @@ document.addEventListener("alpine:init", () => {
|
|
|
// X view playback history (?)
|
|
// X view playback history (?)
|
|
|
// X include currently open tabs in export
|
|
// X include currently open tabs in export
|
|
|
// - support playlist management (add, remove, rename playlists)
|
|
// - support playlist management (add, remove, rename playlists)
|
|
|
|
|
+ // - support dynamic (on-demand) playlists
|
|
|
// - support input text box for adding to playlist (how to handle title?)
|
|
// - support input text box for adding to playlist (how to handle title?)
|
|
|
// - or raw json editing
|
|
// - or raw json editing
|
|
|
// X 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)
|
|
// 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)
|
|
|
|
|
|
|
+ // 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?
|
|
// - long-term: replace youtube page? rss feeds? need server?
|
|
|
// X add personal rating feature ("enjoyed", "this was important", etc)
|
|
// X add personal rating feature ("enjoyed", "this was important", etc)
|
|
|
// - option to move video (including status) to another playlist
|
|
// - 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", () => ({
|
|
Alpine.data("playlistManager", () => ({
|
|
|
playlists: {},
|
|
playlists: {},
|
|
|
currentIndices: {},
|
|
currentIndices: {},
|
|
|
history: {},
|
|
history: {},
|
|
|
sortedHistory: [],
|
|
sortedHistory: [],
|
|
|
openMenus: new Set(), // Track which menus are open
|
|
openMenus: new Set(), // Track which menus are open
|
|
|
- currentView: "playlists", // Track current view: 'playlists', 'history', 'playlist', or 'saveChannel'
|
|
|
|
|
|
|
+ currentView: "playlists", // Track current view: 'playlists', 'history', 'playlist', 'saveChannel', or 'wikiInsp'
|
|
|
currentPlaylistName: "", // Track which playlist is being viewed
|
|
currentPlaylistName: "", // Track which playlist is being viewed
|
|
|
playlistsForDisplay: [], // Computed array for display
|
|
playlistsForDisplay: [], // Computed array for display
|
|
|
currentPlaylistVideos: [], // Videos for current playlist view
|
|
currentPlaylistVideos: [], // Videos for current playlist view
|
|
@@ -52,6 +54,16 @@ document.addEventListener("alpine:init", () => {
|
|
|
curlCommand: "", // Generated curl command
|
|
curlCommand: "", // Generated curl command
|
|
|
checkInterval: 14, // Check interval in days
|
|
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() {
|
|
init() {
|
|
|
this.loadPlaylists();
|
|
this.loadPlaylists();
|
|
|
this.loadHistory();
|
|
this.loadHistory();
|
|
@@ -172,16 +184,79 @@ document.addEventListener("alpine:init", () => {
|
|
|
this.isCurrentTabYoutube = url.hostname === "www.youtube.com";
|
|
this.isCurrentTabYoutube = url.hostname === "www.youtube.com";
|
|
|
this.isCurrentTabChannelPage =
|
|
this.isCurrentTabChannelPage =
|
|
|
this.isCurrentTabYoutube && url.pathname.includes("/videos");
|
|
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.updateAddCurrentPageButtonText();
|
|
|
this.updateCurlCommand();
|
|
this.updateCurlCommand();
|
|
|
|
|
+ this.updateWikitextCommand();
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
console.error("Error getting current tab:", error);
|
|
console.error("Error getting current tab:", error);
|
|
|
this.currentTab = null;
|
|
this.currentTab = null;
|
|
|
this.isCurrentTabYoutube = false;
|
|
this.isCurrentTabYoutube = false;
|
|
|
this.isCurrentTabChannelPage = false;
|
|
this.isCurrentTabChannelPage = false;
|
|
|
|
|
+ this.isCurrentVideoYoutube = false;
|
|
|
this.updateAddCurrentPageButtonText();
|
|
this.updateAddCurrentPageButtonText();
|
|
|
this.updateCurlCommand();
|
|
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;
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
@@ -250,6 +325,79 @@ document.addEventListener("alpine:init", () => {
|
|
|
this.updateCurlCommand();
|
|
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() {
|
|
sortHistoryByRecentInteraction() {
|
|
|
// Convert history object to array with video ID and sort by most recent interaction
|
|
// Convert history object to array with video ID and sort by most recent interaction
|
|
|
this.sortedHistory = Object.entries(this.history)
|
|
this.sortedHistory = Object.entries(this.history)
|
|
@@ -459,6 +607,69 @@ document.addEventListener("alpine:init", () => {
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ async moveVideoToTop(playlistName, index) {
|
|
|
|
|
+ const playlists = JSON.parse(JSON.stringify(this.playlists));
|
|
|
|
|
+ const playlist = [...playlists[playlistName]];
|
|
|
|
|
+
|
|
|
|
|
+ const topPosition = this.findTopPosition(playlistName);
|
|
|
|
|
+
|
|
|
|
|
+ if (index === topPosition) return;
|
|
|
|
|
+
|
|
|
|
|
+ const video = playlist.splice(index, 1)[0];
|
|
|
|
|
+ playlist.splice(topPosition, 0, video);
|
|
|
|
|
+
|
|
|
|
|
+ const updatedPlaylists = {
|
|
|
|
|
+ ...playlists,
|
|
|
|
|
+ [playlistName]: playlist,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await browser.storage.local.set({ playlists: updatedPlaylists });
|
|
|
|
|
+ this.playlists = updatedPlaylists;
|
|
|
|
|
+ this.updatePlaylistsForDisplay();
|
|
|
|
|
+ this.updateCurrentPlaylistVideos();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Error moving video to top:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ async moveVideoToBottom(playlistName, index) {
|
|
|
|
|
+ const playlists = JSON.parse(JSON.stringify(this.playlists));
|
|
|
|
|
+ const playlist = [...playlists[playlistName]];
|
|
|
|
|
+
|
|
|
|
|
+ if (index === playlist.length - 1) return;
|
|
|
|
|
+
|
|
|
|
|
+ const video = playlist.splice(index, 1)[0];
|
|
|
|
|
+ playlist.push(video);
|
|
|
|
|
+
|
|
|
|
|
+ const updatedPlaylists = {
|
|
|
|
|
+ ...playlists,
|
|
|
|
|
+ [playlistName]: playlist,
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await browser.storage.local.set({ playlists: updatedPlaylists });
|
|
|
|
|
+ this.playlists = updatedPlaylists;
|
|
|
|
|
+ this.updatePlaylistsForDisplay();
|
|
|
|
|
+ this.updateCurrentPlaylistVideos();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error("Error moving video to bottom:", error);
|
|
|
|
|
+ }
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ findTopPosition(playlistName) {
|
|
|
|
|
+ const playlist = this.playlists[playlistName];
|
|
|
|
|
+ if (!playlist) return 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (let i = 0; i < playlist.length; i++) {
|
|
|
|
|
+ if (playlist[i].status !== "done") {
|
|
|
|
|
+ return i;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return playlist.length;
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
async toggleVideoDoneStatus(playlistName, index) {
|
|
async toggleVideoDoneStatus(playlistName, index) {
|
|
|
const playlists = JSON.parse(JSON.stringify(this.playlists));
|
|
const playlists = JSON.parse(JSON.stringify(this.playlists));
|
|
|
const playlist = [...playlists[playlistName]];
|
|
const playlist = [...playlists[playlistName]];
|
|
@@ -699,45 +910,66 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- exportButton: {
|
|
|
|
|
|
|
+ moveToTopButton: {
|
|
|
["@click"]() {
|
|
["@click"]() {
|
|
|
- this.exportPlaylists();
|
|
|
|
|
|
|
+ this.moveVideoToTop(
|
|
|
|
|
+ this.$el.dataset.playlistName,
|
|
|
|
|
+ parseInt(this.$el.dataset.playlistIndex),
|
|
|
|
|
+ );
|
|
|
|
|
+ this.closeAllMenus();
|
|
|
|
|
+ },
|
|
|
|
|
+ [":disabled"]() {
|
|
|
|
|
+ const index = parseInt(this.$el.dataset.playlistIndex);
|
|
|
|
|
+ const playlistName = this.$el.dataset.playlistName;
|
|
|
|
|
+ const topPosition = this.findTopPosition(playlistName);
|
|
|
|
|
+ return index === topPosition;
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- importButton: {
|
|
|
|
|
|
|
+ moveToBottomButton: {
|
|
|
["@click"]() {
|
|
["@click"]() {
|
|
|
- document.getElementById("import-file-input").click();
|
|
|
|
|
|
|
+ this.moveVideoToBottom(
|
|
|
|
|
+ this.$el.dataset.playlistName,
|
|
|
|
|
+ parseInt(this.$el.dataset.playlistIndex),
|
|
|
|
|
+ );
|
|
|
|
|
+ this.closeAllMenus();
|
|
|
|
|
+ },
|
|
|
|
|
+ [":disabled"]() {
|
|
|
|
|
+ return (
|
|
|
|
|
+ parseInt(this.$el.dataset.playlistIndex) ===
|
|
|
|
|
+ this.playlists[this.$el.dataset.playlistName].length - 1
|
|
|
|
|
+ );
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- importFileInput: {
|
|
|
|
|
- ["@change"]() {
|
|
|
|
|
- this.importFile(this.$event);
|
|
|
|
|
|
|
+ exportButton: {
|
|
|
|
|
+ ["@click"]() {
|
|
|
|
|
+ this.exportPlaylists();
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- importFile(event) {
|
|
|
|
|
- const file = event.target.files[0];
|
|
|
|
|
- if (!file) return;
|
|
|
|
|
|
|
+ importButton: {
|
|
|
|
|
+ ["@click"]() {
|
|
|
|
|
+ this.showImport();
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
|
|
|
- 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 = "";
|
|
|
|
|
- }
|
|
|
|
|
- };
|
|
|
|
|
|
|
+ async handleImport() {
|
|
|
|
|
+ if (!this.importText.trim()) {
|
|
|
|
|
+ alert("Please paste your JSON export data");
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
- reader.readAsText(file);
|
|
|
|
|
|
|
+ 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) {
|
|
validateAndImportPlaylists(data) {
|
|
@@ -761,7 +993,7 @@ document.addEventListener("alpine:init", () => {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Validate each video in the playlist
|
|
|
|
|
|
|
+ // Validate each video in the playlist and filter out done videos
|
|
|
const validVideos = videos.filter((video) => {
|
|
const validVideos = videos.filter((video) => {
|
|
|
if (!video || typeof video !== "object") {
|
|
if (!video || typeof video !== "object") {
|
|
|
console.error(`Invalid video object in '${playlistName}'`);
|
|
console.error(`Invalid video object in '${playlistName}'`);
|
|
@@ -780,6 +1012,10 @@ document.addEventListener("alpine:init", () => {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ if (video.status === "done") {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return true;
|
|
return true;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -802,18 +1038,40 @@ document.addEventListener("alpine:init", () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Update storage and state
|
|
// Update storage and state
|
|
|
- this.updatePlaylists(validPlaylists);
|
|
|
|
|
|
|
+ this.appendToPlaylists(validPlaylists);
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
- async updatePlaylists(playlists) {
|
|
|
|
|
|
|
+ async appendToPlaylists(importedPlaylists) {
|
|
|
try {
|
|
try {
|
|
|
- await browser.storage.local.set({ playlists });
|
|
|
|
|
- this.playlists = playlists;
|
|
|
|
|
|
|
+ const currentPlaylists = JSON.parse(JSON.stringify(this.playlists));
|
|
|
|
|
+ let addedCount = 0;
|
|
|
|
|
+ let skippedCount = 0;
|
|
|
|
|
+
|
|
|
|
|
+ for (const [playlistName, importedVideos] of Object.entries(importedPlaylists)) {
|
|
|
|
|
+ if (!currentPlaylists[playlistName]) {
|
|
|
|
|
+ currentPlaylists[playlistName] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ for (const video of importedVideos) {
|
|
|
|
|
+ const alreadyExists = PlaylistUtils.findPlaylist(currentPlaylists, video.url);
|
|
|
|
|
+
|
|
|
|
|
+ if (!alreadyExists) {
|
|
|
|
|
+ currentPlaylists[playlistName].push(video);
|
|
|
|
|
+ addedCount++;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ skippedCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await browser.storage.local.set({ playlists: currentPlaylists });
|
|
|
|
|
+ this.playlists = currentPlaylists;
|
|
|
this.updatePlaylistsForDisplay();
|
|
this.updatePlaylistsForDisplay();
|
|
|
this.updateCurrentPlaylistVideos();
|
|
this.updateCurrentPlaylistVideos();
|
|
|
- alert("Playlists imported successfully!");
|
|
|
|
|
|
|
+
|
|
|
|
|
+ alert(`Import complete! Added ${addedCount} video(s), skipped ${skippedCount} duplicate(s).`);
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- console.error("Error updating playlists:", error);
|
|
|
|
|
|
|
+ console.error("Error appending playlists:", error);
|
|
|
alert("Error importing playlists: " + error.message);
|
|
alert("Error importing playlists: " + error.message);
|
|
|
}
|
|
}
|
|
|
},
|
|
},
|
|
@@ -919,6 +1177,12 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ wikiInspButton: {
|
|
|
|
|
+ ["@click"]() {
|
|
|
|
|
+ this.showWikiInsp();
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
backButton: {
|
|
backButton: {
|
|
|
["@click"]() {
|
|
["@click"]() {
|
|
|
this.showPlaylists();
|
|
this.showPlaylists();
|
|
@@ -986,6 +1250,18 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ wikiInspHeader: {
|
|
|
|
|
+ ["x-show"]() {
|
|
|
|
|
+ return this.currentView === "wikiInsp";
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ importHeader: {
|
|
|
|
|
+ ["x-show"]() {
|
|
|
|
|
+ return this.currentView === "import";
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
playlistsContainer: {
|
|
playlistsContainer: {
|
|
|
["x-show"]() {
|
|
["x-show"]() {
|
|
|
return this.currentView === "playlists";
|
|
return this.currentView === "playlists";
|
|
@@ -1010,6 +1286,18 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ wikiInspContainer: {
|
|
|
|
|
+ ["x-show"]() {
|
|
|
|
|
+ return this.currentView === "wikiInsp";
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ importContainer: {
|
|
|
|
|
+ ["x-show"]() {
|
|
|
|
|
+ return this.currentView === "import";
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
exportContainer: {
|
|
exportContainer: {
|
|
|
["x-show"]() {
|
|
["x-show"]() {
|
|
|
return this.currentView === "playlists";
|
|
return this.currentView === "playlists";
|
|
@@ -1056,6 +1344,34 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ // 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: {
|
|
categoryButton: {
|
|
|
["@click"]() {
|
|
["@click"]() {
|
|
|
const category = this.$el.dataset.category;
|
|
const category = this.$el.dataset.category;
|
|
@@ -1075,6 +1391,12 @@ document.addEventListener("alpine:init", () => {
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
|
|
|
|
|
+ wikiInspNotice: {
|
|
|
|
|
+ ["x-show"]() {
|
|
|
|
|
+ return !this.isCurrentVideoYoutube;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
intervalInput: {
|
|
intervalInput: {
|
|
|
[":value"]() {
|
|
[":value"]() {
|
|
|
return this.checkInterval;
|
|
return this.checkInterval;
|
|
@@ -1105,5 +1427,21 @@ document.addEventListener("alpine:init", () => {
|
|
|
};
|
|
};
|
|
|
},
|
|
},
|
|
|
},
|
|
},
|
|
|
|
|
+
|
|
|
|
|
+ // Import view bindings
|
|
|
|
|
+ importTextarea: {
|
|
|
|
|
+ [":value"]() {
|
|
|
|
|
+ return this.importText;
|
|
|
|
|
+ },
|
|
|
|
|
+ ["@input"]() {
|
|
|
|
|
+ this.importText = this.$el.value;
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
|
|
+
|
|
|
|
|
+ importSubmitButton: {
|
|
|
|
|
+ ["@click"]() {
|
|
|
|
|
+ this.handleImport();
|
|
|
|
|
+ },
|
|
|
|
|
+ },
|
|
|
}));
|
|
}));
|
|
|
});
|
|
});
|