Compare commits

..
Author SHA1 Message Date
zhom b0ca14c184 chore: version bump 2025-06-07 06:14:34 +04:00
zhom eea94ad360 chore: linter 2025-06-07 06:12:44 +04:00
zhom 2b678ed04d fix: hide brave releases without zip files for linux 2025-06-07 06:00:55 +04:00
zhom dff201ddec chore: add cursor rule 2025-06-07 05:46:32 +04:00
zhom 743ad59348 fix: consider all brave releases nightly if there is no release name 2025-06-07 05:42:57 +04:00
zhom d43e9ef21b chore: ui copy 2025-06-07 05:30:33 +04:00
zhom 7515cbacd6 refactor: check for similar-named brave binaries 2025-06-07 05:25:57 +04:00
zhom f41172e822 fix: update brave url for linux arm 2025-06-07 05:12:04 +04:00
zhom 25ce691bbc fix: use correct file extension on linux 2025-06-07 05:02:24 +04:00
zhom b945ee7088 chore: add cursor rule 2025-06-07 04:39:57 +04:00
15 changed files with 58 additions and 78 deletions
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Don't leave comments that don't add value
+6
View File
@@ -0,0 +1,6 @@
---
description:
globs:
alwaysApply: true
---
Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
+1
View File
@@ -2,3 +2,4 @@
- If you want to run tests, only ever run them as "pnpm format && pnpm lint && pnpm test". - If you want to run tests, only ever run them as "pnpm format && pnpm lint && pnpm test".
- Don't leave comments that don't add value - Don't leave comments that don't add value
- Do not duplicate code unless you have a very good reason to do so. It is important that the same logic is not duplicated multiple times
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "donutbrowser", "name": "donutbrowser",
"private": true, "private": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"version": "0.3.1", "version": "0.3.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "next dev --turbopack", "dev": "next dev --turbopack",
+1 -1
View File
@@ -993,7 +993,7 @@ dependencies = [
[[package]] [[package]]
name = "donutbrowser" name = "donutbrowser"
version = "0.3.1" version = "0.3.2"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"base64 0.22.1", "base64 0.22.1",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "donutbrowser" name = "donutbrowser"
version = "0.3.1" version = "0.3.2"
description = "Simple Yet Powerful Browser Orchestrator" description = "Simple Yet Powerful Browser Orchestrator"
authors = ["zhom@github"] authors = ["zhom@github"]
edition = "2021" edition = "2021"
+1 -1
View File
@@ -13,7 +13,7 @@
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1</string> <string>1</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>0.3.1</string> <string>0.3.2</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleIconFile</key> <key>CFBundleIconFile</key>
+8 -14
View File
@@ -246,8 +246,7 @@ pub fn is_browser_version_nightly(
if let Some(name) = release_name { if let Some(name) = release_name {
!name.starts_with("Release") !name.starts_with("Release")
} else { } else {
// Fallback to version string analysis if no release name true
is_nightly_version(version)
} }
} }
"firefox" | "firefox-developer" => { "firefox" | "firefox-developer" => {
@@ -806,22 +805,17 @@ impl ApiClient {
}) || assets.iter().any(|asset| asset.name.ends_with(".dmg")) }) || assets.iter().any(|asset| asset.name.ends_with(".dmg"))
} }
"linux" => { "linux" => {
// For Linux, check for architecture-specific packages (prefer ZIP for stable releases) // For Linux, be strict about architecture matching - only allow assets that explicitly match the current architecture
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" }; let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
assets.iter().any(|asset| { if assets.iter().any(|asset| {
let name = asset.name.to_lowercase(); let name = asset.name.to_lowercase();
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip") name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
}) || assets.iter().any(|asset| { }) {
let name = asset.name.to_lowercase(); return true;
name.contains(arch_pattern) && (name.ends_with(".deb") || name.ends_with(".rpm")) }
}) || assets.iter().any(|asset| {
let name = asset.name.to_lowercase(); false
name.contains("linux") && name.ends_with(".zip")
}) || assets.iter().any(|asset| {
let name = asset.name.to_lowercase();
name.ends_with(".deb") || name.ends_with(".rpm")
})
} }
_ => false, _ => false,
} }
+2
View File
@@ -307,6 +307,8 @@ mod linux {
BrowserType::Brave => vec![ BrowserType::Brave => vec![
browser_subdir.join("brave"), browser_subdir.join("brave"),
browser_subdir.join("brave-browser"), browser_subdir.join("brave-browser"),
browser_subdir.join("brave-browser-nightly"),
browser_subdir.join("brave-browser-beta"),
], ],
_ => vec![], _ => vec![],
}; };
+7 -9
View File
@@ -442,8 +442,8 @@ impl BrowserVersionService {
format!("Firefox Setup {version}.exe"), format!("Firefox Setup {version}.exe"),
false, false,
), ),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.bz2"), true), ("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.bz2"), true), ("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true), ("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => { _ => {
return Err( return Err(
@@ -468,8 +468,8 @@ impl BrowserVersionService {
format!("Firefox Setup {version}.exe"), format!("Firefox Setup {version}.exe"),
false, false,
), ),
("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.bz2"), true), ("linux", "x64") => ("linux-x86_64", format!("firefox-{version}.tar.xz"), true),
("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.bz2"), true), ("linux", "arm64") => ("linux-aarch64", format!("firefox-{version}.tar.xz"), true),
("macos", _) => ("mac", format!("Firefox {version}.dmg"), true), ("macos", _) => ("mac", format!("Firefox {version}.dmg"), true),
_ => { _ => {
return Err( return Err(
@@ -568,8 +568,6 @@ impl BrowserVersionService {
}) })
} }
"brave" => { "brave" => {
// Brave uses different asset naming conventions
// The actual URL will be resolved dynamically in the download service
let (filename, is_archive) = match (&os[..], &arch[..]) { let (filename, is_archive) = match (&os[..], &arch[..]) {
("windows", _) => (format!("brave-{version}.exe"), false), ("windows", _) => (format!("brave-{version}.exe"), false),
("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true), ("linux", "x64") => (format!("brave-browser-{version}-linux-amd64.zip"), true),
@@ -582,7 +580,7 @@ impl BrowserVersionService {
Ok(DownloadInfo { Ok(DownloadInfo {
url: format!( url: format!(
"https://github.com/brave/brave-browser/releases/download/{version}/brave-placeholder" "https://github.com/brave/brave-browser/releases/download/{version}/{filename}"
), ),
filename, filename,
is_archive, is_archive,
@@ -1521,10 +1519,10 @@ mod tests {
assert!(chromium_info.url.contains("chrome-mac.zip")); assert!(chromium_info.url.contains("chrome-mac.zip"));
assert!(chromium_info.is_archive); assert!(chromium_info.is_archive);
// Test Brave // Test Brave - Note: Brave uses dynamic URL resolution, so get_download_info provides a template URL
let brave_info = service.get_download_info("brave", "v1.81.9").unwrap(); let brave_info = service.get_download_info("brave", "v1.81.9").unwrap();
assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg"); assert_eq!(brave_info.filename, "Brave-Browser-universal.dmg");
assert!(brave_info.url.contains("brave-placeholder")); assert_eq!(brave_info.url, "https://github.com/brave/brave-browser/releases/download/v1.81.9/Brave-Browser-universal.dmg");
assert!(brave_info.is_archive); assert!(brave_info.is_archive);
// Test unsupported browser // Test unsupported browser
+5 -32
View File
@@ -195,40 +195,13 @@ impl Downloader {
}) })
} }
"linux" => { "linux" => {
// For Linux, prefer ZIP files matching architecture (new format for stable releases) // For Linux, be strict about architecture matching - same logic as has_compatible_brave_asset
let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" }; let arch_pattern = if arch == "arm64" { "arm64" } else { "amd64" };
assets assets.iter().find(|asset| {
.iter() let name = asset.name.to_lowercase();
.find(|asset| { name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
let name = asset.name.to_lowercase(); })
name.contains("linux") && name.contains(arch_pattern) && name.ends_with(".zip")
})
.or_else(|| {
// Fallback to DEB packages
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains(arch_pattern) && name.ends_with(".deb")
})
})
.or_else(|| {
// Fallback to any ZIP
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("linux") && name.ends_with(".zip")
})
})
.or_else(|| {
// Fallback to any DEB
assets.iter().find(|asset| asset.name.ends_with(".deb"))
})
.or_else(|| {
// Last fallback to RPM if no ZIP or DEB found
assets.iter().find(|asset| {
let name = asset.name.to_lowercase();
name.contains("x86_64") && name.ends_with(".rpm")
})
})
} }
_ => None, _ => None,
}; };
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"$schema": "https://schema.tauri.app/config/2", "$schema": "https://schema.tauri.app/config/2",
"productName": "Donut Browser", "productName": "Donut Browser",
"version": "0.3.1", "version": "0.3.2",
"identifier": "com.donutbrowser", "identifier": "com.donutbrowser",
"build": { "build": {
"beforeDevCommand": "pnpm dev", "beforeDevCommand": "pnpm dev",
+1 -1
View File
@@ -293,7 +293,7 @@ export function CreateProfileDialog({
disabled={true} disabled={true}
className="opacity-50" className="opacity-50"
> >
{displayName} (Not supported on this platform) {displayName} (Not supported)
</SelectItem> </SelectItem>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
+15 -15
View File
@@ -116,31 +116,31 @@ type ToastProps =
function getToastIcon(type: ToastProps["type"], stage?: string) { function getToastIcon(type: ToastProps["type"], stage?: string) {
switch (type) { switch (type) {
case "success": case "success":
return <LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" />; return <LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />;
case "error": case "error":
return <LuTriangleAlert className="h-4 w-4 text-red-500 flex-shrink-0" />; return <LuTriangleAlert className="flex-shrink-0 w-4 h-4 text-red-500" />;
case "download": case "download":
if (stage === "completed") { if (stage === "completed") {
return ( return (
<LuCheckCheck className="h-4 w-4 text-green-500 flex-shrink-0" /> <LuCheckCheck className="flex-shrink-0 w-4 h-4 text-green-500" />
); );
} }
return <LuDownload className="h-4 w-4 text-blue-500 flex-shrink-0" />; return <LuDownload className="flex-shrink-0 w-4 h-4 text-blue-500" />;
case "version-update": case "version-update":
return ( return (
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" /> <LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
); );
case "fetching": case "fetching":
return ( return (
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" /> <LuRefreshCw className="flex-shrink-0 w-4 h-4 text-blue-500 animate-spin" />
); );
case "twilight-update": case "twilight-update":
return ( return (
<LuRefreshCw className="h-4 w-4 text-purple-500 animate-spin flex-shrink-0" /> <LuRefreshCw className="flex-shrink-0 w-4 h-4 text-purple-500 animate-spin" />
); );
default: default:
return ( return (
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" /> <div className="flex-shrink-0 w-4 h-4 rounded-full border-2 border-blue-500 animate-spin border-t-transparent" />
); );
} }
} }
@@ -151,10 +151,10 @@ export function UnifiedToast(props: ToastProps) {
const progress = "progress" in props ? props.progress : undefined; const progress = "progress" in props ? props.progress : undefined;
return ( return (
<div className="flex items-start w-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-3 shadow-lg"> <div className="flex items-start p-3 w-full bg-white rounded-lg border border-gray-200 shadow-lg dark:bg-gray-800 dark:border-gray-700">
<div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div> <div className="mr-3 mt-0.5">{getToastIcon(type, stage)}</div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 dark:text-white leading-tight"> <p className="text-sm font-medium leading-tight text-gray-900 dark:text-white">
{title} {title}
</p> </p>
@@ -165,7 +165,7 @@ export function UnifiedToast(props: ToastProps) {
stage === "downloading" && ( stage === "downloading" && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<p className="text-xs text-gray-600 dark:text-gray-300 min-w-0 flex-1"> <p className="flex-1 min-w-0 text-xs text-gray-600 dark:text-gray-300">
{progress.percentage.toFixed(1)}% {progress.percentage.toFixed(1)}%
{progress.speed && `${progress.speed} MB/s`} {progress.speed && `${progress.speed} MB/s`}
{progress.eta && `${progress.eta} remaining`} {progress.eta && `${progress.eta} remaining`}
@@ -195,7 +195,7 @@ export function UnifiedToast(props: ToastProps) {
}} }}
/> />
</div> </div>
<span className="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap shrink-0 w-8 text-right"> <span className="w-8 text-xs text-right text-gray-500 whitespace-nowrap dark:text-gray-400 shrink-0">
{progress.current}/{progress.total} {progress.current}/{progress.total}
</span> </span>
</div> </div>
@@ -211,7 +211,7 @@ export function UnifiedToast(props: ToastProps) {
: "Checking for twilight updates..."} : "Checking for twilight updates..."}
</p> </p>
{props.browserName && ( {props.browserName && (
<p className="text-xs text-purple-600 dark:text-purple-400 mt-1"> <p className="mt-1 text-xs text-purple-600 dark:text-purple-400">
{props.browserName} Rolling Release {props.browserName} Rolling Release
</p> </p>
)} )}
@@ -220,7 +220,7 @@ export function UnifiedToast(props: ToastProps) {
{/* Description */} {/* Description */}
{description && ( {description && (
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300 leading-tight"> <p className="mt-1 text-xs leading-tight text-gray-600 dark:text-gray-300">
{description} {description}
</p> </p>
)} )}
@@ -235,7 +235,7 @@ export function UnifiedToast(props: ToastProps) {
)} )}
{stage === "verifying" && ( {stage === "verifying" && (
<p className="mt-1 text-xs text-gray-600 dark:text-gray-300"> <p className="mt-1 text-xs text-gray-600 dark:text-gray-300">
Verifying installation... Verifying browser files...
</p> </p>
)} )}
{stage === "downloading (twilight rolling release)" && ( {stage === "downloading (twilight rolling release)" && (
+2 -2
View File
@@ -458,7 +458,7 @@ export function ImportProfileDialog({
isLoading isLoading
} }
> >
Import Detected Profile Import Profile
</LoadingButton> </LoadingButton>
) : ( ) : (
<LoadingButton <LoadingButton
@@ -472,7 +472,7 @@ export function ImportProfileDialog({
!manualProfileName.trim() !manualProfileName.trim()
} }
> >
Import Manual Profile Import Profile
</LoadingButton> </LoadingButton>
)} )}
</DialogFooter> </DialogFooter>