Compare commits

..
Author SHA1 Message Date
zhom 3d57a622b1 chore: version bump 2026-02-18 09:41:20 +04:00
zhom 5dfe7cb216 fix: download zip instead of exe 2026-02-18 09:40:45 +04:00
zhom dea0181009 chore: version bump 2026-02-17 23:15:36 +04:00
zhom 4983f622d0 fix: properly signed the app 2026-02-17 23:15:06 +04:00
zhom 6654ab9fdc refactor: simplify auto-update login 2026-02-17 22:54:45 +04:00
9 changed files with 97 additions and 204 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser",
"private": true,
"license": "AGPL-3.0",
"version": "0.14.3",
"version": "0.14.5",
"type": "module",
"scripts": {
"dev": "next dev --turbopack -p 12341",
+1 -1
View File
@@ -1547,7 +1547,7 @@ dependencies = [
[[package]]
name = "donutbrowser"
version = "0.14.3"
version = "0.14.5"
dependencies = [
"aes-gcm",
"argon2",
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "donutbrowser"
version = "0.14.3"
version = "0.14.5"
description = "Simple Yet Powerful Anti-Detect Browser"
authors = ["zhom@github"]
edition = "2021"
+2
View File
@@ -14,6 +14,8 @@
<string>Donut</string>
<key>CFBundleIdentifier</key>
<string>com.donutbrowser</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleURLName</key>
<string>com.donutbrowser</string>
<key>CFBundleExecutable</key>
+18 -2
View File
@@ -612,7 +612,7 @@ mod windows {
if let Ok(entries) = std::fs::read_dir(install_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "exe") {
if path.extension().is_some_and(|ext| ext == "exe") && is_pe_executable(&path) {
let name = path
.file_stem()
.unwrap_or_default()
@@ -716,7 +716,7 @@ mod windows {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|ext| ext == "exe") {
if path.extension().is_some_and(|ext| ext == "exe") && is_pe_executable(&path) {
let name = path
.file_stem()
.unwrap_or_default()
@@ -1185,6 +1185,22 @@ impl BrowserFactory {
}
}
/// Check if a file is a valid PE executable by reading its magic bytes (MZ).
/// Returns false for archive files (.zip starts with PK, etc.) that were
/// incorrectly named with a .exe extension.
#[cfg(target_os = "windows")]
fn is_pe_executable(path: &Path) -> bool {
use std::io::Read;
let Ok(mut file) = std::fs::File::open(path) else {
return false;
};
let mut magic = [0u8; 2];
if file.read_exact(&mut magic).is_err() {
return false;
}
magic == [0x4D, 0x5A] // MZ
}
// Factory function to create browser instances (kept for backward compatibility)
pub fn create_browser(browser_type: BrowserType) -> Box<dyn Browser> {
BrowserFactory::instance().create_browser(browser_type)
+1 -1
View File
@@ -685,7 +685,7 @@ impl BrowserVersionManager {
"macos-arm64" | "macos-x64" => (format!("wayfern-{version}-{platform_key}.dmg"), true),
"linux-x64" | "linux-arm64" => (format!("wayfern-{version}-{platform_key}.tar.xz"), true),
"windows-x64" | "windows-arm64" => {
(format!("wayfern-{version}-{platform_key}.exe"), false)
(format!("wayfern-{version}-{platform_key}.zip"), true)
}
_ => {
return Err(
+1 -1
View File
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Donut",
"version": "0.14.3",
"version": "0.14.5",
"identifier": "com.donutbrowser",
"build": {
"beforeDevCommand": "pnpm copy-proxy-binary && pnpm dev",
+38 -174
View File
@@ -1,69 +1,24 @@
"use client";
import { FaDownload, FaExternalLinkAlt, FaTimes } from "react-icons/fa";
import { LuCheckCheck, LuCog, LuRefreshCw } from "react-icons/lu";
import { Badge } from "@/components/ui/badge";
import { FaExternalLinkAlt, FaTimes } from "react-icons/fa";
import { LuCheckCheck } from "react-icons/lu";
import { Button } from "@/components/ui/button";
import type { AppUpdateInfo, AppUpdateProgress } from "@/types";
import type { AppUpdateInfo } from "@/types";
import { RippleButton } from "./ui/ripple";
interface AppUpdateToastProps {
updateInfo: AppUpdateInfo;
onUpdate: (updateInfo: AppUpdateInfo) => Promise<void>;
onRestart: () => Promise<void>;
onDismiss: () => void;
isUpdating?: boolean;
updateProgress?: AppUpdateProgress | null;
updateReady?: boolean;
}
function getStageIcon(stage?: string, isUpdating?: boolean) {
if (!isUpdating) {
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
}
switch (stage) {
case "downloading":
return <FaDownload className="flex-shrink-0 w-5 h-5" />;
case "extracting":
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "installing":
return <LuCog className="flex-shrink-0 w-5 h-5 animate-spin" />;
case "completed":
return <LuCheckCheck className="flex-shrink-0 w-5 h-5" />;
default:
return <LuRefreshCw className="flex-shrink-0 w-5 h-5 animate-spin" />;
}
}
function getStageDisplayName(stage?: string) {
switch (stage) {
case "downloading":
return "Downloading";
case "extracting":
return "Extracting";
case "installing":
return "Installing";
case "completed":
return "Completed";
default:
return "Updating";
}
}
export function AppUpdateToast({
updateInfo,
onUpdate,
onRestart,
onDismiss,
isUpdating = false,
updateProgress,
updateReady = false,
}: AppUpdateToastProps) {
const handleUpdateClick = async () => {
await onUpdate(updateInfo);
};
const handleRestartClick = async () => {
await onRestart();
};
@@ -77,115 +32,37 @@ export function AppUpdateToast({
}
};
const showDownloadProgress =
isUpdating &&
updateProgress?.stage === "downloading" &&
updateProgress.percentage !== undefined;
const showOtherStageProgress =
isUpdating &&
updateProgress &&
(updateProgress.stage === "extracting" ||
updateProgress.stage === "installing" ||
updateProgress.stage === "completed");
return (
<div className="flex items-start p-4 w-full max-w-md rounded-lg border shadow-lg bg-card border-border text-card-foreground">
<div className="mr-3 mt-0.5">
{updateReady ? (
<LuCheckCheck className="flex-shrink-0 w-5 h-5 text-green-500" />
) : (
getStageIcon(updateProgress?.stage, isUpdating)
)}
<LuCheckCheck className="flex-shrink-0 w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<div className="flex gap-2 justify-between items-start">
<div className="flex flex-col gap-1">
<div className="flex gap-2 items-center">
<span className="text-sm font-semibold text-foreground">
{updateReady
? "The update is ready, restart app"
: isUpdating
? `${getStageDisplayName(updateProgress?.stage)} Donut Browser Update`
: "Donut Browser Update Available"}
</span>
{!updateReady && (
<Badge
variant={updateInfo.is_nightly ? "secondary" : "default"}
className="text-xs"
>
{updateInfo.is_nightly ? "Nightly" : "Stable"}
</Badge>
)}
<span className="text-sm font-semibold text-foreground">
{updateReady
? "Update ready, restart to apply"
: "Manual download required"}
</span>
<div className="text-xs text-muted-foreground">
{updateInfo.current_version} {updateInfo.new_version}
</div>
{!updateReady && (
<div className="text-xs text-muted-foreground">
{isUpdating ? (
updateProgress?.message || "Updating..."
) : (
<>
Update from {updateInfo.current_version} to{" "}
<span className="font-medium">
{updateInfo.new_version}
</span>
{updateInfo.manual_update_required && (
<span className="block mt-1 text-muted-foreground/80">
Manual download required on Linux
</span>
)}
</>
)}
</div>
)}
</div>
{!isUpdating && !updateReady && (
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="w-3 h-3" />
</Button>
)}
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="p-0 w-6 h-6 shrink-0"
>
<FaTimes className="w-3 h-3" />
</Button>
</div>
{!updateReady && showDownloadProgress && updateProgress && (
<div className="mt-2 space-y-1">
<div className="flex justify-between items-center">
<p className="flex-1 min-w-0 text-xs text-muted-foreground">
{updateProgress.percentage?.toFixed(1)}%
{updateProgress.speed && `${updateProgress.speed} MB/s`}
{updateProgress.eta && `${updateProgress.eta} remaining`}
</p>
</div>
<div className="w-full bg-muted rounded-full h-1.5">
<div
className="bg-primary h-1.5 rounded-full transition-all duration-300"
style={{ width: `${updateProgress.percentage}%` }}
/>
</div>
</div>
)}
{!updateReady && showOtherStageProgress && (
<div className="mt-2 space-y-1">
<div className="w-full bg-muted rounded-full h-1.5">
<div
className={`h-1.5 rounded-full transition-all duration-500 ${
updateProgress.stage === "completed"
? "bg-green-500 w-full"
: "bg-primary w-full animate-pulse"
}`}
/>
</div>
</div>
)}
{updateReady ? (
<div className="flex gap-2 items-center mt-3">
<div className="flex gap-2 items-center mt-3">
{updateReady ? (
<RippleButton
onClick={() => void handleRestartClick()}
size="sm"
@@ -194,40 +71,27 @@ export function AppUpdateToast({
<LuCheckCheck className="w-3 h-3" />
Restart Now
</RippleButton>
</div>
) : (
!isUpdating && (
<div className="flex gap-2 items-center mt-3">
{updateInfo.manual_update_required ? (
<RippleButton
onClick={handleViewRelease}
size="sm"
className="flex gap-2 items-center text-xs"
>
<FaExternalLinkAlt className="w-3 h-3" />
View Release
</RippleButton>
) : (
<RippleButton
onClick={() => void handleUpdateClick()}
size="sm"
className="flex gap-2 items-center text-xs"
>
<FaDownload className="w-3 h-3" />
Download Update
</RippleButton>
)}
) : (
updateInfo.manual_update_required && (
<RippleButton
variant="outline"
onClick={onDismiss}
onClick={handleViewRelease}
size="sm"
className="text-xs"
className="flex gap-2 items-center text-xs"
>
Later
<FaExternalLinkAlt className="w-3 h-3" />
View Release
</RippleButton>
</div>
)
)}
)
)}
<RippleButton
variant="outline"
onClick={onDismiss}
size="sm"
className="text-xs"
>
Later
</RippleButton>
</div>
</div>
</div>
);
+34 -23
View File
@@ -2,7 +2,7 @@
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { toast } from "sonner";
import { AppUpdateToast } from "@/components/app-update-toast";
import { showToast } from "@/lib/toast-utils";
@@ -16,6 +16,7 @@ export function useAppUpdateNotifications() {
const [updateReady, setUpdateReady] = useState(false);
const [isClient, setIsClient] = useState(false);
const [dismissedVersion, setDismissedVersion] = useState<string | null>(null);
const autoDownloadedVersion = useRef<string | null>(null);
// Ensure we're on the client side to prevent hydration mismatches
useEffect(() => {
@@ -52,6 +53,7 @@ export function useAppUpdateNotifications() {
console.log("Manual check result:", update);
// Always show manual check results, even if previously dismissed
autoDownloadedVersion.current = null;
setUpdateInfo(update);
} catch (error) {
console.error("Failed to manually check for app updates:", error);
@@ -112,7 +114,7 @@ export function useAppUpdateNotifications() {
toast.dismiss("app-update");
}, [isClient, updateInfo]);
// Listen for app update availability
// Listen for app update events
useEffect(() => {
if (!isClient) return;
@@ -127,16 +129,7 @@ export function useAppUpdateNotifications() {
const unlistenProgress = listen<AppUpdateProgress>(
"app-update-progress",
(event) => {
console.log("App update progress:", event.payload);
setUpdateProgress(event.payload);
// If update is completed, mark as no longer updating after a delay
if (event.payload.stage === "completed") {
setTimeout(() => {
setIsUpdating(false);
setUpdateProgress(null);
}, 5000); // Show completion message for 5 seconds instead of 2
}
},
);
@@ -160,41 +153,59 @@ export function useAppUpdateNotifications() {
};
}, [isClient]);
// Show toast when update is available
// Auto-download update in background when found
useEffect(() => {
if (!isClient || !updateInfo) return;
if (
!isClient ||
!updateInfo ||
updateInfo.manual_update_required ||
isUpdating ||
updateReady ||
autoDownloadedVersion.current === updateInfo.new_version
)
return;
autoDownloadedVersion.current = updateInfo.new_version;
console.log("Auto-downloading app update:", updateInfo.new_version);
void handleAppUpdate(updateInfo);
}, [isClient, updateInfo, isUpdating, updateReady, handleAppUpdate]);
// Show toast only when update is ready to install or requires manual action
useEffect(() => {
if (!isClient) return;
const showManualToast = updateInfo?.manual_update_required && !isUpdating;
if (!updateReady && !showManualToast) {
return;
}
if (!updateInfo) return;
toast.custom(
() => (
<AppUpdateToast
updateInfo={updateInfo}
onUpdate={handleAppUpdate}
onRestart={handleRestart}
onDismiss={dismissAppUpdate}
isUpdating={isUpdating}
updateProgress={updateProgress}
updateReady={updateReady}
/>
),
{
id: "app-update",
duration: Number.POSITIVE_INFINITY, // Persistent until user action
duration: Number.POSITIVE_INFINITY,
position: "top-left",
style: {
zIndex: 99999, // Ensure app updates appear above dialogs
pointerEvents: "auto", // Ensure app updates remain interactive
marginTop: "16px", // slightly lower on macOS-like top controls
zIndex: 99999,
pointerEvents: "auto",
marginTop: "16px",
},
},
);
}, [
updateInfo,
handleAppUpdate,
handleRestart,
dismissAppUpdate,
isUpdating,
updateProgress,
updateReady,
isUpdating,
isClient,
]);