mirror of
https://github.com/MrLowPr/overdrive.git
synced 2026-10-21 02:01:44 +02:00
277 lines
14 KiB
React
277 lines
14 KiB
React
// ../pub-game-store/native/game/DownloadButton.jsx
|
|
// reconstructed from the esbuild bundle in eboot.bin
|
|
|
|
import '../assets/Inter-Regular.ttf';
|
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { STALE_PARTIAL, cause, explainError } from '../downloads/errors.js';
|
|
import { WAITING, shownState } from '../downloads/labels.js';
|
|
import { Choice, TextAction } from './Choice.jsx';
|
|
import { DISABLED, DownloadLabel, statusLine } from './DownloadLabel.jsx';
|
|
import { MoreOptions, ProblemNote } from './DownloadProblem.jsx';
|
|
import { canDownload, spaceAdvice, spaceNeeded } from './releases.js';
|
|
import { fallbacks, isOverdriveCdn, preferredRelease } from './sources.js';
|
|
import { useDownload } from './useDownload.js';
|
|
import { useFreeSpace } from './useFreeSpace.js';
|
|
import { useShadowMount } from './useShadowMount.js';
|
|
import { formatPercent2 } from '../i18n/core.js';
|
|
import { useT } from '../i18n/index.js';
|
|
import { editionsOf } from '../state/catalog.js';
|
|
import { BUSY, COMPLETE } from '../state/download-queue.js';
|
|
import { downloadQueue, driveName, freeBytes, storageRoots } from '../state/downloads.js';
|
|
import { useNavActions } from '../state/nav.jsx';
|
|
import { formatBytes } from '../state/size.js';
|
|
import { MARGIN2 } from '../state/space.js';
|
|
import { FocusRing } from '../ui/FocusRing.jsx';
|
|
import { glide } from '../ui/ProgressBar.jsx';
|
|
import { ShimmerText } from '../ui/ShimmerText.jsx';
|
|
import { Shine } from '../ui/Shine.jsx';
|
|
import { EASE_OUT } from '../ui/ease.js';
|
|
import { DownloadFormats, Text, View, motion } from '@ps5-react/core';
|
|
|
|
var WIDTH6 = 272;
|
|
var GB2 = 1e9;
|
|
var STATUS = { fontFamily: "Inter-Regular", fontSize: 21, lineHeight: 30 };
|
|
function DownloadButton({ game, start, onDownloaded, nextDown, onRequestDialog }) {
|
|
const t2 = useT();
|
|
const releases = game.releases || [];
|
|
const [selected, setSelected] = (0, useState)(() => {
|
|
let jobs = [];
|
|
try {
|
|
jobs = downloadQueue().snapshot().filter((job3) => job3.gameId === game.id);
|
|
} catch {
|
|
}
|
|
const owned2 = jobs.map((job3) => releases.findIndex((release2) => release2.id === (job3.requestedId ?? job3.release.id))).find((index) => index >= 0);
|
|
return owned2 ?? preferredRelease(releases);
|
|
});
|
|
const [roots] = (0, useState)(() => {
|
|
try {
|
|
return storageRoots();
|
|
} catch {
|
|
return ["/data"];
|
|
}
|
|
});
|
|
const [storage, setStorage] = (0, useState)(() => {
|
|
const first = releases[preferredRelease(releases)];
|
|
return Math.max(0, roots.findIndex((root2) => {
|
|
const need2 = spaceNeeded(first, root2);
|
|
return !need2 || freeBytes(root2) >= need2 + MARGIN2;
|
|
}));
|
|
});
|
|
const release = releases[selected];
|
|
const alternatives = (0, useMemo)(() => fallbacks(releases, selected), [releases, selected]);
|
|
const download = useDownload(game, release, roots[storage], alternatives);
|
|
const { progress, restart, remove, deleteFromDisk, job: job2, error: error2 } = download;
|
|
const free = useFreeSpace(roots, download.status, !job2);
|
|
const need = spaceNeeded(release, roots[storage]);
|
|
const lacking = download.status === "idle" && need && free[storage] !== null && free[storage] < need + MARGIN2;
|
|
const stale = ["paused", "failed"].includes(download.status) && STALE_PARTIAL.test(cause(error2));
|
|
const status = lacking ? "no-space" : stale ? "stale" : download.status;
|
|
// A new download first shows what the chosen source is and how to use it (DownloadTipsDialog);
|
|
// resuming, installing or pausing one already queued goes straight on.
|
|
const begin = () => {
|
|
if (stale) restart();
|
|
else if (lacking) return;
|
|
else if (status === "idle" && release) onRequestDialog({ kind: "tips", game, release, submit: () => download.advance(), changeSource: () => chooseSource() });
|
|
else download.advance();
|
|
};
|
|
const { openEdition, focusLater, push } = useNavActions();
|
|
const editions = editionsOf(game);
|
|
const advance = () => {
|
|
if (status !== "idle" || editions.length < 2) {
|
|
begin();
|
|
return;
|
|
}
|
|
onRequestDialog({
|
|
kind: "version",
|
|
title: game.title,
|
|
editions,
|
|
current: game.id,
|
|
submit: (edition2) => {
|
|
if (edition2.id === game.id) begin();
|
|
else openEdition(edition2.id, true);
|
|
}
|
|
});
|
|
};
|
|
const latest2 = (0, useRef)(null);
|
|
latest2.current = { status, begin };
|
|
(0, useEffect)(() => {
|
|
if (!start) return void 0;
|
|
const id = setTimeout(() => {
|
|
if (latest2.current.status === "idle") latest2.current.begin();
|
|
}, 0);
|
|
return () => clearTimeout(id);
|
|
}, []);
|
|
const shadowMount = useShadowMount(DownloadFormats.images.includes(release?.format));
|
|
const browser = status === "awaiting-browser";
|
|
const step = job2?.parts ? { part: job2.partIndex + 1, parts: job2.parts.length } : null;
|
|
const requestFile = () => onRequestDialog({
|
|
title: step ? t2("download.partFileLink", step) : t2("download.fileLink"),
|
|
detail: t2("download.fileLinkDetail"),
|
|
submit: (url) => downloadQueue().continueWithURL(job2.id, url)
|
|
});
|
|
const written = (job2?.parts?.[job2.partIndex]?.written ?? job2?.written) > 0;
|
|
const recoverable = ["paused", "failed", "stale"].includes(status) || browser;
|
|
const choosing = !job2 || browser && !written;
|
|
const sizeGb = job2?.total ? job2.total / GB2 : release?.sizeBytes ? release.sizeBytes / GB2 : game.sizeGb;
|
|
const inProgress = BUSY.includes(status) || status === "paused";
|
|
const sizeless = inProgress && !job2?.total;
|
|
const amount = sizeless ? formatBytes(job2?.written || 0) : formatPercent2(Math.round(progress * 100));
|
|
const shown = job2 && shownState(job2) === "finishing" ? "finishing" : status;
|
|
const [state, line] = statusLine(stale ? download.status : shown, progress, sizeGb);
|
|
const detail2 = sizeless ? t2("download.soFar", { size: formatBytes(job2?.written || 0) }) : line;
|
|
const problem = browser ? {
|
|
title: step ? t2("download.browser.partTitle", step) : t2("download.browser.title"),
|
|
action: t2("download.browser.action")
|
|
} : error2 ? explainError(error2, Boolean(job2?.release.parts)) : lacking ? { title: t2("download.noFit"), action: spaceAdvice(roots, free, storage, need) } : shadowMount || status === "installed" ? null : { title: t2("shadowMount.offTitle"), action: t2("shadowMount.offAction") };
|
|
const notice = job2?.message;
|
|
const pick2 = (index) => {
|
|
if (index !== selected && job2 && !written && ["awaiting-browser", "failed", "paused"].includes(job2.state)) downloadQueue().remove(job2.id);
|
|
setSelected(index);
|
|
};
|
|
const chooseSource = (from) => onRequestDialog({
|
|
kind: "source",
|
|
title: game.title,
|
|
releases,
|
|
selected,
|
|
submit: pick2,
|
|
from,
|
|
editions,
|
|
current: game.id,
|
|
onEdition: (id) => openEdition(id)
|
|
});
|
|
const saved = job2?.written ? formatBytes(job2.written) : null;
|
|
const removal = { icon: "x", label: t2("download.remove.label"), detail: t2("download.remove.detail"), onSelect: remove };
|
|
const deletion = job2 && {
|
|
icon: "trash",
|
|
label: t2("hints.delete"),
|
|
detail: job2.artifact.destination,
|
|
onSelect: deleteFromDisk,
|
|
armed: { label: t2("hints.confirmDelete"), detail: t2("files.confirmDelete", { size: formatBytes(job2.total || 0) }) }
|
|
};
|
|
const options = COMPLETE.includes(status) ? [removal, deletion] : recoverable ? [
|
|
stale ? null : {
|
|
icon: "restart",
|
|
label: t2("download.restart.label"),
|
|
detail: saved ? t2("download.restart.detailSaved", { size: saved }) : t2("download.restart.detail"),
|
|
onSelect: restart
|
|
},
|
|
removal,
|
|
releases.length > 1 ? {
|
|
icon: "list",
|
|
label: t2("download.otherSource.label"),
|
|
detail: t2("download.otherSource.detail"),
|
|
onSelect: () => chooseSource(),
|
|
next: true
|
|
} : null
|
|
].filter(Boolean) : [];
|
|
const openOptions = () => onRequestDialog({ kind: "options", title: game.title, items: options });
|
|
const [pulse3, setPulse] = (0, useState)(false);
|
|
const hidden = (1 - progress) * WIDTH6;
|
|
const sourceFocus = `download-source:${game.id}`;
|
|
const storageFocus = `download-storage:${game.id}`;
|
|
const moreFocus = `download-more:${game.id}`;
|
|
const browserFocus = `download-continue:${game.id}`;
|
|
const queueFocus = `download-queue:${game.id}`;
|
|
const mainFocus = `download:${game.id}`;
|
|
const disabled = DISABLED.includes(status);
|
|
const sourceChoice = releases.length > 1 || editions.length > 1;
|
|
const chain = [
|
|
mainFocus,
|
|
choosing && release && sourceChoice && sourceFocus,
|
|
!job2 && roots.length > 1 && storageFocus,
|
|
browser && browserFocus,
|
|
job2 && queueFocus,
|
|
options.length > 0 && moreFocus
|
|
].filter(Boolean);
|
|
const after = (key) => chain[chain.indexOf(key) + 1] ?? nextDown;
|
|
(0, useEffect)(() => {
|
|
if (canDownload(game) && disabled) focusLater(`favorite:${game.id}`);
|
|
}, []);
|
|
(0, useEffect)(() => {
|
|
if (!COMPLETE.includes(status)) return;
|
|
setPulse(true);
|
|
onDownloaded();
|
|
}, [status]);
|
|
return /* @__PURE__ */ (0, jsxs)(View, { style: { width: 408, gap: 12 }, children: [
|
|
/* @__PURE__ */ (0, jsxs)(View, { onPress: advance, autoFocus: canDownload(game) && !disabled, focusKey: mainFocus, nextFocusDown: after(mainFocus), style: (_state) => [
|
|
{ position: "relative", height: 72, width: 408, borderRadius: 9 },
|
|
disabled ? { borderWidth: 2, borderColor: "#2a2a2f" } : inProgress ? { backgroundColor: "#212125" } : [{ backgroundColor: "#f2f2f3" }, _state.focused ? { backgroundColor: "#fff" } : null]
|
|
], children: [
|
|
/* @__PURE__ */ (0, jsxs)(
|
|
motion.View,
|
|
{
|
|
style: { overflow: "hidden", borderRadius: 9 },
|
|
initial: false,
|
|
animate: { scale: pulse3 ? 1.035 : 1 },
|
|
transition: { duration: 0.225, ease: EASE_OUT },
|
|
onAnimationComplete: () => setPulse(false),
|
|
children: [
|
|
/* @__PURE__ */ (0, jsx)(DownloadLabel, { status: shown, amount, dark: !inProgress && !disabled }),
|
|
inProgress && /* @__PURE__ */ (0, jsx)(
|
|
motion.View,
|
|
{
|
|
style: { position: "absolute", top: 0, right: 0, bottom: 0, left: 0, overflow: "hidden", backgroundColor: "#f2f2f3" },
|
|
initial: { x: -WIDTH6 },
|
|
animate: { x: -hidden },
|
|
transition: glide,
|
|
children: /* @__PURE__ */ (0, jsx)(motion.View, { initial: { x: WIDTH6 }, animate: { x: hidden }, transition: glide, children: /* @__PURE__ */ (0, jsx)(DownloadLabel, { status: shown, amount, dark: true }) })
|
|
}
|
|
),
|
|
/* @__PURE__ */ (0, jsx)(Shine, { width: WIDTH6 })
|
|
]
|
|
}
|
|
),
|
|
/* @__PURE__ */ (0, jsx)(FocusRing, {})
|
|
] }),
|
|
/* @__PURE__ */ (0, jsxs)(View, { style: { flexDirection: "row", justifyContent: "space-between" }, children: [
|
|
WAITING.includes(shown) ? /* @__PURE__ */ (0, jsx)(ShimmerText, { textStyle: STATUS, children: state }) : /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 21, color: "#8a8a94", lineHeight: 30 }, children: state }),
|
|
detail2 ? /* @__PURE__ */ (0, jsx)(Text, { style: { fontFamily: "Inter-Regular", fontSize: 21, color: "#8a8a94", lineHeight: 30 }, children: detail2 }) : null
|
|
] }),
|
|
problem ? /* @__PURE__ */ (0, jsx)(ProblemNote, { title: problem.title, action: problem.action }) : notice ? /* @__PURE__ */ (0, jsx)(Text, { style: { width: 408, fontFamily: "Inter-Regular", fontSize: 21, color: "#a3a3ab", numberOfLines: 3, ellipsizeMode: "tail", lineHeight: 30 }, children: notice.slice(0, 160) }) : null,
|
|
choosing && release ? /* @__PURE__ */ (0, jsx)(
|
|
Choice,
|
|
{
|
|
focusKey: sourceFocus,
|
|
nextFocusDown: after(sourceFocus),
|
|
onPress: sourceChoice ? () => chooseSource(sourceFocus) : null,
|
|
magic: isOverdriveCdn(release.provider) ? {
|
|
name: release.provider,
|
|
detail: [t2("sources.cdnFastest"), release.sizeBytes && formatBytes(release.sizeBytes)].filter(Boolean).join(" \xB7 ")
|
|
} : null,
|
|
text: [t2("download.source", { source: release.provider }), release.sizeBytes && formatBytes(release.sizeBytes)].filter(Boolean).join(" \xB7 ")
|
|
}
|
|
) : null,
|
|
!job2 ? /* @__PURE__ */ (0, jsx)(
|
|
Choice,
|
|
{
|
|
focusKey: storageFocus,
|
|
nextFocusDown: after(storageFocus),
|
|
onPress: roots.length > 1 ? () => setStorage((index) => (index + 1) % roots.length) : null,
|
|
text: [t2("download.saveTo", { drive: driveName(roots[storage]) }), free[storage] !== null && t2("download.free", { size: formatBytes(free[storage]) })].filter(Boolean).join(" \xB7 ")
|
|
}
|
|
) : null,
|
|
browser ? /* @__PURE__ */ (0, jsx)(
|
|
TextAction,
|
|
{
|
|
focusKey: browserFocus,
|
|
nextFocusDown: after(browserFocus),
|
|
onPress: requestFile,
|
|
text: step ? t2("download.enterPartLink", step) : t2("download.enterLink")
|
|
}
|
|
) : null,
|
|
job2 ? /* @__PURE__ */ (0, jsx)(
|
|
TextAction,
|
|
{
|
|
focusKey: queueFocus,
|
|
nextFocusDown: after(queueFocus),
|
|
text: t2("download.openQueue"),
|
|
onPress: () => push({ name: "downloads" }, queueFocus)
|
|
}
|
|
) : null,
|
|
options.length ? /* @__PURE__ */ (0, jsx)(MoreOptions, { focusKey: moreFocus, nextFocusDown: nextDown, onPress: openOptions }) : null
|
|
] });
|
|
}
|
|
|
|
export { DownloadButton };
|