popup.js 43 KB

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