PlaylistManager.tsx
⎇
Raw
1import { createEffect, createResource, createSignal, For, Show, Signal, type Accessor, type Setter } from "solid-js";
2import "./PlaylistManager.css";
3import "./Dialog.css";
4import "toastify-js/src/toastify.css";
5import type { Playlist, PlaylistItem } from "../types";
6import { formatTime, generateUUID, sleep, toast } from "../utils";
7import { ClearSVG, DeleteSVG, DownloadSVG, UploadSVG } from "./SVG";
8import { deletePlaylist, loadPlaylists, savePlaylist } from "../offline";
9import { LoadingScreen } from "./LoadingScreen";
10
11interface PlaylistManagerProps {
12 showPlaylistManager: Accessor<boolean>;
13 setShowPlaylistManager: Setter<boolean>;
14 playlist: Accessor<PlaylistItem[]>;
15 setPlaylist: Setter<PlaylistItem[]>;
16 playTrack: (track: PlaylistItem) => void;
17}
18
19export default function PlaylistManager(props: PlaylistManagerProps) {
20 const [playlists, fetchPlaylists] = createResource<Playlist[]>(
21 async () => {
22 return await loadPlaylists();
23 },
24 { initialValue: [] },
25 );
26 const [loadingActions, setLoadingActions] = createSignal(0);
27 let dialogElement!: HTMLDialogElement;
28
29 createEffect(() => {
30 if (props.showPlaylistManager()) {
31 dialogElement.showModal();
32 fetchPlaylists.refetch();
33 }
34 });
35
36 function handleClose() {
37 dialogElement.close();
38 props.setShowPlaylistManager(false);
39 }
40
41 function getTotalDuration(playlist: Playlist) {
42 if (playlist.items.length === 0) return 0;
43 //for some reason "duration" is a string at runtime...
44 return playlist.items.map((item) => Number.parseFloat(item.duration as unknown as string)).reduce((a, b) => a + b);
45 }
46
47 function exportPlaylists() {
48 toast("Exporting playlists...", "info");
49 const blob = new Blob([JSON.stringify(playlists())], { type: "application/json" });
50
51 const link = document.createElement("a");
52 link.href = URL.createObjectURL(blob);
53 link.download = "pico-playlists.json";
54 document.body.appendChild(link);
55 link.click();
56 document.body.removeChild(link);
57 URL.revokeObjectURL(link.href);
58 }
59
60 function importPlaylists() {
61 const input = document.createElement("input");
62 input.type = "file";
63
64 input.onchange = async (event) => {
65 const target = event.target as HTMLInputElement;
66 const file = target.files?.[0];
67 if (file) {
68 try {
69 toast("Importing playlists...", "info");
70 setLoadingActions((prev) => prev + 1);
71 const decoder = new TextDecoder();
72 const imports = JSON.parse(decoder.decode(await file.bytes())) as Playlist[];
73 for (const imported of imports) {
74 await savePlaylist(imported, true);
75 }
76 setLoadingActions((prev) => prev - 1);
77 fetchPlaylists.refetch();
78 } catch (e) {
79 toast("Importing playlists failed", "error", e);
80 }
81 }
82 };
83 input.click();
84 }
85
86 return (
87 <dialog ref={dialogElement} onClose={handleClose}>
88 <div>
89 <div class="playlistmanager-header title">
90 Playlists
91 <div class="spacer" />
92 <button title="Export playlists" onClick={exportPlaylists}>
93 <DownloadSVG />
94 </button>
95 <button title="Import playlists" onClick={importPlaylists}>
96 <UploadSVG />
97 </button>
98 <button title="Close dialog" onClick={handleClose}>
99 <ClearSVG />
100 </button>
101 </div>
102 <hr />
103 <div class="playlistmanager-content">
104 <Show when={playlists.loading || loadingActions() > 0}>
105 <LoadingScreen />
106 </Show>
107 <Show when={!playlists.loading}>
108 <For each={playlists()}>
109 {(item) => {
110 const [showSongs, setShowSongs] = createSignal(false);
111 return (
112 <div class="playlistmanager-item">
113 <span>{item.name}</span>
114 <hr />
115 <div class="playlistmanager-buttons">
116 <span>
117 {item.items.length} Items ({formatTime(getTotalDuration(item))})
118 </span>
119 <button
120 title="Show playlist items"
121 onClick={() => {
122 setShowSongs(!showSongs());
123 }}
124 >
125 Show
126 </button>
127 <button
128 title="Replace current playlist with this one"
129 onClick={() => {
130 props.setPlaylist(item.items);
131 if (item.items.length > 0) props.playTrack(item.items[0]);
132 handleClose();
133 }}
134 >
135 Play
136 </button>
137 <button
138 title="Add items from this playlist to current one"
139 onClick={() => props.setPlaylist(props.playlist().concat(item.items))}
140 >
141 Queue
142 </button>
143 <button
144 title="Delete playlist"
145 onClick={async () => {
146 setLoadingActions((prev) => prev + 1);
147 try {
148 await deletePlaylist(item.name);
149 fetchPlaylists.refetch();
150 } finally {
151 setLoadingActions((prev) => prev - 1);
152 }
153 }}
154 >
155 <DeleteSVG />
156 </button>
157 </div>
158 <Show when={showSongs()}>
159 <For each={item.items}>
160 {(item) => {
161 return (
162 <>
163 {item.name}
164 <br />
165 </>
166 );
167 }}
168 </For>
169 </Show>
170 </div>
171 );
172 }}
173 </For>
174 </Show>
175 </div>
176 </div>
177 </dialog>
178 );
179}
180