popup.js 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  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. // X include currently open tabs in export
  11. // - support playlist management (add, remove, rename playlists)
  12. // - support dynamic (on-demand) playlists
  13. // - support input text box for adding to playlist (how to handle title?)
  14. // - or raw json editing
  15. // X option (probably a button) to add current page (url, title) to playlist
  16. // X align with addLinkToPlaylist in background.js (no repeated videos)
  17. // X button to add channel to youtube page (copy gql mutation to clipboard) (like the automa version)
  18. // - long-term: replace youtube page? rss feeds? need server?
  19. // X add personal rating feature ("enjoyed", "this was important", etc)
  20. // - option to move video (including status) to another playlist
  21. // X button to copy current video to clipboard in wikitext format (for personal wiki list)
  22. Alpine.data("playlistManager", () => ({
  23. playlists: {},
  24. currentIndices: {},
  25. history: {},
  26. sortedHistory: [],
  27. openMenus: new Set(), // Track which menus are open
  28. moveSubmenuOpenFor: null, // Track which video has the move-to submenu open (menuId or null)
  29. currentView: "playlists", // Track current view: 'playlists', 'history', 'playlist', 'saveChannel', or 'wikiInsp'
  30. currentPlaylistName: "", // Track which playlist is being viewed
  31. playlistsForDisplay: [], // Computed array for display
  32. currentPlaylistVideos: [], // Videos for current playlist view
  33. otherPlaylists: [], // Playlist names (with display labels) other than currentPlaylistName
  34. currentTab: null, // Current active tab info
  35. isCurrentTabYoutube: false, // Whether current tab is YouTube
  36. isCurrentTabChannelPage: false, // Whether current tab is YouTube videos page
  37. addCurrentPageButtonText: "Add Current Page", // Button text
  38. // Save channel properties
  39. selectedCategory: "FOR BOTH", // Currently selected category
  40. availableCategories: [
  41. "FOR BOTH",
  42. "CHANNELS",
  43. "SCIFI",
  44. "POLI/REL",
  45. "REAL ESTATE",
  46. "HELICOPTER",
  47. "SAILING",
  48. "VAN LIFE",
  49. "BLOCKCHAIN",
  50. "MAKING",
  51. "DESIGN",
  52. "TECH CONF",
  53. "CREATIVE",
  54. ], // Available categories
  55. curlCommand: "", // Generated curl command
  56. checkInterval: 14, // Check interval in days
  57. // Wiki/insp properties
  58. wikitextCommand: "", // Generated wikitext
  59. isCurrentVideoYoutube: false, // Whether current video is YouTube
  60. hasVideoEnded: false, // Whether video has ended
  61. currentVideoTimestamp: "", // Current video timestamp
  62. includeTimestamp: true, // Whether to include timestamp line
  63. // Import properties
  64. importText: "", // Text area content for import
  65. init() {
  66. this.loadPlaylists();
  67. this.loadHistory();
  68. this.getCurrentTab();
  69. // Add document click handler to close menus
  70. document.addEventListener("click", (e) => {
  71. // If click is not on a more button or menu, close all menus
  72. if (!e.target.closest(".more-menu-container")) {
  73. this.closeAllMenus();
  74. }
  75. });
  76. },
  77. async loadPlaylists() {
  78. try {
  79. const result = await browser.storage.local.get("playlists");
  80. console.log("LOAD RESULT", result.playlists);
  81. this.playlists = result.playlists || {};
  82. if (result.playlists) {
  83. this.currentIndices = Object.keys(result.playlists).reduce(
  84. (acc, pln) => {
  85. const ind = result.playlists[pln].findIndex(
  86. (v) => v.status !== "done",
  87. );
  88. if (ind === -1) {
  89. acc[pln] = result.playlists[pln].length;
  90. } else {
  91. acc[pln] = ind;
  92. }
  93. return acc;
  94. },
  95. {},
  96. );
  97. } else {
  98. this.currentIndices = {};
  99. }
  100. } catch (error) {
  101. console.error("Error loading playlists:", error);
  102. }
  103. this.updatePlaylistsForDisplay();
  104. },
  105. updatePlaylistsForDisplay() {
  106. this.playlistsForDisplay = Object.entries(this.playlists).map(
  107. ([playlistName, videos]) => {
  108. const currentIndex = this.currentIndices[playlistName] || 0;
  109. const shouldShowTruncation = currentIndex > 0;
  110. const truncationText = shouldShowTruncation
  111. ? `(${currentIndex} previous video${currentIndex === 1 ? "" : "s"})`
  112. : "";
  113. // Get visible videos from current index onwards with original indices
  114. const visibleVideos = videos
  115. .slice(currentIndex)
  116. .map((video, index) => ({
  117. ...video,
  118. originalIndex: currentIndex + index,
  119. doneButtonText:
  120. video.status === "done" ? "Remove Done Status" : "Mark as Done",
  121. isNonContiguousDone: this.isNonContiguousDone(
  122. playlistName,
  123. currentIndex + index,
  124. ),
  125. }));
  126. return {
  127. name: playlistName,
  128. videos: videos,
  129. visibleVideos: visibleVideos,
  130. shouldShowTruncation: shouldShowTruncation,
  131. truncationText: truncationText,
  132. otherPlaylists: Object.keys(this.playlists)
  133. .filter((n) => n !== playlistName)
  134. .map((n) => ({ name: n, display: this.formatPlaylistName(n) })),
  135. };
  136. },
  137. );
  138. },
  139. updateCurrentPlaylistVideos() {
  140. if (
  141. !this.currentPlaylistName ||
  142. !this.playlists[this.currentPlaylistName]
  143. ) {
  144. this.currentPlaylistVideos = [];
  145. this.otherPlaylists = [];
  146. } else {
  147. this.currentPlaylistVideos = this.playlists[
  148. this.currentPlaylistName
  149. ].map((video, index) => ({
  150. ...video,
  151. doneButtonText:
  152. video.status === "done" ? "Remove Done Status" : "Mark as Done",
  153. isNonContiguousDone: this.isNonContiguousDone(
  154. this.currentPlaylistName,
  155. index,
  156. ),
  157. }));
  158. this.otherPlaylists = Object.keys(this.playlists)
  159. .filter((name) => name !== this.currentPlaylistName)
  160. .map((name) => ({ name, display: this.formatPlaylistName(name) }));
  161. }
  162. },
  163. async loadHistory() {
  164. try {
  165. const result = await browser.storage.local.get("history");
  166. console.log("LOAD HISTORY RESULT", result.history);
  167. this.history = result.history || {};
  168. this.sortHistoryByRecentInteraction();
  169. } catch (error) {
  170. console.error("Error loading history:", error);
  171. }
  172. },
  173. async getCurrentTab() {
  174. try {
  175. const tabs = await browser.tabs.query({
  176. active: true,
  177. currentWindow: true,
  178. });
  179. if (tabs.length > 0) {
  180. this.currentTab = tabs[0];
  181. const url = new URL(this.currentTab.url);
  182. this.isCurrentTabYoutube = url.hostname === "www.youtube.com";
  183. this.isCurrentTabChannelPage =
  184. this.isCurrentTabYoutube && url.pathname.includes("/videos");
  185. this.isCurrentVideoYoutube =
  186. this.isCurrentTabYoutube && url.pathname.includes("/watch");
  187. // Get video timestamp if on YouTube video page
  188. if (this.isCurrentVideoYoutube) {
  189. await this.getVideoTimestamp();
  190. }
  191. this.updateAddCurrentPageButtonText();
  192. this.updateCurlCommand();
  193. this.updateWikitextCommand();
  194. }
  195. } catch (error) {
  196. console.error("Error getting current tab:", error);
  197. this.currentTab = null;
  198. this.isCurrentTabYoutube = false;
  199. this.isCurrentTabChannelPage = false;
  200. this.isCurrentVideoYoutube = false;
  201. this.updateAddCurrentPageButtonText();
  202. this.updateCurlCommand();
  203. this.updateWikitextCommand();
  204. }
  205. },
  206. async getVideoTimestamp() {
  207. try {
  208. // Execute script in the current tab to get video timestamp
  209. const results = await browser.tabs.executeScript(this.currentTab.id, {
  210. code: `
  211. (function() {
  212. const video = document.querySelector('video');
  213. if (video) {
  214. const currentTime = video.currentTime;
  215. const duration = video.duration;
  216. const ended = video.ended;
  217. const formatTime = (seconds) => {
  218. const hours = Math.floor(seconds / 3600);
  219. const minutes = Math.floor((seconds % 3600) / 60);
  220. const remainingSeconds = Math.floor(seconds % 60);
  221. if (hours > 0) {
  222. return hours + ':' + minutes.toString().padStart(2, '0') + ':' + remainingSeconds.toString().padStart(2, '0');
  223. } else {
  224. return minutes + ':' + remainingSeconds.toString().padStart(2, '0');
  225. }
  226. };
  227. return {
  228. currentTime: currentTime,
  229. formattedTime: formatTime(currentTime),
  230. duration: duration,
  231. ended: ended,
  232. isLongVideo: duration > 3600
  233. };
  234. }
  235. return null;
  236. })();
  237. `
  238. });
  239. if (results && results[0]) {
  240. const videoData = results[0];
  241. this.currentVideoTimestamp = videoData.formattedTime;
  242. this.hasVideoEnded = videoData.ended;
  243. } else {
  244. this.currentVideoTimestamp = "0:00";
  245. this.hasVideoEnded = false;
  246. }
  247. } catch (error) {
  248. console.error("Error getting video timestamp:", error);
  249. this.currentVideoTimestamp = "0:00";
  250. this.hasVideoEnded = false;
  251. }
  252. },
  253. updateAddCurrentPageButtonText() {
  254. if (!this.currentTab) {
  255. this.addCurrentPageButtonText = "Unable to get current page";
  256. } else if (!this.isCurrentTabYoutube) {
  257. this.addCurrentPageButtonText = "Add Current Page (YouTube only)";
  258. } else {
  259. this.addCurrentPageButtonText = "Add Current Page to Playlist";
  260. }
  261. },
  262. updateCurlCommand() {
  263. if (!this.currentTab || !this.isCurrentTabChannelPage) {
  264. this.curlCommand =
  265. "This feature is only available on YouTube channel pages (/videos).";
  266. return;
  267. }
  268. const title = this.currentTab.title.replace(" - YouTube", "");
  269. const url = this.currentTab.url;
  270. const category = this.selectedCategory;
  271. const interval = this.checkInterval;
  272. 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 '.'`;
  273. },
  274. async copyCurlCommand() {
  275. try {
  276. await navigator.clipboard.writeText(this.curlCommand);
  277. console.log("Curl command copied to clipboard");
  278. // Could add visual feedback here
  279. } catch (error) {
  280. console.error("Failed to copy to clipboard:", error);
  281. // Fallback for older browsers
  282. try {
  283. const textArea = document.createElement("textarea");
  284. textArea.value = this.curlCommand;
  285. document.body.appendChild(textArea);
  286. textArea.focus();
  287. textArea.select();
  288. document.execCommand("copy");
  289. document.body.removeChild(textArea);
  290. console.log("Curl command copied to clipboard (fallback)");
  291. } catch (fallbackError) {
  292. console.error("Fallback copy failed:", fallbackError);
  293. }
  294. }
  295. },
  296. selectCategory(category) {
  297. this.selectedCategory = category;
  298. this.updateCurlCommand();
  299. },
  300. updateCheckInterval(interval) {
  301. // Ensure it's a positive integer, default to 14 if invalid
  302. const parsedInterval = parseInt(interval);
  303. this.checkInterval = parsedInterval > 0 ? parsedInterval : 14;
  304. this.updateCurlCommand();
  305. },
  306. showSaveChannel() {
  307. this.currentView = "saveChannel";
  308. this.updateCurlCommand();
  309. },
  310. showWikiInsp() {
  311. this.currentView = "wikiInsp";
  312. // Refresh timestamp when opening the view
  313. if (this.isCurrentVideoYoutube) {
  314. this.getVideoTimestamp().then(() => {
  315. this.updateWikitextCommand();
  316. });
  317. } else {
  318. this.updateWikitextCommand();
  319. }
  320. },
  321. showImport() {
  322. this.currentView = "import";
  323. },
  324. async refreshVideoTimestamp() {
  325. if (this.isCurrentVideoYoutube) {
  326. await this.getVideoTimestamp();
  327. this.updateWikitextCommand();
  328. }
  329. },
  330. updateWikitextCommand() {
  331. if (!this.currentTab || !this.isCurrentVideoYoutube) {
  332. this.wikitextCommand = "This feature is only available on YouTube videos.";
  333. return;
  334. }
  335. const today = new Date();
  336. const dateString = today.toISOString().split('T')[0]; // YYYY-MM-DD format
  337. const title = this.currentTab.title.replace(" - YouTube", "").replace(/\|/g, "-");
  338. const url = this.currentTab.url;
  339. let wikitext = `!!! ${dateString}\n* [[${title}|${url}]] (yt)`;
  340. // Add timestamp if user has enabled it and video hasn't ended
  341. if (this.includeTimestamp && !this.hasVideoEnded) {
  342. wikitext += `\n** ${this.currentVideoTimestamp || "0:00"} - `;
  343. }
  344. this.wikitextCommand = wikitext;
  345. },
  346. async copyWikitextCommand() {
  347. try {
  348. await navigator.clipboard.writeText(this.wikitextCommand);
  349. console.log("Wikitext copied to clipboard");
  350. // Could add visual feedback here
  351. } catch (error) {
  352. console.error("Failed to copy to clipboard:", error);
  353. // Fallback for older browsers
  354. try {
  355. const textArea = document.createElement("textarea");
  356. textArea.value = this.wikitextCommand;
  357. document.body.appendChild(textArea);
  358. textArea.focus();
  359. textArea.select();
  360. document.execCommand("copy");
  361. document.body.removeChild(textArea);
  362. console.log("Wikitext copied to clipboard (fallback)");
  363. } catch (fallbackError) {
  364. console.error("Fallback copy failed:", fallbackError);
  365. }
  366. }
  367. },
  368. toggleTimestamp() {
  369. this.includeTimestamp = !this.includeTimestamp;
  370. this.updateWikitextCommand();
  371. },
  372. sortHistoryByRecentInteraction() {
  373. // Convert history object to array with video ID and sort by most recent interaction
  374. this.sortedHistory = Object.entries(this.history)
  375. .map(([videoId, videoData]) => {
  376. const lastInteraction =
  377. videoData.history.length > 0
  378. ? Math.max(...videoData.history.map((event) => event.timestamp))
  379. : 0;
  380. // Pre-process events with formatted data for CSP compliance
  381. const processedEvents = videoData.history
  382. .slice()
  383. .reverse()
  384. .map((event, index) => ({
  385. ...event,
  386. formattedAction: this.formatActionName(event.action),
  387. formattedPosition: `at ${this.formatVideoPosition(event.position)}`,
  388. formattedTimestamp: this.formatTimestamp(event.timestamp),
  389. uniqueKey: `${videoId}-${event.timestamp}-${index}`,
  390. }));
  391. return {
  392. videoId,
  393. formattedVideoId: `(${videoId})`,
  394. ...videoData,
  395. lastInteraction,
  396. processedEvents,
  397. tags: videoData.tags || [], // Ensure tags array exists
  398. };
  399. })
  400. .sort((a, b) => b.lastInteraction - a.lastInteraction);
  401. },
  402. formatTimestamp(timestamp) {
  403. const date = new Date(timestamp);
  404. const now = new Date();
  405. const diffMs = now - date;
  406. const diffMins = Math.floor(diffMs / (1000 * 60));
  407. const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
  408. const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
  409. if (diffMins < 1) return "Just now";
  410. if (diffMins < 60)
  411. return `${diffMins} minute${diffMins > 1 ? "s" : ""} ago`;
  412. if (diffHours < 24)
  413. return `${diffHours} hour${diffHours > 1 ? "s" : ""} ago`;
  414. if (diffDays < 7) return `${diffDays} day${diffDays > 1 ? "s" : ""} ago`;
  415. return (
  416. date.toLocaleDateString() +
  417. " " +
  418. date.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
  419. );
  420. },
  421. formatVideoPosition(seconds) {
  422. const minutes = Math.floor(seconds / 60);
  423. const remainingSeconds = Math.floor(seconds % 60);
  424. return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`;
  425. },
  426. async toggleTag(videoId, tag) {
  427. // Create a deep copy of history to avoid proxy issues
  428. const history = JSON.parse(JSON.stringify(this.history));
  429. if (!history[videoId]) {
  430. console.error(`Video ${videoId} not found in history`);
  431. return;
  432. }
  433. const videoData = history[videoId];
  434. if (!videoData.tags) {
  435. videoData.tags = [];
  436. }
  437. const tagIndex = videoData.tags.indexOf(tag);
  438. if (tagIndex === -1) {
  439. // Add tag
  440. videoData.tags.push(tag);
  441. } else {
  442. // Remove tag
  443. videoData.tags.splice(tagIndex, 1);
  444. }
  445. // Save to storage and update local state
  446. try {
  447. await browser.storage.local.set({ history: history });
  448. this.history = history;
  449. this.sortHistoryByRecentInteraction(); // Refresh display
  450. } catch (error) {
  451. console.error("Error saving tag changes:", error);
  452. }
  453. },
  454. isTagActive(videoId, tag) {
  455. const videoData = this.history[videoId];
  456. return videoData && videoData.tags && videoData.tags.includes(tag);
  457. },
  458. formatActionName(action) {
  459. const actionMap = {
  460. play: "Started",
  461. playing: "Playing",
  462. pause: "Paused",
  463. ended: "Finished",
  464. };
  465. return actionMap[action] || action;
  466. },
  467. formatPlaylistName(name) {
  468. // Convert "listening-1" to "Listening - 1"
  469. return name
  470. .split("-")
  471. .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
  472. .join(" - ");
  473. },
  474. openVideo(url) {
  475. browser.tabs.create({ url });
  476. },
  477. async removeVideo(playlistName, index) {
  478. const playlists = JSON.parse(JSON.stringify(this.playlists));
  479. // Make a copy of the current playlist
  480. const playlist = [...playlists[playlistName]];
  481. // Remove the video at the specified index
  482. playlist.splice(index, 1);
  483. // Create an updated playlists object with the remaining playlists unchanged
  484. const updatedPlaylists = {
  485. ...playlists,
  486. [playlistName]: playlist,
  487. };
  488. // Update the playlists in storage
  489. try {
  490. await browser.storage.local.set({ playlists: updatedPlaylists });
  491. this.playlists = updatedPlaylists;
  492. this.updatePlaylistsForDisplay();
  493. this.updateCurrentPlaylistVideos();
  494. } catch (error) {
  495. console.error("Error removing video:", error);
  496. }
  497. },
  498. async moveVideoUp(playlistName, index) {
  499. // Can't move the first item up
  500. if (index <= 0) return;
  501. const playlists = JSON.parse(JSON.stringify(this.playlists));
  502. // Make a copy of the current playlist
  503. const playlist = [...playlists[playlistName]];
  504. // Swap the video with the one above it
  505. [playlist[index], playlist[index - 1]] = [
  506. playlist[index - 1],
  507. playlist[index],
  508. ];
  509. // Create an updated playlists object
  510. const updatedPlaylists = {
  511. ...playlists,
  512. [playlistName]: playlist,
  513. };
  514. // Update the playlists in storage
  515. try {
  516. await browser.storage.local.set({ playlists: updatedPlaylists });
  517. this.playlists = updatedPlaylists;
  518. this.updatePlaylistsForDisplay();
  519. this.updateCurrentPlaylistVideos();
  520. } catch (error) {
  521. console.error("Error moving video up:", error);
  522. }
  523. },
  524. async moveVideoDown(playlistName, index) {
  525. const playlists = JSON.parse(JSON.stringify(this.playlists));
  526. const playlist = [...playlists[playlistName]];
  527. // Can't move the last item down
  528. if (index >= playlist.length - 1) return;
  529. // Swap the video with the one below it
  530. [playlist[index], playlist[index + 1]] = [
  531. playlist[index + 1],
  532. playlist[index],
  533. ];
  534. // Create an updated playlists object
  535. const updatedPlaylists = {
  536. ...playlists,
  537. [playlistName]: playlist,
  538. };
  539. // Update the playlists in storage
  540. try {
  541. await browser.storage.local.set({ playlists: updatedPlaylists });
  542. this.playlists = updatedPlaylists;
  543. this.updatePlaylistsForDisplay();
  544. this.updateCurrentPlaylistVideos();
  545. } catch (error) {
  546. console.error("Error moving video down:", error);
  547. }
  548. },
  549. async moveVideoToTop(playlistName, index) {
  550. const playlists = JSON.parse(JSON.stringify(this.playlists));
  551. const playlist = [...playlists[playlistName]];
  552. const topPosition = this.findTopPosition(playlistName);
  553. if (index === topPosition) return;
  554. const video = playlist.splice(index, 1)[0];
  555. playlist.splice(topPosition, 0, video);
  556. const updatedPlaylists = {
  557. ...playlists,
  558. [playlistName]: playlist,
  559. };
  560. try {
  561. await browser.storage.local.set({ playlists: updatedPlaylists });
  562. this.playlists = updatedPlaylists;
  563. this.updatePlaylistsForDisplay();
  564. this.updateCurrentPlaylistVideos();
  565. } catch (error) {
  566. console.error("Error moving video to top:", error);
  567. }
  568. },
  569. async moveVideoToBottom(playlistName, index) {
  570. const playlists = JSON.parse(JSON.stringify(this.playlists));
  571. const playlist = [...playlists[playlistName]];
  572. if (index === playlist.length - 1) return;
  573. const video = playlist.splice(index, 1)[0];
  574. playlist.push(video);
  575. const updatedPlaylists = {
  576. ...playlists,
  577. [playlistName]: playlist,
  578. };
  579. try {
  580. await browser.storage.local.set({ playlists: updatedPlaylists });
  581. this.playlists = updatedPlaylists;
  582. this.updatePlaylistsForDisplay();
  583. this.updateCurrentPlaylistVideos();
  584. } catch (error) {
  585. console.error("Error moving video to bottom:", error);
  586. }
  587. },
  588. async moveVideoToPlaylist(fromPlaylistName, videoIndex, toPlaylistName) {
  589. const playlists = JSON.parse(JSON.stringify(this.playlists));
  590. const video = playlists[fromPlaylistName].splice(videoIndex, 1)[0];
  591. playlists[toPlaylistName].push(video);
  592. try {
  593. await browser.storage.local.set({ playlists });
  594. this.playlists = playlists;
  595. this.updatePlaylistsForDisplay();
  596. this.updateCurrentPlaylistVideos();
  597. } catch (error) {
  598. console.error("Error moving video to playlist:", error);
  599. }
  600. },
  601. findTopPosition(playlistName) {
  602. const playlist = this.playlists[playlistName];
  603. if (!playlist) return 0;
  604. for (let i = 0; i < playlist.length; i++) {
  605. if (playlist[i].status !== "done") {
  606. return i;
  607. }
  608. }
  609. return playlist.length;
  610. },
  611. async toggleVideoDoneStatus(playlistName, index) {
  612. const playlists = JSON.parse(JSON.stringify(this.playlists));
  613. const playlist = [...playlists[playlistName]];
  614. const video = { ...playlist[index] };
  615. // Toggle the done status
  616. if (video.status === "done") {
  617. // Remove status property (undefined status means not done)
  618. delete video.status;
  619. } else {
  620. // Set status to done
  621. video.status = "done";
  622. }
  623. // Update the video in the playlist
  624. playlist[index] = video;
  625. // Create an updated playlists object
  626. const updatedPlaylists = {
  627. ...playlists,
  628. [playlistName]: playlist,
  629. };
  630. // Update the playlists in storage
  631. try {
  632. await browser.storage.local.set({ playlists: updatedPlaylists });
  633. this.playlists = updatedPlaylists;
  634. this.updatePlaylistsForDisplay();
  635. this.updateCurrentPlaylistVideos();
  636. } catch (error) {
  637. console.error("Error toggling video done status:", error);
  638. }
  639. },
  640. async addCurrentPageToPlaylist() {
  641. if (
  642. !this.currentTab ||
  643. !this.isCurrentTabYoutube ||
  644. !this.currentPlaylistName
  645. ) {
  646. return;
  647. }
  648. // Create video object using current tab info
  649. const video = {
  650. url: this.currentTab.url,
  651. title: this.currentTab.title,
  652. };
  653. // Use shared utility function to add video (handles duplicate checking)
  654. const wasAdded = await PlaylistUtils.addVideoToPlaylist(
  655. this.currentPlaylistName,
  656. video,
  657. );
  658. if (wasAdded) {
  659. // Refresh displays only if video was actually added
  660. this.loadPlaylists(); // This will update both display arrays
  661. } else {
  662. // Could show user feedback that video already exists
  663. console.log("Video already exists in a playlist");
  664. }
  665. },
  666. async exportPlaylists() {
  667. try {
  668. // Get current playlists
  669. const playlistResult = await browser.storage.local.get("playlists");
  670. const playlists = playlistResult.playlists || {};
  671. // Get playback history
  672. const historyResult = await browser.storage.local.get("history");
  673. const playbackHistory = historyResult.history || {};
  674. // Get open tabs
  675. let openTabs = [];
  676. try {
  677. const tabsResult = await browser.tabs.query({});
  678. openTabs = tabsResult
  679. .filter(
  680. (tab) =>
  681. tab.url &&
  682. !tab.url.startsWith("moz-extension://") &&
  683. !tab.url.startsWith("about:") &&
  684. !tab.url.startsWith("chrome://"),
  685. )
  686. .map((tab) => ({
  687. url: tab.url,
  688. title: tab.title || "Untitled",
  689. }));
  690. } catch (error) {
  691. console.warn("Could not retrieve open tabs:", error);
  692. openTabs = [];
  693. }
  694. // Create export data object
  695. const exportData = {
  696. playlists,
  697. playbackHistory,
  698. openTabs,
  699. exportDate: new Date().toISOString(),
  700. };
  701. // Convert to JSON
  702. const jsonString = JSON.stringify(exportData, null, 2);
  703. // Create download
  704. const blob = new Blob([jsonString], { type: "application/json" });
  705. const url = URL.createObjectURL(blob);
  706. // Trigger download
  707. const a = document.createElement("a");
  708. a.href = url;
  709. a.download = `playlists-export-${new Date().toISOString().split("T")[0]}.json`;
  710. document.body.appendChild(a);
  711. a.click();
  712. // Clean up
  713. setTimeout(() => {
  714. document.body.removeChild(a);
  715. URL.revokeObjectURL(url);
  716. }, 100);
  717. } catch (error) {
  718. console.error("Error exporting playlists:", error);
  719. }
  720. },
  721. videotitle: {
  722. ["@click"]() {
  723. console.log("TITLE CLICK", this.$el);
  724. },
  725. },
  726. videoPlayLink: {
  727. ["@click.prevent"]() {
  728. browser.tabs.update({ url: this.$el.href });
  729. },
  730. },
  731. isNonContiguousDone(playlistName, videoIndex) {
  732. const playlist = this.playlists[playlistName];
  733. if (
  734. !playlist ||
  735. !playlist[videoIndex] ||
  736. playlist[videoIndex].status !== "done"
  737. ) {
  738. return false;
  739. }
  740. // Check if there's any non-done video before this done video
  741. for (let i = 0; i < videoIndex; i++) {
  742. if (playlist[i].status !== "done") {
  743. return true;
  744. }
  745. }
  746. return false;
  747. },
  748. isCurrentVideo(playlistName, index) {
  749. const currentIndex = this.currentIndices[playlistName];
  750. return currentIndex === index;
  751. },
  752. isDoneVideo(playlistName, index) {
  753. const currentIndex = this.currentIndices[playlistName];
  754. return index < currentIndex;
  755. },
  756. isVideoDone(playlistName, index) {
  757. const video =
  758. this.playlists[playlistName] && this.playlists[playlistName][index];
  759. return video && video.status === "done";
  760. },
  761. videoItemClass: {
  762. [":class"]() {
  763. const playlistName = this.$el.dataset.playlistName;
  764. const index = parseInt(this.$el.dataset.playlistIndex);
  765. const video = this.playlists[playlistName][index];
  766. return {
  767. "current-video": this.isCurrentVideo(playlistName, index),
  768. "done-video":
  769. this.isDoneVideo(playlistName, index) && video.status === "done",
  770. "non-contiguous-done-video": this.isNonContiguousDone(
  771. playlistName,
  772. index,
  773. ),
  774. };
  775. },
  776. },
  777. removeVideoButton: {
  778. ["@click"]() {
  779. this.removeVideo(
  780. this.$el.dataset.playlistName,
  781. this.$el.dataset.playlistIndex,
  782. );
  783. this.closeAllMenus();
  784. },
  785. },
  786. toggleVideoDoneButton: {
  787. ["@click"]() {
  788. this.toggleVideoDoneStatus(
  789. this.$el.dataset.playlistName,
  790. parseInt(this.$el.dataset.playlistIndex),
  791. );
  792. this.closeAllMenus();
  793. },
  794. },
  795. moveUpButton: {
  796. ["@click"]() {
  797. this.moveVideoUp(
  798. this.$el.dataset.playlistName,
  799. parseInt(this.$el.dataset.playlistIndex),
  800. );
  801. },
  802. [":disabled"]() {
  803. return parseInt(this.$el.dataset.playlistIndex) === 0;
  804. },
  805. },
  806. moveDownButton: {
  807. ["@click"]() {
  808. this.moveVideoDown(
  809. this.$el.dataset.playlistName,
  810. parseInt(this.$el.dataset.playlistIndex),
  811. );
  812. },
  813. [":disabled"]() {
  814. return (
  815. parseInt(this.$el.dataset.playlistIndex) ===
  816. this.playlists[this.$el.dataset.playlistName].length - 1
  817. );
  818. },
  819. },
  820. moveToTopButton: {
  821. ["@click"]() {
  822. this.moveVideoToTop(
  823. this.$el.dataset.playlistName,
  824. parseInt(this.$el.dataset.playlistIndex),
  825. );
  826. this.closeAllMenus();
  827. },
  828. [":disabled"]() {
  829. const index = parseInt(this.$el.dataset.playlistIndex);
  830. const playlistName = this.$el.dataset.playlistName;
  831. const topPosition = this.findTopPosition(playlistName);
  832. return index === topPosition;
  833. },
  834. },
  835. moveToBottomButton: {
  836. ["@click"]() {
  837. this.moveVideoToBottom(
  838. this.$el.dataset.playlistName,
  839. parseInt(this.$el.dataset.playlistIndex),
  840. );
  841. this.closeAllMenus();
  842. },
  843. [":disabled"]() {
  844. return (
  845. parseInt(this.$el.dataset.playlistIndex) ===
  846. this.playlists[this.$el.dataset.playlistName].length - 1
  847. );
  848. },
  849. },
  850. moveToSubmenuButton: {
  851. ["@click.stop"]() {
  852. const playlistName = this.$el.dataset.playlistName;
  853. const index = parseInt(this.$el.dataset.playlistIndex);
  854. const menuId = this.getMenuId(playlistName, index);
  855. this.moveSubmenuOpenFor = this.moveSubmenuOpenFor === menuId ? null : menuId;
  856. },
  857. },
  858. moveToSubmenu: {
  859. ["x-show"]() {
  860. const playlistName = this.$el.dataset.playlistName;
  861. const index = parseInt(this.$el.dataset.playlistIndex);
  862. return this.moveSubmenuOpenFor === this.getMenuId(playlistName, index);
  863. },
  864. },
  865. moveToPlaylistButton: {
  866. ["@click"]() {
  867. const fromPlaylistName = this.$el.dataset.fromPlaylistName;
  868. const videoIndex = parseInt(this.$el.dataset.playlistIndex);
  869. const toPlaylistName = this.$el.dataset.toPlaylistName;
  870. this.moveVideoToPlaylist(fromPlaylistName, videoIndex, toPlaylistName);
  871. this.closeAllMenus();
  872. },
  873. },
  874. exportButton: {
  875. ["@click"]() {
  876. this.exportPlaylists();
  877. },
  878. },
  879. importButton: {
  880. ["@click"]() {
  881. this.showImport();
  882. },
  883. },
  884. async handleImport() {
  885. if (!this.importText.trim()) {
  886. alert("Please paste your JSON export data");
  887. return;
  888. }
  889. try {
  890. const importedData = JSON.parse(this.importText);
  891. this.validateAndImportPlaylists(importedData);
  892. this.importText = "";
  893. } catch (error) {
  894. console.error("Error parsing JSON:", error);
  895. alert(
  896. "Invalid JSON format. Please paste valid playlist export data.",
  897. );
  898. }
  899. },
  900. validateAndImportPlaylists(data) {
  901. // Validate the playlists structure
  902. if (!data.playlists || typeof data.playlists !== "object") {
  903. alert("Invalid file format: Missing or invalid 'playlists' property");
  904. return;
  905. }
  906. // Validate each playlist
  907. const validPlaylists = {};
  908. let hasErrors = false;
  909. for (const [playlistName, videos] of Object.entries(data.playlists)) {
  910. // Check if videos is an array
  911. if (!Array.isArray(videos)) {
  912. console.error(
  913. `Playlist '${playlistName}' does not contain a valid array of videos`,
  914. );
  915. hasErrors = true;
  916. continue;
  917. }
  918. // Validate each video in the playlist and filter out done videos
  919. const validVideos = videos.filter((video) => {
  920. if (!video || typeof video !== "object") {
  921. console.error(`Invalid video object in '${playlistName}'`);
  922. return false;
  923. }
  924. if (
  925. !video.url ||
  926. typeof video.url !== "string" ||
  927. !video.title ||
  928. typeof video.title !== "string"
  929. ) {
  930. console.error(
  931. `Video in '${playlistName}' missing required properties (url, title)`,
  932. );
  933. return false;
  934. }
  935. if (video.status === "done") {
  936. return false;
  937. }
  938. return true;
  939. });
  940. // Add the validated playlist if it has valid videos
  941. if (validVideos.length > 0) {
  942. validPlaylists[playlistName] = validVideos;
  943. }
  944. }
  945. if (Object.keys(validPlaylists).length === 0) {
  946. alert("No valid playlists found in the import file");
  947. return;
  948. }
  949. if (hasErrors) {
  950. const confirmImport = confirm(
  951. "Some playlists or videos were invalid and will be skipped. Do you want to continue with the import?",
  952. );
  953. if (!confirmImport) return;
  954. }
  955. // Update storage and state
  956. this.appendToPlaylists(validPlaylists);
  957. },
  958. async appendToPlaylists(importedPlaylists) {
  959. try {
  960. const currentPlaylists = JSON.parse(JSON.stringify(this.playlists));
  961. let addedCount = 0;
  962. let skippedCount = 0;
  963. for (const [playlistName, importedVideos] of Object.entries(importedPlaylists)) {
  964. if (!currentPlaylists[playlistName]) {
  965. currentPlaylists[playlistName] = [];
  966. }
  967. for (const video of importedVideos) {
  968. const alreadyExists = PlaylistUtils.findPlaylist(currentPlaylists, video.url);
  969. if (!alreadyExists) {
  970. currentPlaylists[playlistName].push(video);
  971. addedCount++;
  972. } else {
  973. skippedCount++;
  974. }
  975. }
  976. }
  977. await browser.storage.local.set({ playlists: currentPlaylists });
  978. this.playlists = currentPlaylists;
  979. this.updatePlaylistsForDisplay();
  980. this.updateCurrentPlaylistVideos();
  981. alert(`Import complete! Added ${addedCount} video(s), skipped ${skippedCount} duplicate(s).`);
  982. } catch (error) {
  983. console.error("Error appending playlists:", error);
  984. alert("Error importing playlists: " + error.message);
  985. }
  986. },
  987. getMenuId(playlistName, index) {
  988. return `${playlistName}-${index}`;
  989. },
  990. isMenuOpen(playlistName, index) {
  991. return this.openMenus.has(this.getMenuId(playlistName, index));
  992. },
  993. toggleMenu(playlistName, index) {
  994. const menuId = this.getMenuId(playlistName, index);
  995. if (this.openMenus.has(menuId)) {
  996. this.openMenus.delete(menuId);
  997. } else {
  998. this.openMenus.clear();
  999. this.openMenus.add(menuId);
  1000. }
  1001. this.moveSubmenuOpenFor = null;
  1002. },
  1003. closeAllMenus() {
  1004. this.openMenus.clear();
  1005. this.moveSubmenuOpenFor = null;
  1006. },
  1007. moreMenuButton: {
  1008. ["@click.stop"]() {
  1009. this.toggleMenu(
  1010. this.$el.dataset.playlistName,
  1011. parseInt(this.$el.dataset.playlistIndex),
  1012. );
  1013. },
  1014. },
  1015. moreMenu: {
  1016. ["x-show"]() {
  1017. return this.isMenuOpen(
  1018. this.$el.dataset.playlistName,
  1019. parseInt(this.$el.dataset.playlistIndex),
  1020. );
  1021. },
  1022. },
  1023. // View navigation methods
  1024. showHistory() {
  1025. this.currentView = "history";
  1026. this.loadHistory(); // Refresh history data when switching to history view
  1027. },
  1028. showPlaylists() {
  1029. this.currentView = "playlists";
  1030. },
  1031. showPlaylist(playlistName) {
  1032. this.currentView = "playlist";
  1033. this.currentPlaylistName = playlistName;
  1034. this.updateCurrentPlaylistVideos();
  1035. },
  1036. // Methods for truncated display
  1037. shouldShowTruncation(playlistName) {
  1038. const currentIndex = this.currentIndices[playlistName] || 0;
  1039. return currentIndex > 0;
  1040. },
  1041. getTruncationText(playlistName) {
  1042. const currentIndex = this.currentIndices[playlistName] || 0;
  1043. const count = currentIndex;
  1044. return `(${count} previous video${count === 1 ? "" : "s"})`;
  1045. },
  1046. getVisibleVideos(playlistName, videos) {
  1047. const currentIndex = this.currentIndices[playlistName] || 0;
  1048. // Show from current video onwards, but keep original indices
  1049. return videos.slice(currentIndex).map((video, index) => ({
  1050. ...video,
  1051. originalIndex: currentIndex + index,
  1052. }));
  1053. },
  1054. getCurrentPlaylistVideos() {
  1055. if (
  1056. !this.currentPlaylistName ||
  1057. !this.playlists[this.currentPlaylistName]
  1058. ) {
  1059. return [];
  1060. }
  1061. return this.playlists[this.currentPlaylistName];
  1062. },
  1063. // Button bindings for navigation
  1064. historyButton: {
  1065. ["@click"]() {
  1066. this.showHistory();
  1067. },
  1068. },
  1069. saveChannelButton: {
  1070. ["@click"]() {
  1071. this.showSaveChannel();
  1072. },
  1073. },
  1074. wikiInspButton: {
  1075. ["@click"]() {
  1076. this.showWikiInsp();
  1077. },
  1078. },
  1079. backButton: {
  1080. ["@click"]() {
  1081. this.showPlaylists();
  1082. },
  1083. },
  1084. // Event handlers for playlist navigation
  1085. playlistNameClick: {
  1086. ["@click"]() {
  1087. const playlistName = this.$el.dataset.playlistName;
  1088. this.showPlaylist(playlistName);
  1089. },
  1090. },
  1091. truncatedVideosClick: {
  1092. ["@click"]() {
  1093. const playlistName = this.$el.dataset.playlistName;
  1094. this.showPlaylist(playlistName);
  1095. },
  1096. },
  1097. truncatedVideosClick: {
  1098. ["@click"]() {
  1099. const playlistName = this.$el.dataset.playlistName;
  1100. this.showPlaylist(playlistName);
  1101. },
  1102. },
  1103. truncatedVideosDisplay: {
  1104. ["x-show"]() {
  1105. const playlistName = this.$el.dataset.playlistName;
  1106. const playlistData = this.playlistsForDisplay.find(
  1107. (p) => p.name === playlistName,
  1108. );
  1109. return playlistData ? playlistData.shouldShowTruncation : false;
  1110. },
  1111. ["@click"]() {
  1112. const playlistName = this.$el.dataset.playlistName;
  1113. this.showPlaylist(playlistName);
  1114. },
  1115. },
  1116. // CSP-compliant view bindings
  1117. playlistsHeader: {
  1118. ["x-show"]() {
  1119. return this.currentView === "playlists";
  1120. },
  1121. },
  1122. historyHeader: {
  1123. ["x-show"]() {
  1124. return this.currentView === "history";
  1125. },
  1126. },
  1127. playlistViewHeader: {
  1128. ["x-show"]() {
  1129. return this.currentView === "playlist";
  1130. },
  1131. },
  1132. saveChannelHeader: {
  1133. ["x-show"]() {
  1134. return this.currentView === "saveChannel";
  1135. },
  1136. },
  1137. wikiInspHeader: {
  1138. ["x-show"]() {
  1139. return this.currentView === "wikiInsp";
  1140. },
  1141. },
  1142. importHeader: {
  1143. ["x-show"]() {
  1144. return this.currentView === "import";
  1145. },
  1146. },
  1147. playlistsContainer: {
  1148. ["x-show"]() {
  1149. return this.currentView === "playlists";
  1150. },
  1151. },
  1152. historyContainer: {
  1153. ["x-show"]() {
  1154. return this.currentView === "history";
  1155. },
  1156. },
  1157. playlistViewContainer: {
  1158. ["x-show"]() {
  1159. return this.currentView === "playlist";
  1160. },
  1161. },
  1162. saveChannelContainer: {
  1163. ["x-show"]() {
  1164. return this.currentView === "saveChannel";
  1165. },
  1166. },
  1167. wikiInspContainer: {
  1168. ["x-show"]() {
  1169. return this.currentView === "wikiInsp";
  1170. },
  1171. },
  1172. importContainer: {
  1173. ["x-show"]() {
  1174. return this.currentView === "import";
  1175. },
  1176. },
  1177. exportContainer: {
  1178. ["x-show"]() {
  1179. return this.currentView === "playlists";
  1180. },
  1181. },
  1182. // History-specific bindings for CSP compliance
  1183. historyEmptyState: {
  1184. ["x-show"]() {
  1185. return this.sortedHistory.length === 0;
  1186. },
  1187. },
  1188. // Playlist view-specific bindings for CSP compliance
  1189. playlistViewEmptyState: {
  1190. ["x-show"]() {
  1191. return this.currentPlaylistVideos.length === 0;
  1192. },
  1193. },
  1194. // Tag chip bindings for CSP compliance
  1195. tagChip: {
  1196. ["@click"]() {
  1197. const videoId = this.$el.dataset.videoId;
  1198. const tag = this.$el.dataset.tag;
  1199. this.toggleTag(videoId, tag);
  1200. },
  1201. [":class"]() {
  1202. const videoId = this.$el.dataset.videoId;
  1203. const tag = this.$el.dataset.tag;
  1204. return {
  1205. active: this.isTagActive(videoId, tag),
  1206. };
  1207. },
  1208. },
  1209. // Save Channel specific bindings
  1210. copyCurlButton: {
  1211. ["@click"]() {
  1212. this.copyCurlCommand();
  1213. },
  1214. [":disabled"]() {
  1215. return !this.isCurrentTabChannelPage;
  1216. },
  1217. },
  1218. // Wiki/insp specific bindings
  1219. copyWikitextButton: {
  1220. ["@click"]() {
  1221. this.copyWikitextCommand();
  1222. },
  1223. [":disabled"]() {
  1224. return !this.isCurrentVideoYoutube;
  1225. },
  1226. },
  1227. timestampCheckbox: {
  1228. ["@change"]() {
  1229. this.toggleTimestamp();
  1230. },
  1231. [":checked"]() {
  1232. return this.includeTimestamp;
  1233. },
  1234. },
  1235. refreshTimestampButton: {
  1236. ["@click"]() {
  1237. this.refreshVideoTimestamp();
  1238. },
  1239. [":disabled"]() {
  1240. return !this.isCurrentVideoYoutube;
  1241. },
  1242. },
  1243. categoryButton: {
  1244. ["@click"]() {
  1245. const category = this.$el.dataset.category;
  1246. this.selectCategory(category);
  1247. },
  1248. [":class"]() {
  1249. const category = this.$el.dataset.category;
  1250. return {
  1251. active: this.selectedCategory === category,
  1252. };
  1253. },
  1254. },
  1255. saveChannelNotice: {
  1256. ["x-show"]() {
  1257. return !this.isCurrentTabChannelPage;
  1258. },
  1259. },
  1260. wikiInspNotice: {
  1261. ["x-show"]() {
  1262. return !this.isCurrentVideoYoutube;
  1263. },
  1264. },
  1265. intervalInput: {
  1266. [":value"]() {
  1267. return this.checkInterval;
  1268. },
  1269. ["@input"]() {
  1270. this.updateCheckInterval(this.$el.value);
  1271. },
  1272. },
  1273. // Add current page button binding
  1274. addCurrentPageButton: {
  1275. ["@click"]() {
  1276. this.addCurrentPageToPlaylist();
  1277. },
  1278. [":disabled"]() {
  1279. return (
  1280. !this.currentTab ||
  1281. !this.isCurrentTabYoutube ||
  1282. !this.currentPlaylistName
  1283. );
  1284. },
  1285. [":class"]() {
  1286. return {
  1287. disabled:
  1288. !this.currentTab ||
  1289. !this.isCurrentTabYoutube ||
  1290. !this.currentPlaylistName,
  1291. };
  1292. },
  1293. },
  1294. // Import view bindings
  1295. importTextarea: {
  1296. [":value"]() {
  1297. return this.importText;
  1298. },
  1299. ["@input"]() {
  1300. this.importText = this.$el.value;
  1301. },
  1302. },
  1303. importSubmitButton: {
  1304. ["@click"]() {
  1305. this.handleImport();
  1306. },
  1307. },
  1308. }));
  1309. });