popup.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932
  1. document.addEventListener("alpine:init", () => {
  2. //TODO
  3. // - preserve playlist order (sort)
  4. // - "play/resume playlist" button (pick up where you left off)
  5. // X track timestamp to truly resume where you left off
  6. // X periodically remove watched videos
  7. // X or move to an "old" category, and add a screen to see the list
  8. // - consider separating context menu items (rather than having a sub-menu)
  9. // X view playback history (?)
  10. // - include currently open tabs in export
  11. // - support playlist management (add, remove, rename playlists)
  12. // - support input text box for adding to playlist (how to handle title?)
  13. // - or raw json editing
  14. // X option (probably a button) to add current page (url, title) to playlist
  15. // X align with addLinkToPlaylist in background.js (no repeated videos)
  16. // - button to add channel to youtube page (copy gql mutation to clipboard) (like the automa version)
  17. // - long-term: replace youtube page? rss feeds? need server?
  18. // X add personal rating feature ("enjoyed", "this was important", etc)
  19. Alpine.data("playlistManager", () => ({
  20. playlists: {},
  21. currentIndices: {},
  22. history: {},
  23. sortedHistory: [],
  24. openMenus: new Set(), // Track which menus are open
  25. currentView: "playlists", // Track current view: 'playlists', 'history', or 'playlist'
  26. currentPlaylistName: "", // Track which playlist is being viewed
  27. playlistsForDisplay: [], // Computed array for display
  28. currentPlaylistVideos: [], // Videos for current playlist view
  29. currentTab: null, // Current active tab info
  30. isCurrentTabYoutube: false, // Whether current tab is YouTube
  31. addCurrentPageButtonText: "Add Current Page", // Button text
  32. init() {
  33. this.loadPlaylists();
  34. this.loadHistory();
  35. this.getCurrentTab();
  36. // Add document click handler to close menus
  37. document.addEventListener("click", (e) => {
  38. // If click is not on a more button or menu, close all menus
  39. if (!e.target.closest(".more-menu-container")) {
  40. this.closeAllMenus();
  41. }
  42. });
  43. },
  44. async loadPlaylists() {
  45. try {
  46. const result = await browser.storage.local.get("playlists");
  47. console.log("LOAD RESULT", result.playlists);
  48. this.playlists = result.playlists || {};
  49. if (result.playlists) {
  50. this.currentIndices = Object.keys(result.playlists).reduce(
  51. (acc, pln) => {
  52. const ind = result.playlists[pln].findIndex(
  53. (v) => v.status !== "done",
  54. );
  55. if (ind === -1) {
  56. acc[pln] = result.playlists[pln].length;
  57. } else {
  58. acc[pln] = ind;
  59. }
  60. return acc;
  61. },
  62. {},
  63. );
  64. } else {
  65. this.currentIndices = {};
  66. }
  67. } catch (error) {
  68. console.error("Error loading playlists:", error);
  69. }
  70. this.updatePlaylistsForDisplay();
  71. },
  72. updatePlaylistsForDisplay() {
  73. this.playlistsForDisplay = Object.entries(this.playlists).map(
  74. ([playlistName, videos]) => {
  75. const currentIndex = this.currentIndices[playlistName] || 0;
  76. const shouldShowTruncation = currentIndex > 0;
  77. const truncationText = shouldShowTruncation
  78. ? `(${currentIndex} previous video${currentIndex === 1 ? "" : "s"})`
  79. : "";
  80. // Get visible videos from current index onwards with original indices
  81. const visibleVideos = videos
  82. .slice(currentIndex)
  83. .map((video, index) => ({
  84. ...video,
  85. originalIndex: currentIndex + index,
  86. doneButtonText: video.status === "done" ? "Remove Done Status" : "Mark as Done",
  87. isNonContiguousDone: this.isNonContiguousDone(playlistName, currentIndex + index),
  88. }));
  89. return {
  90. name: playlistName,
  91. videos: videos,
  92. visibleVideos: visibleVideos,
  93. shouldShowTruncation: shouldShowTruncation,
  94. truncationText: truncationText,
  95. };
  96. },
  97. );
  98. },
  99. updateCurrentPlaylistVideos() {
  100. if (
  101. !this.currentPlaylistName ||
  102. !this.playlists[this.currentPlaylistName]
  103. ) {
  104. this.currentPlaylistVideos = [];
  105. } else {
  106. this.currentPlaylistVideos = this.playlists[this.currentPlaylistName].map((video, index) => ({
  107. ...video,
  108. doneButtonText: video.status === "done" ? "Remove Done Status" : "Mark as Done",
  109. isNonContiguousDone: this.isNonContiguousDone(this.currentPlaylistName, index),
  110. }));
  111. }
  112. },
  113. async loadHistory() {
  114. try {
  115. const result = await browser.storage.local.get("history");
  116. console.log("LOAD HISTORY RESULT", result.history);
  117. this.history = result.history || {};
  118. this.sortHistoryByRecentInteraction();
  119. } catch (error) {
  120. console.error("Error loading history:", error);
  121. }
  122. },
  123. async getCurrentTab() {
  124. try {
  125. const tabs = await browser.tabs.query({
  126. active: true,
  127. currentWindow: true,
  128. });
  129. if (tabs.length > 0) {
  130. this.currentTab = tabs[0];
  131. const url = new URL(this.currentTab.url);
  132. this.isCurrentTabYoutube = url.hostname === "www.youtube.com";
  133. this.updateAddCurrentPageButtonText();
  134. }
  135. } catch (error) {
  136. console.error("Error getting current tab:", error);
  137. this.currentTab = null;
  138. this.isCurrentTabYoutube = false;
  139. this.updateAddCurrentPageButtonText();
  140. }
  141. },
  142. updateAddCurrentPageButtonText() {
  143. if (!this.currentTab) {
  144. this.addCurrentPageButtonText = "Unable to get current page";
  145. } else if (!this.isCurrentTabYoutube) {
  146. this.addCurrentPageButtonText = "Add Current Page (YouTube only)";
  147. } else {
  148. this.addCurrentPageButtonText = "Add Current Page to Playlist";
  149. }
  150. },
  151. sortHistoryByRecentInteraction() {
  152. // Convert history object to array with video ID and sort by most recent interaction
  153. this.sortedHistory = Object.entries(this.history)
  154. .map(([videoId, videoData]) => {
  155. const lastInteraction =
  156. videoData.history.length > 0
  157. ? Math.max(...videoData.history.map((event) => event.timestamp))
  158. : 0;
  159. // Pre-process events with formatted data for CSP compliance
  160. const processedEvents = videoData.history
  161. .slice()
  162. .reverse()
  163. .map((event, index) => ({
  164. ...event,
  165. formattedAction: this.formatActionName(event.action),
  166. formattedPosition: `at ${this.formatVideoPosition(event.position)}`,
  167. formattedTimestamp: this.formatTimestamp(event.timestamp),
  168. uniqueKey: `${videoId}-${event.timestamp}-${index}`,
  169. }));
  170. return {
  171. videoId,
  172. formattedVideoId: `(${videoId})`,
  173. ...videoData,
  174. lastInteraction,
  175. processedEvents,
  176. tags: videoData.tags || [], // Ensure tags array exists
  177. };
  178. })
  179. .sort((a, b) => b.lastInteraction - a.lastInteraction);
  180. },
  181. formatTimestamp(timestamp) {
  182. const date = new Date(timestamp);
  183. const now = new Date();
  184. const diffMs = now - date;
  185. const diffMins = Math.floor(diffMs / (1000 * 60));
  186. const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  187. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  188. if (diffMins < 1) return "Just now";
  189. if (diffMins < 60)
  190. return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
  191. if (diffHours < 24)
  192. return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
  193. if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
  194. return (
  195. date.toLocaleDateString() +
  196. " " +
  197. date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
  198. );
  199. },
  200. formatVideoPosition(seconds) {
  201. const minutes = Math.floor(seconds / 60);
  202. const remainingSeconds = Math.floor(seconds % 60);
  203. return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
  204. },
  205. async toggleTag(videoId, tag) {
  206. // Create a deep copy of history to avoid proxy issues
  207. const history = JSON.parse(JSON.stringify(this.history));
  208. if (!history[videoId]) {
  209. console.error(`Video ${videoId} not found in history`);
  210. return;
  211. }
  212. const videoData = history[videoId];
  213. if (!videoData.tags) {
  214. videoData.tags = [];
  215. }
  216. const tagIndex = videoData.tags.indexOf(tag);
  217. if (tagIndex === -1) {
  218. // Add tag
  219. videoData.tags.push(tag);
  220. } else {
  221. // Remove tag
  222. videoData.tags.splice(tagIndex, 1);
  223. }
  224. // Save to storage and update local state
  225. try {
  226. await browser.storage.local.set({ history: history });
  227. this.history = history;
  228. this.sortHistoryByRecentInteraction(); // Refresh display
  229. } catch (error) {
  230. console.error("Error saving tag changes:", error);
  231. }
  232. },
  233. isTagActive(videoId, tag) {
  234. const videoData = this.history[videoId];
  235. return videoData && videoData.tags && videoData.tags.includes(tag);
  236. },
  237. formatActionName(action) {
  238. const actionMap = {
  239. play: "Started",
  240. playing: "Playing",
  241. pause: "Paused",
  242. ended: "Finished",
  243. };
  244. return actionMap[action] || action;
  245. },
  246. formatPlaylistName(name) {
  247. // Convert "listening-1" to "Listening - 1"
  248. return name
  249. .split("-")
  250. .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
  251. .join(" - ");
  252. },
  253. openVideo(url) {
  254. browser.tabs.create({ url });
  255. },
  256. async removeVideo(playlistName, index) {
  257. const playlists = JSON.parse(JSON.stringify(this.playlists));
  258. // Make a copy of the current playlist
  259. const playlist = [...playlists[playlistName]];
  260. // Remove the video at the specified index
  261. playlist.splice(index, 1);
  262. // Create an updated playlists object with the remaining playlists unchanged
  263. const updatedPlaylists = {
  264. ...playlists,
  265. [playlistName]: playlist,
  266. };
  267. // Update the playlists in storage
  268. try {
  269. await browser.storage.local.set({ playlists: updatedPlaylists });
  270. this.playlists = updatedPlaylists;
  271. this.updatePlaylistsForDisplay();
  272. this.updateCurrentPlaylistVideos();
  273. } catch (error) {
  274. console.error("Error removing video:", error);
  275. }
  276. },
  277. async moveVideoUp(playlistName, index) {
  278. // Can't move the first item up
  279. if (index <= 0) return;
  280. const playlists = JSON.parse(JSON.stringify(this.playlists));
  281. // Make a copy of the current playlist
  282. const playlist = [...playlists[playlistName]];
  283. // Swap the video with the one above it
  284. [playlist[index], playlist[index - 1]] = [
  285. playlist[index - 1],
  286. playlist[index],
  287. ];
  288. // Create an updated playlists object
  289. const updatedPlaylists = {
  290. ...playlists,
  291. [playlistName]: playlist,
  292. };
  293. // Update the playlists in storage
  294. try {
  295. await browser.storage.local.set({ playlists: updatedPlaylists });
  296. this.playlists = updatedPlaylists;
  297. this.updatePlaylistsForDisplay();
  298. this.updateCurrentPlaylistVideos();
  299. } catch (error) {
  300. console.error("Error moving video up:", error);
  301. }
  302. },
  303. async moveVideoDown(playlistName, index) {
  304. const playlists = JSON.parse(JSON.stringify(this.playlists));
  305. const playlist = [...playlists[playlistName]];
  306. // Can't move the last item down
  307. if (index >= playlist.length - 1) return;
  308. // Swap the video with the one below it
  309. [playlist[index], playlist[index + 1]] = [
  310. playlist[index + 1],
  311. playlist[index],
  312. ];
  313. // Create an updated playlists object
  314. const updatedPlaylists = {
  315. ...playlists,
  316. [playlistName]: playlist,
  317. };
  318. // Update the playlists in storage
  319. try {
  320. await browser.storage.local.set({ playlists: updatedPlaylists });
  321. this.playlists = updatedPlaylists;
  322. this.updatePlaylistsForDisplay();
  323. this.updateCurrentPlaylistVideos();
  324. } catch (error) {
  325. console.error("Error moving video down:", error);
  326. }
  327. },
  328. async toggleVideoDoneStatus(playlistName, index) {
  329. const playlists = JSON.parse(JSON.stringify(this.playlists));
  330. const playlist = [...playlists[playlistName]];
  331. const video = {...playlist[index]};
  332. // Toggle the done status
  333. if (video.status === "done") {
  334. // Remove status property (undefined status means not done)
  335. delete video.status;
  336. } else {
  337. // Set status to done
  338. video.status = "done";
  339. }
  340. // Update the video in the playlist
  341. playlist[index] = video;
  342. // Create an updated playlists object
  343. const updatedPlaylists = {
  344. ...playlists,
  345. [playlistName]: playlist,
  346. };
  347. // Update the playlists in storage
  348. try {
  349. await browser.storage.local.set({ playlists: updatedPlaylists });
  350. this.playlists = updatedPlaylists;
  351. this.updatePlaylistsForDisplay();
  352. this.updateCurrentPlaylistVideos();
  353. } catch (error) {
  354. console.error("Error toggling video done status:", error);
  355. }
  356. },
  357. async addCurrentPageToPlaylist() {
  358. if (
  359. !this.currentTab ||
  360. !this.isCurrentTabYoutube ||
  361. !this.currentPlaylistName
  362. ) {
  363. return;
  364. }
  365. // Create video object using current tab info
  366. const video = {
  367. url: this.currentTab.url,
  368. title: this.currentTab.title,
  369. };
  370. // Use shared utility function to add video (handles duplicate checking)
  371. const wasAdded = await PlaylistUtils.addVideoToPlaylist(
  372. this.currentPlaylistName,
  373. video,
  374. );
  375. if (wasAdded) {
  376. // Refresh displays only if video was actually added
  377. this.loadPlaylists(); // This will update both display arrays
  378. } else {
  379. // Could show user feedback that video already exists
  380. console.log("Video already exists in a playlist");
  381. }
  382. },
  383. async exportPlaylists() {
  384. try {
  385. // Get current playlists
  386. const playlistResult = await browser.storage.local.get("playlists");
  387. const playlists = playlistResult.playlists || {};
  388. // Get playback history
  389. const historyResult = await browser.storage.local.get("history");
  390. const playbackHistory = historyResult.history || {};
  391. // Create export data object
  392. const exportData = {
  393. playlists,
  394. playbackHistory,
  395. exportDate: new Date().toISOString(),
  396. };
  397. // Convert to JSON
  398. const jsonString = JSON.stringify(exportData, null, 2);
  399. // Create download
  400. const blob = new Blob([jsonString], { type: "application/json" });
  401. const url = URL.createObjectURL(blob);
  402. // Trigger download
  403. const a = document.createElement("a");
  404. a.href = url;
  405. a.download = `playlists-export-${new Date().toISOString().split("T")[0]}.json`;
  406. document.body.appendChild(a);
  407. a.click();
  408. // Clean up
  409. setTimeout(() => {
  410. document.body.removeChild(a);
  411. URL.revokeObjectURL(url);
  412. }, 100);
  413. } catch (error) {
  414. console.error("Error exporting playlists:", error);
  415. }
  416. },
  417. videotitle: {
  418. ["@click"]() {
  419. console.log("TITLE CLICK", this.$el);
  420. },
  421. },
  422. videoPlayLink: {
  423. ["@click.prevent"]() {
  424. browser.tabs.update({ url: this.$el.href });
  425. },
  426. },
  427. isNonContiguousDone(playlistName, videoIndex) {
  428. const playlist = this.playlists[playlistName];
  429. if (!playlist || !playlist[videoIndex] || playlist[videoIndex].status !== "done") {
  430. return false;
  431. }
  432. // Check if there's any non-done video before this done video
  433. for (let i = 0; i < videoIndex; i++) {
  434. if (playlist[i].status !== "done") {
  435. return true;
  436. }
  437. }
  438. return false;
  439. },
  440. isCurrentVideo(playlistName, index) {
  441. const currentIndex = this.currentIndices[playlistName];
  442. return currentIndex === index;
  443. },
  444. isDoneVideo(playlistName, index) {
  445. const currentIndex = this.currentIndices[playlistName];
  446. return index < currentIndex;
  447. },
  448. isVideoDone(playlistName, index) {
  449. const video = this.playlists[playlistName] && this.playlists[playlistName][index];
  450. return video && video.status === "done";
  451. },
  452. videoItemClass: {
  453. [":class"]() {
  454. const playlistName = this.$el.dataset.playlistName;
  455. const index = parseInt(this.$el.dataset.playlistIndex);
  456. const video = this.playlists[playlistName][index];
  457. return {
  458. "current-video": this.isCurrentVideo(playlistName, index),
  459. "done-video":
  460. this.isDoneVideo(playlistName, index) && video.status === "done",
  461. "non-contiguous-done-video": this.isNonContiguousDone(playlistName, index),
  462. };
  463. },
  464. },
  465. removeVideoButton: {
  466. ["@click"]() {
  467. this.removeVideo(
  468. this.$el.dataset.playlistName,
  469. this.$el.dataset.playlistIndex,
  470. );
  471. this.closeAllMenus();
  472. },
  473. },
  474. toggleVideoDoneButton: {
  475. ["@click"]() {
  476. this.toggleVideoDoneStatus(
  477. this.$el.dataset.playlistName,
  478. parseInt(this.$el.dataset.playlistIndex),
  479. );
  480. this.closeAllMenus();
  481. },
  482. },
  483. moveUpButton: {
  484. ["@click"]() {
  485. this.moveVideoUp(
  486. this.$el.dataset.playlistName,
  487. parseInt(this.$el.dataset.playlistIndex),
  488. );
  489. },
  490. [":disabled"]() {
  491. return parseInt(this.$el.dataset.playlistIndex) === 0;
  492. },
  493. },
  494. moveDownButton: {
  495. ["@click"]() {
  496. this.moveVideoDown(
  497. this.$el.dataset.playlistName,
  498. parseInt(this.$el.dataset.playlistIndex),
  499. );
  500. },
  501. [":disabled"]() {
  502. return (
  503. parseInt(this.$el.dataset.playlistIndex) ===
  504. this.playlists[this.$el.dataset.playlistName].length - 1
  505. );
  506. },
  507. },
  508. exportButton: {
  509. ["@click"]() {
  510. this.exportPlaylists();
  511. },
  512. },
  513. importButton: {
  514. ["@click"]() {
  515. document.getElementById("import-file-input").click();
  516. },
  517. },
  518. importFileInput: {
  519. ["@change"]() {
  520. this.importFile(this.$event);
  521. },
  522. },
  523. importFile(event) {
  524. const file = event.target.files[0];
  525. if (!file) return;
  526. const reader = new FileReader();
  527. reader.onload = (e) => {
  528. try {
  529. const importedData = JSON.parse(e.target.result);
  530. this.validateAndImportPlaylists(importedData);
  531. } catch (error) {
  532. console.error("Error parsing JSON file:", error);
  533. alert(
  534. "Invalid JSON file. Please select a valid playlist export file.",
  535. );
  536. } finally {
  537. // Reset the file input so the same file can be selected again
  538. event.target.value = "";
  539. }
  540. };
  541. reader.readAsText(file);
  542. },
  543. validateAndImportPlaylists(data) {
  544. // Validate the playlists structure
  545. if (!data.playlists || typeof data.playlists !== "object") {
  546. alert("Invalid file format: Missing or invalid 'playlists' property");
  547. return;
  548. }
  549. // Validate each playlist
  550. const validPlaylists = {};
  551. let hasErrors = false;
  552. for (const [playlistName, videos] of Object.entries(data.playlists)) {
  553. // Check if videos is an array
  554. if (!Array.isArray(videos)) {
  555. console.error(
  556. `Playlist '${playlistName}' does not contain a valid array of videos`,
  557. );
  558. hasErrors = true;
  559. continue;
  560. }
  561. // Validate each video in the playlist
  562. const validVideos = videos.filter((video) => {
  563. if (!video || typeof video !== "object") {
  564. console.error(`Invalid video object in '${playlistName}'`);
  565. return false;
  566. }
  567. if (
  568. !video.url ||
  569. typeof video.url !== "string" ||
  570. !video.title ||
  571. typeof video.title !== "string"
  572. ) {
  573. console.error(
  574. `Video in '${playlistName}' missing required properties (url, title)`,
  575. );
  576. return false;
  577. }
  578. return true;
  579. });
  580. // Add the validated playlist if it has valid videos
  581. if (validVideos.length > 0) {
  582. validPlaylists[playlistName] = validVideos;
  583. }
  584. }
  585. if (Object.keys(validPlaylists).length === 0) {
  586. alert("No valid playlists found in the import file");
  587. return;
  588. }
  589. if (hasErrors) {
  590. const confirmImport = confirm(
  591. "Some playlists or videos were invalid and will be skipped. Do you want to continue with the import?",
  592. );
  593. if (!confirmImport) return;
  594. }
  595. // Update storage and state
  596. this.updatePlaylists(validPlaylists);
  597. },
  598. async updatePlaylists(playlists) {
  599. try {
  600. await browser.storage.local.set({ playlists });
  601. this.playlists = playlists;
  602. this.updatePlaylistsForDisplay();
  603. this.updateCurrentPlaylistVideos();
  604. alert("Playlists imported successfully!");
  605. } catch (error) {
  606. console.error("Error updating playlists:", error);
  607. alert("Error importing playlists: " + error.message);
  608. }
  609. },
  610. getMenuId(playlistName, index) {
  611. return `${playlistName}-${index}`;
  612. },
  613. isMenuOpen(playlistName, index) {
  614. return this.openMenus.has(this.getMenuId(playlistName, index));
  615. },
  616. toggleMenu(playlistName, index) {
  617. const menuId = this.getMenuId(playlistName, index);
  618. if (this.openMenus.has(menuId)) {
  619. this.openMenus.delete(menuId);
  620. } else {
  621. // Close all other menus first
  622. this.openMenus.clear();
  623. this.openMenus.add(menuId);
  624. }
  625. },
  626. closeAllMenus() {
  627. this.openMenus.clear();
  628. },
  629. moreMenuButton: {
  630. ["@click.stop"]() {
  631. this.toggleMenu(
  632. this.$el.dataset.playlistName,
  633. parseInt(this.$el.dataset.playlistIndex),
  634. );
  635. },
  636. },
  637. moreMenu: {
  638. ["x-show"]() {
  639. return this.isMenuOpen(
  640. this.$el.dataset.playlistName,
  641. parseInt(this.$el.dataset.playlistIndex),
  642. );
  643. },
  644. },
  645. // View navigation methods
  646. showHistory() {
  647. this.currentView = "history";
  648. this.loadHistory(); // Refresh history data when switching to history view
  649. },
  650. showPlaylists() {
  651. this.currentView = "playlists";
  652. },
  653. showPlaylist(playlistName) {
  654. this.currentView = "playlist";
  655. this.currentPlaylistName = playlistName;
  656. this.updateCurrentPlaylistVideos();
  657. },
  658. // Methods for truncated display
  659. shouldShowTruncation(playlistName) {
  660. const currentIndex = this.currentIndices[playlistName] || 0;
  661. return currentIndex > 0;
  662. },
  663. getTruncationText(playlistName) {
  664. const currentIndex = this.currentIndices[playlistName] || 0;
  665. const count = currentIndex;
  666. return `(${count} previous video${count === 1 ? "" : "s"})`;
  667. },
  668. getVisibleVideos(playlistName, videos) {
  669. const currentIndex = this.currentIndices[playlistName] || 0;
  670. // Show from current video onwards, but keep original indices
  671. return videos.slice(currentIndex).map((video, index) => ({
  672. ...video,
  673. originalIndex: currentIndex + index,
  674. }));
  675. },
  676. getCurrentPlaylistVideos() {
  677. if (
  678. !this.currentPlaylistName ||
  679. !this.playlists[this.currentPlaylistName]
  680. ) {
  681. return [];
  682. }
  683. return this.playlists[this.currentPlaylistName];
  684. },
  685. // Button bindings for navigation
  686. historyButton: {
  687. ["@click"]() {
  688. this.showHistory();
  689. },
  690. },
  691. backButton: {
  692. ["@click"]() {
  693. this.showPlaylists();
  694. },
  695. },
  696. // Event handlers for playlist navigation
  697. playlistNameClick: {
  698. ["@click"]() {
  699. const playlistName = this.$el.dataset.playlistName;
  700. this.showPlaylist(playlistName);
  701. },
  702. },
  703. truncatedVideosClick: {
  704. ["@click"]() {
  705. const playlistName = this.$el.dataset.playlistName;
  706. this.showPlaylist(playlistName);
  707. },
  708. },
  709. truncatedVideosClick: {
  710. ["@click"]() {
  711. const playlistName = this.$el.dataset.playlistName;
  712. this.showPlaylist(playlistName);
  713. },
  714. },
  715. truncatedVideosDisplay: {
  716. ["x-show"]() {
  717. const playlistName = this.$el.dataset.playlistName;
  718. const playlistData = this.playlistsForDisplay.find(
  719. (p) => p.name === playlistName,
  720. );
  721. return playlistData ? playlistData.shouldShowTruncation : false;
  722. },
  723. ["@click"]() {
  724. const playlistName = this.$el.dataset.playlistName;
  725. this.showPlaylist(playlistName);
  726. },
  727. },
  728. // CSP-compliant view bindings
  729. playlistsHeader: {
  730. ["x-show"]() {
  731. return this.currentView === "playlists";
  732. },
  733. },
  734. historyHeader: {
  735. ["x-show"]() {
  736. return this.currentView === "history";
  737. },
  738. },
  739. playlistViewHeader: {
  740. ["x-show"]() {
  741. return this.currentView === "playlist";
  742. },
  743. },
  744. playlistsContainer: {
  745. ["x-show"]() {
  746. return this.currentView === "playlists";
  747. },
  748. },
  749. historyContainer: {
  750. ["x-show"]() {
  751. return this.currentView === "history";
  752. },
  753. },
  754. playlistViewContainer: {
  755. ["x-show"]() {
  756. return this.currentView === "playlist";
  757. },
  758. },
  759. exportContainer: {
  760. ["x-show"]() {
  761. return this.currentView === "playlists";
  762. },
  763. },
  764. // History-specific bindings for CSP compliance
  765. historyEmptyState: {
  766. ["x-show"]() {
  767. return this.sortedHistory.length === 0;
  768. },
  769. },
  770. // Playlist view-specific bindings for CSP compliance
  771. playlistViewEmptyState: {
  772. ["x-show"]() {
  773. return this.currentPlaylistVideos.length === 0;
  774. },
  775. },
  776. // Tag chip bindings for CSP compliance
  777. tagChip: {
  778. ["@click"]() {
  779. const videoId = this.$el.dataset.videoId;
  780. const tag = this.$el.dataset.tag;
  781. this.toggleTag(videoId, tag);
  782. },
  783. [":class"]() {
  784. const videoId = this.$el.dataset.videoId;
  785. const tag = this.$el.dataset.tag;
  786. return {
  787. active: this.isTagActive(videoId, tag),
  788. };
  789. },
  790. },
  791. // Add current page button binding
  792. addCurrentPageButton: {
  793. ["@click"]() {
  794. this.addCurrentPageToPlaylist();
  795. },
  796. [":disabled"]() {
  797. return (
  798. !this.currentTab ||
  799. !this.isCurrentTabYoutube ||
  800. !this.currentPlaylistName
  801. );
  802. },
  803. [":class"]() {
  804. return {
  805. disabled:
  806. !this.currentTab ||
  807. !this.isCurrentTabYoutube ||
  808. !this.currentPlaylistName,
  809. };
  810. },
  811. },
  812. }));
  813. });