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