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 try {
58 toast("Importing playlists...", "info");
59 setLoadingActions((prev) => prev + 1);
60 const imports = JSON.parse(await file.text()) as Playlist[];
61 for (const imported of imports) {
62 await savePlaylist(imported, true);
63 }
64 setLoadingActions((prev) => prev - 1);
65 fetchPlaylists.refetch();
66 } catch (e) {
67 toast("Importing playlists failed", "error", e);
68 }
69 }
70 };
71 input.click();
72 }
73
74 function generateIds(items: Omit<PlaylistItem, "id">[]): PlaylistItem[] {
75 return items.map((item) => ({ ...item, id: crypto.randomUUID() }));
76 }
77
78 return (
79 <Modal
80 show={props.showPlaylistManager}
81 setShow={props.setShowPlaylistManager}
82 onOpen={() => fetchPlaylists.refetch()}
83 >
84 {(close) => (
85 <div>
86 <div class="playlistmanager-header title">
87 Playlists
88 <div class="spacer" />
89 <button title="Export playlists" onClick={exportPlaylists}>
90 <DownloadSVG />
91 </button>
92 <button title="Import playlists" onClick={importPlaylists}>
93 <UploadSVG />
94 </button>
95 <button title="Close dialog" onClick={close}>
96 <ClearSVG />
97 </button>
98 </div>
99 <hr />
100 <div class="playlistmanager-content">
101 <Show when={playlists.loading || loadingActions() > 0}>
102 <LoadingScreen />
103 </Show>
104 <Show when={!playlists.loading}>
105 <For each={playlists()}>
106 {(item) => {
107 const [showSongs, setShowSongs] = createSignal(false);
108 return (
109 <div class="playlistmanager-item">
110 <span>{item.name}</span>
111 <hr />
112 <div class="playlistmanager-buttons">
113 <span>
114 {item.items.length} Items ({formatTime(getTotalDuration(item))})
115 </span>
116 <button
117 title="Show playlist items"
118 onClick={() => {
119 setShowSongs(!showSongs());
120 }}
121 >
122 Show
123 </button>
124 <button
125 title="Replace current playlist with this one"
126 onClick={() => {
127 const playlist = generateIds(item.items);
128 props.setPlaylist(playlist);
129 if (item.items.length > 0) props.playTrack(playlist[0]);
130 close();
131 }}
132 >
133 Play
134 </button>
135 <button
136 title="Add items from this playlist to current one"
137 onClick={() => props.setPlaylist(props.playlist().concat(generateIds(item.items)))}
138 >
139 Queue
140 </button>
141 <button
142 title="Delete playlist"
143 onClick={async () => {
144 setLoadingActions((prev) => prev + 1);
145 try {
146 await deletePlaylist(item.name);
147 fetchPlaylists.refetch();
148 } finally {
149 setLoadingActions((prev) => prev - 1);
150 }
151 }}
152 >
153 <DeleteSVG />
154 </button>
155 </div>
156 <Show when={showSongs()}>
157 <For each={item.items}>
158 {(item) => {
159 return (
160 <>
161 {formatFilename(item, props.options.playlistDisplay)}
162 <br />
163 </>
164 );
165 }}
166 </For>
167 </Show>
168 </div>
169 );
170 }}
171 </For>
172 </Show>
173 </div>
174 </div>
175 )}
176 </Modal>
177 );
178}
179