popup.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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. // - periodically remove watched videos
  7. // - 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. // - view playback history (?)
  10. // - include currently open tabs in export
  11. // - support playlist management (add, remove, rename playlists)
  12. // - support input text box for adding to playlist (how to handle title?)
  13. // - option (probably a button) to add current page (url, title) to playlist
  14. // - button to add channel to youtube page (copy gql mutation to clipboard) (like the automa version)
  15. // - long-term: replace youtube page? rss feeds? need server?
  16. // - add personal rating feature ("enjoyed", "this was important", etc)
  17. Alpine.data("playlistManager", () => ({
  18. playlists: {},
  19. currentIndices: {},
  20. init() {
  21. this.loadPlaylists();
  22. },
  23. async loadPlaylists() {
  24. try {
  25. const result = await browser.storage.local.get("playlists");
  26. console.log("LOAD RESULT", result.playlists);
  27. this.playlists = result.playlists || {};
  28. if (result.playlists) {
  29. this.currentIndices = Object.keys(result.playlists).reduce(
  30. (acc, pln) => {
  31. const ind = result.playlists[pln].findIndex(
  32. (v) => v.status !== "done",
  33. );
  34. if (ind === -1) {
  35. acc[pln] = result.playlists[pln].length;
  36. } else {
  37. acc[pln] = ind;
  38. }
  39. return acc;
  40. },
  41. {},
  42. );
  43. } else {
  44. this.currentIndices = {};
  45. }
  46. } catch (error) {
  47. console.error("Error loading playlists:", error);
  48. }
  49. },
  50. formatPlaylistName(name) {
  51. // Convert "listening-1" to "Listening - 1"
  52. return name
  53. .split("-")
  54. .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
  55. .join(" - ");
  56. },
  57. openVideo(url) {
  58. browser.tabs.create({ url });
  59. },
  60. async removeVideo(playlistName, index) {
  61. const playlists = JSON.parse(JSON.stringify(this.playlists));
  62. // Make a copy of the current playlist
  63. const playlist = [...playlists[playlistName]];
  64. // Remove the video at the specified index
  65. playlist.splice(index, 1);
  66. // Create an updated playlists object with the remaining playlists unchanged
  67. const updatedPlaylists = {
  68. ...playlists,
  69. [playlistName]: playlist,
  70. };
  71. // Update the playlists in storage
  72. try {
  73. await browser.storage.local.set({ playlists: updatedPlaylists });
  74. this.playlists = updatedPlaylists;
  75. } catch (error) {
  76. console.error("Error removing video:", error);
  77. }
  78. },
  79. async moveVideoUp(playlistName, index) {
  80. // Can't move the first item up
  81. if (index <= 0) return;
  82. const playlists = JSON.parse(JSON.stringify(this.playlists));
  83. // Make a copy of the current playlist
  84. const playlist = [...playlists[playlistName]];
  85. // Swap the video with the one above it
  86. [playlist[index], playlist[index - 1]] = [
  87. playlist[index - 1],
  88. playlist[index],
  89. ];
  90. // Create an updated playlists object
  91. const updatedPlaylists = {
  92. ...playlists,
  93. [playlistName]: playlist,
  94. };
  95. // Update the playlists in storage
  96. try {
  97. await browser.storage.local.set({ playlists: updatedPlaylists });
  98. this.playlists = updatedPlaylists;
  99. } catch (error) {
  100. console.error("Error moving video up:", error);
  101. }
  102. },
  103. async moveVideoDown(playlistName, index) {
  104. const playlists = JSON.parse(JSON.stringify(this.playlists));
  105. const playlist = [...playlists[playlistName]];
  106. // Can't move the last item down
  107. if (index >= playlist.length - 1) return;
  108. // Swap the video with the one below it
  109. [playlist[index], playlist[index + 1]] = [
  110. playlist[index + 1],
  111. playlist[index],
  112. ];
  113. // Create an updated playlists object
  114. const updatedPlaylists = {
  115. ...playlists,
  116. [playlistName]: playlist,
  117. };
  118. // Update the playlists in storage
  119. try {
  120. await browser.storage.local.set({ playlists: updatedPlaylists });
  121. this.playlists = updatedPlaylists;
  122. } catch (error) {
  123. console.error("Error moving video down:", error);
  124. }
  125. },
  126. async exportPlaylists() {
  127. try {
  128. // Get current playlists
  129. const playlistResult = await browser.storage.local.get("playlists");
  130. const playlists = playlistResult.playlists || {};
  131. // Get playback history
  132. const historyResult = await browser.storage.local.get("history");
  133. const playbackHistory = historyResult.history || {};
  134. // Create export data object
  135. const exportData = {
  136. playlists,
  137. playbackHistory,
  138. exportDate: new Date().toISOString(),
  139. };
  140. // Convert to JSON
  141. const jsonString = JSON.stringify(exportData, null, 2);
  142. // Create download
  143. const blob = new Blob([jsonString], { type: "application/json" });
  144. const url = URL.createObjectURL(blob);
  145. // Trigger download
  146. const a = document.createElement("a");
  147. a.href = url;
  148. a.download = `playlists-export-${new Date().toISOString().split("T")[0]}.json`;
  149. document.body.appendChild(a);
  150. a.click();
  151. // Clean up
  152. setTimeout(() => {
  153. document.body.removeChild(a);
  154. URL.revokeObjectURL(url);
  155. }, 100);
  156. } catch (error) {
  157. console.error("Error exporting playlists:", error);
  158. }
  159. },
  160. videotitle: {
  161. ["@click"]() {
  162. console.log("TITLE CLICK", this.$el);
  163. },
  164. },
  165. videoPlayLink: {
  166. ["@click.prevent"]() {
  167. browser.tabs.update({ url: this.$el.href });
  168. },
  169. },
  170. isCurrentVideo(playlistName, index) {
  171. const currentIndex = this.currentIndices[playlistName];
  172. return currentIndex === index;
  173. },
  174. isDoneVideo(playlistName, index) {
  175. const currentIndex = this.currentIndices[playlistName];
  176. return index < currentIndex;
  177. },
  178. videoItemClass: {
  179. [":class"]() {
  180. const playlistName = this.$el.dataset.playlistName;
  181. const index = parseInt(this.$el.dataset.playlistIndex);
  182. const video = this.playlists[playlistName][index];
  183. return {
  184. "current-video": this.isCurrentVideo(playlistName, index),
  185. "done-video":
  186. this.isDoneVideo(playlistName, index) && video.status === "done",
  187. };
  188. },
  189. },
  190. removeVideoButton: {
  191. ["@click"]() {
  192. this.removeVideo(
  193. this.$el.dataset.playlistName,
  194. this.$el.dataset.playlistIndex,
  195. );
  196. },
  197. },
  198. moveUpButton: {
  199. ["@click"]() {
  200. this.moveVideoUp(
  201. this.$el.dataset.playlistName,
  202. parseInt(this.$el.dataset.playlistIndex),
  203. );
  204. },
  205. [":disabled"]() {
  206. return parseInt(this.$el.dataset.playlistIndex) === 0;
  207. },
  208. },
  209. moveDownButton: {
  210. ["@click"]() {
  211. this.moveVideoDown(
  212. this.$el.dataset.playlistName,
  213. parseInt(this.$el.dataset.playlistIndex),
  214. );
  215. },
  216. [":disabled"]() {
  217. return (
  218. parseInt(this.$el.dataset.playlistIndex) ===
  219. this.playlists[this.$el.dataset.playlistName].length - 1
  220. );
  221. },
  222. },
  223. exportButton: {
  224. ["@click"]() {
  225. this.exportPlaylists();
  226. },
  227. },
  228. importButton: {
  229. ["@click"]() {
  230. document.getElementById("import-file-input").click();
  231. },
  232. },
  233. importFile(event) {
  234. const file = event.target.files[0];
  235. if (!file) return;
  236. const reader = new FileReader();
  237. reader.onload = (e) => {
  238. try {
  239. const importedData = JSON.parse(e.target.result);
  240. this.validateAndImportPlaylists(importedData);
  241. } catch (error) {
  242. console.error("Error parsing JSON file:", error);
  243. alert(
  244. "Invalid JSON file. Please select a valid playlist export file.",
  245. );
  246. } finally {
  247. // Reset the file input so the same file can be selected again
  248. event.target.value = "";
  249. }
  250. };
  251. reader.readAsText(file);
  252. },
  253. validateAndImportPlaylists(data) {
  254. // Validate the playlists structure
  255. if (!data.playlists || typeof data.playlists !== "object") {
  256. alert("Invalid file format: Missing or invalid 'playlists' property");
  257. return;
  258. }
  259. // Validate each playlist
  260. const validPlaylists = {};
  261. let hasErrors = false;
  262. for (const [playlistName, videos] of Object.entries(data.playlists)) {
  263. // Check if videos is an array
  264. if (!Array.isArray(videos)) {
  265. console.error(
  266. `Playlist '${playlistName}' does not contain a valid array of videos`,
  267. );
  268. hasErrors = true;
  269. continue;
  270. }
  271. // Validate each video in the playlist
  272. const validVideos = videos.filter((video) => {
  273. if (!video || typeof video !== "object") {
  274. console.error(`Invalid video object in '${playlistName}'`);
  275. return false;
  276. }
  277. if (
  278. !video.url ||
  279. typeof video.url !== "string" ||
  280. !video.title ||
  281. typeof video.title !== "string"
  282. ) {
  283. console.error(
  284. `Video in '${playlistName}' missing required properties (url, title)`,
  285. );
  286. return false;
  287. }
  288. return true;
  289. });
  290. // Add the validated playlist if it has valid videos
  291. if (validVideos.length > 0) {
  292. validPlaylists[playlistName] = validVideos;
  293. }
  294. }
  295. if (Object.keys(validPlaylists).length === 0) {
  296. alert("No valid playlists found in the import file");
  297. return;
  298. }
  299. if (hasErrors) {
  300. const confirmImport = confirm(
  301. "Some playlists or videos were invalid and will be skipped. Do you want to continue with the import?",
  302. );
  303. if (!confirmImport) return;
  304. }
  305. // Update storage and state
  306. this.updatePlaylists(validPlaylists);
  307. },
  308. async updatePlaylists(playlists) {
  309. try {
  310. await browser.storage.local.set({ playlists });
  311. this.playlists = playlists;
  312. alert("Playlists imported successfully!");
  313. } catch (error) {
  314. console.error("Error updating playlists:", error);
  315. alert("Error importing playlists: " + error.message);
  316. }
  317. },
  318. }));
  319. });