signal map to nested structure

AuthorKonata <konata@posteo.jp>
Date
Commitf976c4606ef1306e771a659c3c6a9486b7d4746d
Parent2e490e6
3 files changed, 56 insertions(+), 21 deletions(-)
▾Mwebclient/src/components/DownloadButton.tsx
@@ -1,6 +1,6 @@
import "./DownloadButton.css";
import type { MediaFile, Directory, FileListing } from "music-server-shared/types";
import { deleteFile, download, downloadStatusMap } from "../offline";
import { deleteFile, download, getDownloadStatus } from "../offline";
import type { Accessor, ResourceActions } from "solid-js";
import type { AppOptions } from "../types";
import { formatBytes, joinPath } from "../utils";
@@ -30,7 +30,8 @@ export function DownloadButton(props: DownloadButtonProps) {
}}
>
{(() => {
const progress = downloadStatusMap[fullPath];
const [getStatus] = getDownloadStatus(fullPath);
const progress = getStatus();
if (progress === "UnspecificProgress") return <LoadingSpinner center={true} />;
if (props.isOffline()) {
return (
@@ -53,7 +54,6 @@ export function DownloadButton(props: DownloadButtonProps) {
</button>
);
}
if ("bytes" in progress) return <span>{progress.bytes === 0 ? "Queued..." : formatBytes(progress.bytes)}</span>;
console.error("Unexpected", progress);
})()}
▾Mwebclient/src/components/Playlist.tsx
@@ -13,7 +13,7 @@ import "./Playlist.css";
import { getItemName, type PlayerState, type PlaylistItem, type AppOptions } from "../types";
import { formatTime, LocalStorageValues, toast } from "../utils";
import { ClearSVG, DownloadSVG, DownSVG, DragSVG, LinkSVG, SaveSVG, SyncSVG, UpSVG } from "./SVG";
import { savePlaylist, download, downloadStatusMap, getStoredFiles } from "../offline";
import { savePlaylist, download, getStoredFiles, getDownloadStatus } from "../offline";
import { DraggableList } from "./DraggableList";
import type { FileListing } from "music-server-shared/types";
@@ -117,7 +117,7 @@ export default function Playlist(props: PlaylistProps) {
const [syncedCount, setSyncedCount] = createSignal(0);
createEffect(
on(
() => props.playlist().map((item) => downloadStatusMap[item.path]),
() => props.playlist().map((item) => getDownloadStatus(item.path)[0]()),
async () => {
const playlist = props.playlist();
const storedFiles = await getStoredFiles();
@@ -125,7 +125,6 @@ export default function Playlist(props: PlaylistProps) {
},
),
);
onMount(() => {
const savedPlaylistButtons = localStorage.getItem(LocalStorageValues.playlistButtons);
if (savedPlaylistButtons !== null) setShowActionButtons(savedPlaylistButtons === "true");
▾Mwebclient/src/offline.ts
@@ -8,8 +8,7 @@ import type {
import { parseMap } from "music-server-shared/mapconversion";
import type { AppOptions, Playlist, PlaylistItem } from "./types";
import { joinPath, toast } from "./utils";
import type { Accessor, ResourceActions } from "solid-js";
import { createStore } from "solid-js/store";
import { createSignal, type Accessor, type ResourceActions, type Signal } from "solid-js";
import { AsyncSemaphore } from "./semaphore";
import { encodePath } from "music-server-shared/utils";
@@ -19,7 +18,40 @@ interface Progress {
export type DownloadStatus = Progress | "UnspecificProgress" | "Synced" | undefined;
//used to display download status for directories/files in the filebrowser view
export const [downloadStatusMap, setDownloadStatusMap] = createStore<Record<string, DownloadStatus>>({});
// export const [downloadStatusMap, setDownloadStatusMap] = createStore<Record<string, DownloadStatus>>({});
interface StatusWithChildren {
status: Signal<DownloadStatus>;
children?: RecursiveStatus;
}
type RecursiveStatus = Map<string, StatusWithChildren>;
const downloadStatusMap: RecursiveStatus = new Map<string, StatusWithChildren>();
export function getDownloadStatus(path: string) {
const parts = path.split("/");
let current = downloadStatusMap;
//iterate through the basepath
for (const part of parts.slice(0, -1)) {
let next = current.get(part);
if (!next) {
next = { status: createSignal<DownloadStatus>(undefined) };
current.set(part, next);
}
if (!next.children) {
//might be a directory whose children have not been referenced before, in which case "children" would be undefined
next.children = new Map();
}
current = next.children as RecursiveStatus;
}
const fileName = parts.toReversed()[0];
let ret = current.get(fileName);
if (!ret) {
ret = { status: createSignal<DownloadStatus>(undefined) };
current.set(fileName, ret);
}
return ret.status;
}
//initialize filetrees and download statuses from indexedDB
export async function loadDataFromDB() {
@@ -27,7 +59,8 @@ export async function loadDataFromDB() {
const db = await openDB();
const files = await db.listFiles(FileStore);
for (const file of files) {
setDownloadStatusMap(file.path, "Synced");
const [, setStatus] = getDownloadStatus(file.path);
setStatus("Synced");
updateFileTree(file.path, { metadata: file.metadata }, true);
}
} catch (e) {
@@ -304,9 +337,10 @@ export async function deleteFile(
fetchFiles: ResourceActions<FileListing>,
) {
const fullPath = joinPath(basePath, fileName);
setDownloadStatusMap(fullPath, "UnspecificProgress");
const [getStatus, setStatus] = getDownloadStatus(fullPath);
try {
if ("metadata" in item && !downloadStatusMap[fullPath]) return;
if ("metadata" in item && getStatus() !== "Synced") return;
setStatus("UnspecificProgress");
let toDelete: string[];
if ("files" in item) {
@@ -319,11 +353,11 @@ export async function deleteFile(
await db.deleteAll(FileStore, toDelete);
deleteOfflinePath(fullPath);
for (const path of toDelete) {
setDownloadStatusMap(path, undefined);
setStatus(undefined);
}
fetchFiles.refetch();
} finally {
setDownloadStatusMap(fullPath, undefined);
setStatus(undefined);
}
}
@@ -338,11 +372,12 @@ async function downloadFile(
item: MediaFile,
fetchFiles: ResourceActions<FileListing>,
): Promise<void> {
const initialProgress = downloadStatusMap[path];
const [getStatus, setStatus] = getDownloadStatus(path);
const initialProgress = getStatus();
if (typeof initialProgress === "object" && "bytes" in initialProgress) return; // download already in progress
try {
setDownloadStatusMap(path, { bytes: 0 });
setStatus({ bytes: 0 });
const db = await openDB();
let mimeType: string | undefined;
@@ -372,7 +407,7 @@ async function downloadFile(
const now = Date.now();
if (now - lastUpdate < 250) continue;
lastUpdate = now;
setDownloadStatusMap(path, { bytes: received });
setStatus({ bytes: received });
}
blob = await blobPromise;
} finally {
@@ -385,7 +420,7 @@ async function downloadFile(
metadata: item.metadata,
} as StoredFile);
updateFileTree(path, item, true);
setDownloadStatusMap(path, "Synced");
setStatus("Synced");
const basePath = path.split("/").slice(0, -1).join("/");
const fileName = path.split("/").toReversed()[0];
//add files to listing if the containing directory is currently shown
@@ -400,7 +435,7 @@ async function downloadFile(
} catch (e) {
toast(`Downloading ${path} failed`, "error", e);
//restore original status
setDownloadStatusMap(path, initialProgress === "Synced" ? "Synced" : undefined);
setStatus(initialProgress === "Synced" ? "Synced" : undefined);
}
}
@@ -416,7 +451,8 @@ export async function download(
if ("metadata" in item) {
await downloadFile(fullPath, currentDir, options, item, fetchFiles);
} else {
setDownloadStatusMap(fullPath, "UnspecificProgress");
const [, setStatus] = getDownloadStatus(fullPath);
setStatus("UnspecificProgress");
try {
await getCover(fullPath, false);
//start downloads for all children to allow them to run in parallel, then wait for the results
@@ -427,7 +463,7 @@ export async function download(
await Promise.allSettled(promises);
} finally {
//always set to undefined afterwards to show the normal sync icon, since we don't know for sure if the directory is completely synced
setDownloadStatusMap(fullPath, undefined);
setStatus(undefined);
}
}
}