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, 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 function generateIds(items: Omit<PlaylistItem, "id">[]): PlaylistItem[] {
87 return items.map((item) => ({ ...item, id: generateUUID() }));
88 }
89
90 return (
91 <dialog ref={dialogElement} onClose={handleClose}>
92 <div>
93 <div class="playlistmanager-header title">
94 Playlists
95 <div class="spacer" />
96 <button title="Export playlists" onClick={exportPlaylists}>
97 <DownloadSVG />
98 </button>
99 <button title="Import playlists" onClick={importPlaylists}>
100 <UploadSVG />
101 </button>
102 <button title="Close dialog" onClick={handleClose}>
103 <ClearSVG />
104 </button>
105 </div>
106 <hr />
107 <div class="playlistmanager-content">
108 <Show when={playlists.loading || loadingActions() > 0}>
109 <LoadingScreen />
110 </Show>
111 <Show when={!playlists.loading}>
112 <For each={playlists()}>
113 {(item) => {
114 const [showSongs, setShowSongs] = createSignal(false);
115 return (
116 <div class="playlistmanager-item">
117 <span>{item.name}</span>
118 <hr />
119 <div class="playlistmanager-buttons">
120 <span>
121 {item.items.length} Items ({formatTime(getTotalDuration(item))})
122 </span>
123 <button
124 title="Show playlist items"
125 onClick={() => {
126 setShowSongs(!showSongs());
127 }}
128 >
129 Show
130 </button>
131 <button
132 title="Replace current playlist with this one"
133 onClick={() => {
134 const playlist = generateIds(item.items);
135 props.setPlaylist(playlist);
136 if (item.items.length > 0) props.playTrack(playlist[0]);
137 handleClose();
138 }}
139 >
140 Play
141 </button>
142 <button
143 title="Add items from this playlist to current one"
144 onClick={() => props.setPlaylist(props.playlist().concat(generateIds(item.items)))}
145 >
146 Queue
147 </button>
148 <button
149 title="Delete playlist"
150 onClick={async () => {
151 setLoadingActions((prev) => prev + 1);
152 try {
153 await deletePlaylist(item.name);
154 fetchPlaylists.refetch();
155 } finally {
156 setLoadingActions((prev) => prev - 1);
157 }
158 }}
159 >
160 <DeleteSVG />
161 </button>
162 </div>
163 <Show when={showSongs()}>
164 <For each={item.items}>
165 {(item) => {
166 return (
167 <>
168 {item.name}
169 <br />
170 </>
171 );
172 }}
173 </For>
174 </Show>
175 </div>
176 );
177 }}
178 </For>
179 </Show>
180 </div>
181 </div>
182 </dialog>
183 );
184}
185