mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 01:37:51 +02:00
520 lines
16 KiB
TypeScript
520 lines
16 KiB
TypeScript
import { invoke } from "@tauri-apps/api/core";
|
|
import type { Event as TauriEvent } from "@tauri-apps/api/event";
|
|
import { listen } from "@tauri-apps/api/event";
|
|
import { useCallback, useEffect, useState } from "react";
|
|
import i18n from "@/i18n";
|
|
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
|
import { isOnboardingActive } from "@/lib/onboarding-signal";
|
|
import {
|
|
dismissToast,
|
|
showDownloadToast,
|
|
showErrorToast,
|
|
showSuccessToast,
|
|
showToast,
|
|
} from "@/lib/toast-utils";
|
|
|
|
interface GithubRelease {
|
|
tag_name: string;
|
|
assets: {
|
|
name: string;
|
|
browser_download_url: string;
|
|
hash?: string;
|
|
}[];
|
|
published_at: string;
|
|
is_nightly: boolean;
|
|
}
|
|
|
|
interface BrowserVersionInfo {
|
|
version: string;
|
|
is_prerelease: boolean;
|
|
date: string;
|
|
}
|
|
|
|
interface DownloadProgress {
|
|
browser: string;
|
|
version: string;
|
|
downloaded_bytes: number;
|
|
total_bytes?: number;
|
|
percentage: number;
|
|
speed_bytes_per_sec: number;
|
|
eta_seconds?: number;
|
|
stage: string;
|
|
}
|
|
|
|
interface BrowserVersionsResult {
|
|
versions: string[];
|
|
new_versions_count?: number;
|
|
total_versions_count: number;
|
|
}
|
|
|
|
export function useBrowserDownload() {
|
|
const [availableVersions, setAvailableVersions] = useState<GithubRelease[]>(
|
|
[],
|
|
);
|
|
const [downloadedVersionsMap, setDownloadedVersionsMap] = useState<
|
|
Record<string, string[]>
|
|
>({});
|
|
const [downloadingBrowsers, setDownloadingBrowsers] = useState<Set<string>>(
|
|
new Set(),
|
|
);
|
|
const [downloadProgress, setDownloadProgress] =
|
|
useState<DownloadProgress | null>(null);
|
|
|
|
const formatTime = useCallback((seconds: number): string => {
|
|
if (seconds < 60) {
|
|
return `${Math.round(seconds)}s`;
|
|
}
|
|
if (seconds < 3600) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const remainingSeconds = Math.round(seconds % 60);
|
|
return `${minutes}m ${remainingSeconds}s`;
|
|
}
|
|
const hours = Math.floor(seconds / 3600);
|
|
const minutes = Math.floor((seconds % 3600) / 60);
|
|
return `${hours}h ${minutes}m`;
|
|
}, []);
|
|
|
|
const formatBytes = useCallback((bytes: number): string => {
|
|
if (bytes === 0) return "0 B";
|
|
const k = 1024;
|
|
const sizes = ["B", "KB", "MB", "GB"];
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
|
}, []);
|
|
|
|
const loadVersions = useCallback(async (browserStr: string) => {
|
|
const browserName = getBrowserDisplayName(browserStr);
|
|
|
|
// Use a simple loading state instead of toast for version fetching
|
|
console.log(`Fetching ${browserName} versions...`);
|
|
|
|
try {
|
|
const versionInfos = await invoke<BrowserVersionInfo[]>(
|
|
"fetch_browser_versions_cached_first",
|
|
{ browserStr },
|
|
);
|
|
|
|
// Convert BrowserVersionInfo to GithubRelease format for compatibility
|
|
const githubReleases: GithubRelease[] = versionInfos.map(
|
|
(versionInfo) => ({
|
|
tag_name: versionInfo.version,
|
|
assets: [],
|
|
published_at: versionInfo.date,
|
|
is_nightly: versionInfo.is_prerelease,
|
|
}),
|
|
);
|
|
|
|
setAvailableVersions(githubReleases);
|
|
return githubReleases;
|
|
} catch (error) {
|
|
console.error("Failed to load versions:", error);
|
|
showErrorToast(
|
|
i18n.t("browserDownload.toast.fetchVersionsFailed", {
|
|
browser: browserName,
|
|
}),
|
|
{
|
|
description:
|
|
error instanceof Error
|
|
? error.message
|
|
: i18n.t("common.errors.unknown"),
|
|
duration: 4000,
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
}, []);
|
|
|
|
const loadVersionsWithNewCount = useCallback(async (browserStr: string) => {
|
|
const browserName = getBrowserDisplayName(browserStr);
|
|
|
|
try {
|
|
// Get versions with new count info and cached detailed info
|
|
const result = await invoke<BrowserVersionsResult>(
|
|
"fetch_browser_versions_with_count_cached_first",
|
|
{ browserStr },
|
|
);
|
|
|
|
// Get detailed version info for compatibility
|
|
const versionInfos = await invoke<BrowserVersionInfo[]>(
|
|
"fetch_browser_versions_cached_first",
|
|
{ browserStr },
|
|
);
|
|
|
|
// Convert BrowserVersionInfo to GithubRelease format for compatibility
|
|
const githubReleases: GithubRelease[] = versionInfos.map(
|
|
(versionInfo) => ({
|
|
tag_name: versionInfo.version,
|
|
assets: [],
|
|
published_at: versionInfo.date,
|
|
is_nightly: versionInfo.is_prerelease,
|
|
}),
|
|
);
|
|
|
|
setAvailableVersions(githubReleases);
|
|
|
|
// Show notification about new versions if any were found
|
|
if (result.new_versions_count && result.new_versions_count > 0) {
|
|
showSuccessToast(
|
|
i18n.t("browserDownload.toast.foundNewVersions", {
|
|
count: result.new_versions_count,
|
|
browser: browserName,
|
|
}),
|
|
{
|
|
duration: 3000,
|
|
description: i18n.t(
|
|
"browserDownload.toast.totalAvailableVersions",
|
|
{ count: result.total_versions_count },
|
|
),
|
|
},
|
|
);
|
|
}
|
|
|
|
return githubReleases;
|
|
} catch (error) {
|
|
console.error("Failed to load versions:", error);
|
|
showErrorToast(
|
|
i18n.t("browserDownload.toast.fetchVersionsFailed", {
|
|
browser: browserName,
|
|
}),
|
|
{
|
|
description:
|
|
error instanceof Error
|
|
? error.message
|
|
: i18n.t("common.errors.unknown"),
|
|
duration: 4000,
|
|
},
|
|
);
|
|
throw error;
|
|
}
|
|
}, []);
|
|
|
|
const loadDownloadedVersions = useCallback(async (browserStr: string) => {
|
|
try {
|
|
const versions = await invoke<string[]>(
|
|
"get_downloaded_browser_versions",
|
|
{ browserStr },
|
|
);
|
|
setDownloadedVersionsMap((prev) => ({ ...prev, [browserStr]: versions }));
|
|
return versions;
|
|
} catch (error) {
|
|
console.error("Failed to load downloaded versions:", error);
|
|
throw error;
|
|
}
|
|
}, []);
|
|
|
|
const downloadBrowser = useCallback(
|
|
async (
|
|
browserStr: string,
|
|
version: string,
|
|
suppressNotifications = false,
|
|
) => {
|
|
const browserName = getBrowserDisplayName(browserStr);
|
|
setDownloadingBrowsers((prev) => new Set(prev).add(browserStr));
|
|
|
|
try {
|
|
// Check browser compatibility before attempting download
|
|
const isSupported = await invoke<boolean>(
|
|
"is_browser_supported_on_platform",
|
|
{ browserStr },
|
|
);
|
|
if (!isSupported) {
|
|
const supportedBrowsers = await invoke<string[]>(
|
|
"get_supported_browsers",
|
|
);
|
|
throw new Error(
|
|
`${browserName} is not supported on your platform. Supported browsers: ${supportedBrowsers
|
|
.map(getBrowserDisplayName)
|
|
.join(", ")}`,
|
|
);
|
|
}
|
|
|
|
await invoke("download_browser", { browserStr, version });
|
|
await loadDownloadedVersions(browserStr);
|
|
} catch (error) {
|
|
console.error("Failed to download browser:", error);
|
|
|
|
if (!suppressNotifications) {
|
|
// Dismiss any existing download toast and show error
|
|
dismissToast(`download-${browserStr}-${version}`);
|
|
|
|
let errorMessage = i18n.t("common.errors.unknown");
|
|
if (error instanceof Error) {
|
|
errorMessage = error.message;
|
|
} else if (typeof error === "string") {
|
|
errorMessage = error;
|
|
} else if (error && typeof error === "object" && "message" in error) {
|
|
errorMessage = String(error.message);
|
|
}
|
|
|
|
// Ensure the long-running download toast is dismissed, and show a finite error toast
|
|
dismissToast(`download-${browserStr}-${version}`);
|
|
showErrorToast(
|
|
i18n.t("browserDownload.toast.downloadFailed", {
|
|
browser: browserName,
|
|
version,
|
|
}),
|
|
{
|
|
description: errorMessage,
|
|
duration: 8000,
|
|
},
|
|
);
|
|
}
|
|
throw error;
|
|
} finally {
|
|
setDownloadingBrowsers((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(browserStr);
|
|
return next;
|
|
});
|
|
}
|
|
},
|
|
[loadDownloadedVersions],
|
|
);
|
|
|
|
const isVersionDownloaded = useCallback(
|
|
(version: string) => {
|
|
return Object.values(downloadedVersionsMap).some((versions) =>
|
|
versions.includes(version),
|
|
);
|
|
},
|
|
[downloadedVersionsMap],
|
|
);
|
|
|
|
// Check if a browser type is currently downloading
|
|
const isBrowserDownloading = useCallback(
|
|
(browserStr: string) => {
|
|
return downloadingBrowsers.has(browserStr);
|
|
},
|
|
[downloadingBrowsers],
|
|
);
|
|
|
|
// Legacy isDownloading for backwards compatibility
|
|
const isDownloading = downloadingBrowsers.size > 0;
|
|
|
|
// Listen for download progress events (browsers) and GeoIP progress events
|
|
useEffect(() => {
|
|
let unlistenBrowser: (() => void) | null = null;
|
|
let unlistenGeoip: (() => void) | null = null;
|
|
|
|
const setupListeners = async () => {
|
|
try {
|
|
// Browser binaries download progress
|
|
unlistenBrowser = await listen<DownloadProgress>(
|
|
"download-progress",
|
|
async (event: TauriEvent<DownloadProgress>) => {
|
|
const progress = event.payload;
|
|
setDownloadProgress(progress);
|
|
|
|
if (
|
|
progress.stage === "downloading" ||
|
|
progress.stage === "extracting" ||
|
|
progress.stage === "verifying"
|
|
) {
|
|
setDownloadingBrowsers((prev) => {
|
|
if (prev.has(progress.browser)) return prev;
|
|
return new Set(prev).add(progress.browser);
|
|
});
|
|
}
|
|
|
|
const browserName = getBrowserDisplayName(progress.browser);
|
|
|
|
if (progress.stage === "downloading") {
|
|
const speedMBps = (
|
|
progress.speed_bytes_per_sec /
|
|
(1024 * 1024)
|
|
).toFixed(1);
|
|
const etaText = progress.eta_seconds
|
|
? formatTime(progress.eta_seconds)
|
|
: i18n.t("browserDownload.toast.calculating");
|
|
|
|
const toastId = `download-${browserName.toLowerCase()}-${progress.version}`;
|
|
// During first-run onboarding the welcome dialog shows browser
|
|
// setup progress itself, so suppress the global download toast.
|
|
if (!isOnboardingActive()) {
|
|
showDownloadToast(
|
|
browserName,
|
|
progress.version,
|
|
"downloading",
|
|
{
|
|
percentage: progress.percentage,
|
|
speed: speedMBps,
|
|
eta: etaText,
|
|
},
|
|
{
|
|
onCancel: () => {
|
|
invoke("cancel_download", {
|
|
browserStr: progress.browser,
|
|
version: progress.version,
|
|
}).catch((err) => {
|
|
console.error("Failed to cancel download:", err);
|
|
});
|
|
dismissToast(toastId);
|
|
},
|
|
},
|
|
);
|
|
}
|
|
} else if (progress.stage === "extracting") {
|
|
if (!isOnboardingActive()) {
|
|
showDownloadToast(browserName, progress.version, "extracting");
|
|
}
|
|
} else if (progress.stage === "verifying") {
|
|
if (!isOnboardingActive()) {
|
|
showDownloadToast(browserName, progress.version, "verifying");
|
|
}
|
|
} else if (progress.stage === "cancelled") {
|
|
setDownloadingBrowsers((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(progress.browser);
|
|
return next;
|
|
});
|
|
dismissToast(
|
|
`download-${browserName.toLowerCase()}-${progress.version}`,
|
|
);
|
|
setDownloadProgress(null);
|
|
} else if (progress.stage === "error") {
|
|
setDownloadingBrowsers((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(progress.browser);
|
|
return next;
|
|
});
|
|
dismissToast(
|
|
`download-${browserName.toLowerCase()}-${progress.version}`,
|
|
);
|
|
setDownloadProgress(null);
|
|
// During first-run onboarding the welcome dialog surfaces a
|
|
// concrete setup error itself, so suppress the global toast.
|
|
if (!isOnboardingActive()) {
|
|
showErrorToast(
|
|
i18n.t("browserDownload.toast.extractionFailed", {
|
|
browser: browserName,
|
|
version: progress.version,
|
|
}),
|
|
{
|
|
description: i18n.t(
|
|
"browserDownload.toast.extractionFailedDescription",
|
|
),
|
|
},
|
|
);
|
|
}
|
|
} else if (progress.stage === "completed") {
|
|
setDownloadingBrowsers((prev) => {
|
|
const next = new Set(prev);
|
|
next.delete(progress.browser);
|
|
return next;
|
|
});
|
|
// On completion, refresh the downloaded versions for this browser and also refresh camoufox,
|
|
// since the Create dialog implicitly uses camoufox on the anti-detect tab
|
|
try {
|
|
await Promise.all([
|
|
loadDownloadedVersions(progress.browser),
|
|
progress.browser !== "camoufox"
|
|
? loadDownloadedVersions("camoufox")
|
|
: Promise.resolve([]),
|
|
]);
|
|
} catch {
|
|
/* empty */
|
|
}
|
|
if (!isOnboardingActive()) {
|
|
showDownloadToast(browserName, progress.version, "completed");
|
|
}
|
|
setDownloadProgress(null);
|
|
}
|
|
},
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to setup download progress listener:", error);
|
|
}
|
|
|
|
try {
|
|
// GeoIP database download progress
|
|
unlistenGeoip = await listen<{
|
|
stage: string;
|
|
percentage: number;
|
|
message: string;
|
|
downloaded_bytes?: number;
|
|
total_bytes?: number;
|
|
speed_bytes_per_sec?: number;
|
|
eta_seconds?: number;
|
|
}>(
|
|
"geoip-download-progress",
|
|
(
|
|
event: TauriEvent<{
|
|
stage: string;
|
|
percentage: number;
|
|
message: string;
|
|
downloaded_bytes?: number;
|
|
total_bytes?: number;
|
|
speed_bytes_per_sec?: number;
|
|
eta_seconds?: number;
|
|
}>,
|
|
) => {
|
|
const { stage, percentage, speed_bytes_per_sec, eta_seconds } =
|
|
event.payload;
|
|
if (stage === "downloading") {
|
|
const speedMBps = speed_bytes_per_sec
|
|
? (speed_bytes_per_sec / (1024 * 1024)).toFixed(1)
|
|
: undefined;
|
|
const etaText = eta_seconds ? formatTime(eta_seconds) : undefined;
|
|
showToast({
|
|
id: "geoip-download",
|
|
type: "download",
|
|
title: i18n.t("browserDownload.toast.geoipDownloading"),
|
|
stage: "downloading",
|
|
progress: {
|
|
percentage,
|
|
...(speedMBps ? { speed: speedMBps } : {}),
|
|
...(etaText ? { eta: etaText } : {}),
|
|
},
|
|
});
|
|
} else if (stage === "completed") {
|
|
showToast({
|
|
id: "geoip-download",
|
|
type: "download",
|
|
title: i18n.t("browserDownload.toast.geoipDownloaded"),
|
|
stage: "completed",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
} catch (error) {
|
|
console.error("Failed to setup GeoIP progress listener:", error);
|
|
}
|
|
};
|
|
|
|
void setupListeners();
|
|
|
|
return () => {
|
|
if (unlistenBrowser) {
|
|
try {
|
|
unlistenBrowser();
|
|
} catch (error) {
|
|
console.error("Failed to cleanup browser download listener:", error);
|
|
}
|
|
}
|
|
if (unlistenGeoip) {
|
|
try {
|
|
unlistenGeoip();
|
|
} catch (error) {
|
|
console.error("Failed to cleanup GeoIP progress listener:", error);
|
|
}
|
|
}
|
|
};
|
|
}, [formatTime, loadDownloadedVersions]);
|
|
|
|
return {
|
|
availableVersions,
|
|
downloadedVersionsMap,
|
|
isDownloading,
|
|
isBrowserDownloading,
|
|
downloadingBrowsers,
|
|
downloadProgress,
|
|
loadVersions,
|
|
loadVersionsWithNewCount,
|
|
loadDownloadedVersions,
|
|
downloadBrowser,
|
|
isVersionDownloaded,
|
|
formatBytes,
|
|
formatTime,
|
|
};
|
|
}
|