fix parent directory progress
Mwebclient/src/components/DownloadButton.tsx
@@ -1,14 +1,7 @@
import "./DownloadButton.css";
import type { MediaFile, Directory, FileListing } from "music-server-shared/types";
import {
deleteFile,
download,
type DownloadStatus,
getDownloadStatus,
getDownloadStatusWithChildren,
type StatusWithChildren,
} from "../offline";
import { createMemo, type Accessor, type ResourceActions, type Signal } from "solid-js";
import type { MediaFile, Directory } from "music-server-shared/types";
import { deleteFile, download, getDownloadStatus } from "../offline";
import { createMemo, type Accessor, type ResourceActions } from "solid-js";
import type { AppOptions } from "../types";
import { formatBytes, joinPath } from "../utils";
import { DeleteSVG, RefreshSVG, SyncSVG } from "./SVG";
@@ -27,24 +20,6 @@ interface DownloadButtonProps {
export function DownloadButton(props: DownloadButtonProps) {
const fullPath = joinPath(props.currentDir(), props.fileName);
const containsProgressChildren = createMemo(() => {
const status = getDownloadStatusWithChildren(fullPath);
if (!status.children) return false;
function inProgress(signal: Signal<DownloadStatus>) {
const status = signal[0]();
return status === "UnspecificProgress" || (typeof status === "object" && "bytes" in status);
}
function recurse(statusWithChildren: StatusWithChildren) {
for (const [, child] of statusWithChildren.children || []) {
if (inProgress(child.status) || recurse(child)) return true;
}
return false;
}
return recurse(status);
});
return (
<div
class="download-item"
@@ -56,24 +31,34 @@ export function DownloadButton(props: DownloadButtonProps) {
}}
>
{(() => {
const [getStatus] = getDownloadStatus(fullPath);
const progress = getStatus();
if (progress === "UnspecificProgress" || containsProgressChildren()) return <LoadingSpinner center={true} />;
if (props.isOffline()) {
//memo needed so progress amount doesn't cause rerenders
type ButtonType = "Spinner" | "Delete" | "Sync" | "Refresh" | { bytes: number };
const progressMemo = createMemo<ButtonType>(() => {
const progress = getDownloadStatus(fullPath)[0]();
if (typeof progress === "object" && "amount" in progress) return "Spinner";
if (props.isOffline()) return "Delete";
if (progress === undefined) return "Sync";
if (progress === "Synced") return "Refresh";
if ("bytes" in progress) return { bytes: progress.bytes };
throw new Error(`Unexpected: ${progress}`);
});
const progress = progressMemo();
if (progress === "Spinner") return <LoadingSpinner center={true} />;
if (progress === "Delete") {
return (
<button title="Purge this file/these files from local sync storage">
<DeleteSVG />
</button>
);
}
if (progress === undefined) {
if (progress === "Sync") {
return (
<button title="Sync this file/directory to your browser for offline use">
<SyncSVG />
</button>
);
}
if (progress === "Synced") {
if (progress === "Refresh") {
return (
<button title="Re-sync this file">
<RefreshSVG />
Mwebclient/src/offline.ts
@@ -17,7 +17,11 @@ interface Progress {
bytes: number;
}
export type DownloadStatus = Progress | "UnspecificProgress" | "Synced" | undefined;
interface LoadingItems {
amount: number;
}
export type DownloadStatus = Progress | LoadingItems | "Synced" | undefined;
//used to display download status for directories/files in the filebrowser view
// export const [downloadStatusMap, setDownloadStatusMap] = createStore<Record<string, DownloadStatus>>({});
export interface StatusWithChildren {
@@ -28,7 +32,19 @@ export interface StatusWithChildren {
type RecursiveStatus = Map<string, StatusWithChildren>;
const downloadStatusMap: RecursiveStatus = new Map<string, StatusWithChildren>();
export function getDownloadStatusWithChildren(path: string) {
function updateProgressAmount(statusSignal: Signal<DownloadStatus>, loadChange: number) {
const [getStatus, setStatus] = statusSignal;
let status = getStatus();
if (typeof status !== "object" || !("amount" in status)) {
status = { amount: 0 };
setStatus(status);
}
const newAmount = status.amount + loadChange;
if (newAmount > 0) setStatus({ amount: newAmount });
else setStatus(undefined);
}
export function getDownloadStatus(path: string, loadChange?: number) {
const parts = path.split("/");
let current = downloadStatusMap;
//iterate through the basepath
@@ -42,6 +58,8 @@ export function getDownloadStatusWithChildren(path: string) {
//might be a directory whose children have not been referenced before, in which case "children" would be undefined
next.children = new Map();
}
if (loadChange) updateProgressAmount(next.status, loadChange);
current = next.children as RecursiveStatus;
}
@@ -51,11 +69,16 @@ export function getDownloadStatusWithChildren(path: string) {
ret = { status: createSignal<DownloadStatus>(undefined) };
current.set(fileName, ret);
}
return ret;
return ret.status;
}
export function getDownloadStatus(path: string) {
return getDownloadStatusWithChildren(path).status;
//increase/decrease load amount for all parents and return requested signal
//needed to set parent directories to a loading state when any child is being downloaded/deleted
function startProgress(path: string) {
return getDownloadStatus(path, 1);
}
function stopProgress(path: string) {
return getDownloadStatus(path, -1);
}
//initialize filetrees and download statuses from indexedDB
@@ -64,8 +87,7 @@ export async function loadDataFromDB() {
const db = await openDB();
const files = await db.listFiles(FileStore);
for (const file of files) {
const [, setStatus] = getDownloadStatus(file.path);
setStatus("Synced");
getDownloadStatus(file.path)[1]("Synced");
updateFileTree(file.path, { metadata: file.metadata }, true);
}
} catch (e) {
@@ -344,10 +366,10 @@ export async function deleteFile(
fetchFiles: ResourceActions<FlatFileListing>,
) {
const fullPath = joinPath(basePath, fileName);
const [getStatus, setStatus] = getDownloadStatus(fullPath);
const [getStatus] = getDownloadStatus(fullPath);
try {
if ("metadata" in item && getStatus() !== "Synced") return;
setStatus("UnspecificProgress");
startProgress(fullPath)[1]({ amount: 1 });
let toDelete: string[];
if ("files" in item) {
@@ -357,14 +379,14 @@ export async function deleteFile(
}
const db = await openDB();
await db.deleteAll(FileStore, toDelete);
deleteInOfflineTree(fullPath);
await db.deleteAll(FileStore, toDelete); //delete from database
deleteInOfflineTree(fullPath); //delete node from tree
for (const path of toDelete) {
getDownloadStatus(path)[1](undefined);
getDownloadStatus(path)[1](undefined); // set all children to unsynced
}
fetchFiles.refetch();
} finally {
setStatus(undefined);
stopProgress(fullPath)[1](undefined);
}
}
@@ -384,7 +406,7 @@ async function downloadFile(
if (typeof initialProgress === "object" && "bytes" in initialProgress) return; // download already in progress
try {
setStatus({ bytes: 0 });
startProgress(path)[1]({ bytes: 0 });
const db = await openDB();
let mimeType: string | undefined;
@@ -426,12 +448,12 @@ async function downloadFile(
metadata: { ...item.metadata }, //spread needed, because item might be a proxy object from the filebrowser, which can't be stored
} as StoredFile);
updateFileTree(path, item, true);
setStatus("Synced");
stopProgress(path)[1]("Synced");
fetchFiles.refetch();
} catch (e) {
toast(`Downloading ${path} failed`, "error", e);
//restore original status
setStatus(initialProgress === "Synced" ? "Synced" : undefined);
stopProgress(path)[1](initialProgress === "Synced" ? "Synced" : undefined);
}
}
@@ -447,13 +469,18 @@ export async function download(
if ("metadata" in item) {
await downloadFile(fullPath, currentDir, options, item, fetchFiles);
} else {
await getCover(fullPath, false);
//start downloads for all children to allow them to run in parallel, then wait for the results
const promises: Promise<unknown>[] = [];
for (const [childName, childItem] of await listFiles(fullPath, false, false)) {
promises.push(download(childName, childItem, fullPath, currentDir, options, fetchFiles));
updateProgressAmount(startProgress(fullPath), 1);
try {
await getCover(fullPath, false);
//start downloads for all children to allow them to run in parallel, then wait for the results
const promises: Promise<unknown>[] = [];
for (const [childName, childItem] of await listFiles(fullPath, false, false)) {
promises.push(download(childName, childItem, fullPath, currentDir, options, fetchFiles));
}
await Promise.allSettled(promises);
} finally {
updateProgressAmount(stopProgress(fullPath), -1);
}
await Promise.allSettled(promises);
}
}