mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-04-29 07:16:11 +02:00
chore: linting for both js and rs
This commit is contained in:
@@ -10,7 +10,28 @@ env:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
jobs:
|
||||
# Wait for linting jobs to complete first
|
||||
wait-for-linting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Wait for Rust linting to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: "Lint Rust"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
- name: Wait for JavaScript linting to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: "Lint Node.js"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
release:
|
||||
needs: wait-for-linting
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
|
||||
@@ -10,7 +10,28 @@ env:
|
||||
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||
|
||||
jobs:
|
||||
# Wait for linting jobs to complete first
|
||||
wait-for-linting:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Wait for Rust linting to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: "Lint Rust"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
- name: Wait for JavaScript linting to complete
|
||||
uses: lewagon/wait-on-check-action@v1.3.4
|
||||
with:
|
||||
ref: ${{ github.ref }}
|
||||
check-name: "Lint Node.js"
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
wait-interval: 10
|
||||
|
||||
rolling-release:
|
||||
needs: wait-for-linting
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
|
||||
Vendored
+4
-1
@@ -9,11 +9,14 @@
|
||||
"ntlm",
|
||||
"propertylist",
|
||||
"rlib",
|
||||
"rustc",
|
||||
"serde",
|
||||
"shadcn",
|
||||
"signon",
|
||||
"sspi",
|
||||
"staticlib",
|
||||
"sysinfo",
|
||||
"systempreferences"
|
||||
"systempreferences",
|
||||
"turbopack"
|
||||
]
|
||||
}
|
||||
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": false
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": []
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"correctness": {
|
||||
"useHookAtTopLevel": "error"
|
||||
},
|
||||
"nursery": {
|
||||
"useGoogleFontDisplay": "error",
|
||||
"noDocumentImportInPage": "error",
|
||||
"noHeadElement": "error",
|
||||
"noHeadImportInDocument": "error",
|
||||
"noImgElement": "off",
|
||||
"useComponentExportOnlyModules": {
|
||||
"level": "error",
|
||||
"options": {
|
||||
"allowExportNames": ["metadata", "badgeVariants", "buttonVariants"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"a11y": {
|
||||
"useSemanticElements": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
},
|
||||
"globals": []
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -7,13 +7,14 @@
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "prettier --check src/ && tsc --noEmit && next lint",
|
||||
"lint": "biome ci src/ && tsc --noEmit && next lint",
|
||||
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all",
|
||||
"tauri": "tauri",
|
||||
"shadcn:add": "pnpm dlx shadcn@latest add",
|
||||
"prepare": "husky",
|
||||
"format:js": "prettier --write src/",
|
||||
"format:js": "biome check src/ --fix && prettier --write src/",
|
||||
"format:rust": "cd src-tauri && cargo fmt --all",
|
||||
"format:biome": "biome check src/ --fix",
|
||||
"format": "pnpm format:js && pnpm format:rust",
|
||||
"prettier": "prettier --write"
|
||||
},
|
||||
@@ -44,6 +45,7 @@
|
||||
"tailwind-merge": "^3.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "1.9.4",
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@eslint/js": "^9.27.0",
|
||||
"@next/eslint-plugin-next": "^15.3.2",
|
||||
@@ -69,6 +71,7 @@
|
||||
"packageManager": "pnpm@10.11.0+sha512.6540583f41cc5f628eb3d9773ecee802f4f9ef9923cc45b69890fb47991d4b092964694ec3a4f738a420c918a333062c8b925d312f42e4f0c263eb603551f977",
|
||||
"lint-staged": {
|
||||
"src/**/*.{js,jsx,ts,tsx,json,css,md}": [
|
||||
"biome check --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"src-tauri/**/*.rs": [
|
||||
|
||||
Generated
+91
@@ -81,6 +81,9 @@ importers:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
devDependencies:
|
||||
'@biomejs/biome':
|
||||
specifier: 1.9.4
|
||||
version: 1.9.4
|
||||
'@eslint/eslintrc':
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
@@ -234,6 +237,59 @@ packages:
|
||||
resolution: {integrity: sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==}
|
||||
engines: {node: '>=6.9.0'}
|
||||
|
||||
'@biomejs/biome@1.9.4':
|
||||
resolution: {integrity: sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
hasBin: true
|
||||
|
||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-darwin-x64@1.9.4':
|
||||
resolution: {integrity: sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||
resolution: {integrity: sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||
resolution: {integrity: sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-linux-x64@1.9.4':
|
||||
resolution: {integrity: sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.9.4':
|
||||
resolution: {integrity: sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@biomejs/cli-win32-x64@1.9.4':
|
||||
resolution: {integrity: sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA==}
|
||||
engines: {node: '>=14.21.3'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@emnapi/core@1.4.3':
|
||||
resolution: {integrity: sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g==}
|
||||
|
||||
@@ -3229,6 +3285,41 @@ snapshots:
|
||||
'@babel/helper-string-parser': 7.27.1
|
||||
'@babel/helper-validator-identifier': 7.27.1
|
||||
|
||||
'@biomejs/biome@1.9.4':
|
||||
optionalDependencies:
|
||||
'@biomejs/cli-darwin-arm64': 1.9.4
|
||||
'@biomejs/cli-darwin-x64': 1.9.4
|
||||
'@biomejs/cli-linux-arm64': 1.9.4
|
||||
'@biomejs/cli-linux-arm64-musl': 1.9.4
|
||||
'@biomejs/cli-linux-x64': 1.9.4
|
||||
'@biomejs/cli-linux-x64-musl': 1.9.4
|
||||
'@biomejs/cli-win32-arm64': 1.9.4
|
||||
'@biomejs/cli-win32-x64': 1.9.4
|
||||
|
||||
'@biomejs/cli-darwin-arm64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-darwin-x64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64-musl@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-arm64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64-musl@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-linux-x64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-arm64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@biomejs/cli-win32-x64@1.9.4':
|
||||
optional: true
|
||||
|
||||
'@emnapi/core@1.4.3':
|
||||
dependencies:
|
||||
'@emnapi/wasi-threads': 1.0.2
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@biomejs/biome'
|
||||
- '@tailwindcss/oxide'
|
||||
- esbuild
|
||||
- sharp
|
||||
|
||||
+15
-16
@@ -54,7 +54,7 @@ impl VersionComponent {
|
||||
.filter_map(|part| part.parse().ok())
|
||||
.collect();
|
||||
|
||||
let major = parts.get(0).copied().unwrap_or(0);
|
||||
let major = parts.first().copied().unwrap_or(0);
|
||||
let minor = parts.get(1).copied().unwrap_or(0);
|
||||
let patch = parts.get(2).copied().unwrap_or(0);
|
||||
|
||||
@@ -103,31 +103,31 @@ impl VersionComponent {
|
||||
}
|
||||
|
||||
// Extract kind and number
|
||||
let (kind, number) = if pre_release.starts_with("alpha") {
|
||||
let (kind, number) = if let Some(stripped) = pre_release.strip_prefix("alpha") {
|
||||
(
|
||||
PreReleaseKind::Alpha,
|
||||
Self::extract_number(&pre_release[5..]),
|
||||
Self::extract_number(stripped),
|
||||
)
|
||||
} else if pre_release.starts_with("beta") {
|
||||
} else if let Some(stripped) = pre_release.strip_prefix("beta") {
|
||||
(
|
||||
PreReleaseKind::Beta,
|
||||
Self::extract_number(&pre_release[4..]),
|
||||
Self::extract_number(stripped),
|
||||
)
|
||||
} else if pre_release.starts_with("rc") {
|
||||
(PreReleaseKind::RC, Self::extract_number(&pre_release[2..]))
|
||||
} else if pre_release.starts_with("dev") {
|
||||
(PreReleaseKind::Dev, Self::extract_number(&pre_release[3..]))
|
||||
} else if pre_release.starts_with("pre") {
|
||||
(PreReleaseKind::Pre, Self::extract_number(&pre_release[3..]))
|
||||
} else if pre_release.starts_with('a') {
|
||||
} else if let Some(stripped) = pre_release.strip_prefix("rc") {
|
||||
(PreReleaseKind::RC, Self::extract_number(stripped))
|
||||
} else if let Some(stripped) = pre_release.strip_prefix("dev") {
|
||||
(PreReleaseKind::Dev, Self::extract_number(stripped))
|
||||
} else if let Some(stripped) = pre_release.strip_prefix("pre") {
|
||||
(PreReleaseKind::Pre, Self::extract_number(stripped))
|
||||
} else if let Some(stripped) = pre_release.strip_prefix('a') {
|
||||
(
|
||||
PreReleaseKind::Alpha,
|
||||
Self::extract_number(&pre_release[1..]),
|
||||
Self::extract_number(stripped),
|
||||
)
|
||||
} else if pre_release.starts_with('b') {
|
||||
} else if let Some(stripped) = pre_release.strip_prefix('b') {
|
||||
(
|
||||
PreReleaseKind::Beta,
|
||||
Self::extract_number(&pre_release[1..]),
|
||||
Self::extract_number(stripped),
|
||||
)
|
||||
} else {
|
||||
return None;
|
||||
@@ -903,7 +903,6 @@ impl ApiClient {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio;
|
||||
|
||||
#[test]
|
||||
fn test_version_parsing() {
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct UpdateNotification {
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, Default)]
|
||||
pub struct AutoUpdateState {
|
||||
pub pending_updates: Vec<UpdateNotification>,
|
||||
pub disabled_browsers: HashSet<String>, // browsers disabled during update
|
||||
@@ -26,17 +26,6 @@ pub struct AutoUpdateState {
|
||||
pub last_check_timestamp: u64,
|
||||
}
|
||||
|
||||
impl Default for AutoUpdateState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
pending_updates: Vec::new(),
|
||||
disabled_browsers: HashSet::new(),
|
||||
auto_update_downloads: HashSet::new(),
|
||||
last_check_timestamp: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AutoUpdater {
|
||||
version_service: BrowserVersionService,
|
||||
browser_runner: BrowserRunner,
|
||||
@@ -77,7 +66,7 @@ impl AutoUpdater {
|
||||
for profile in profiles {
|
||||
browser_profiles
|
||||
.entry(profile.browser.clone())
|
||||
.or_insert_with(Vec::new)
|
||||
.or_default()
|
||||
.push(profile);
|
||||
}
|
||||
|
||||
@@ -132,7 +121,7 @@ impl AutoUpdater {
|
||||
// Only consider versions newer than current
|
||||
self.is_version_newer(&v.version, current_version) &&
|
||||
// Respect version type preference
|
||||
(is_current_stable == !v.is_prerelease || !is_current_stable)
|
||||
is_current_stable != v.is_prerelease
|
||||
})
|
||||
.max_by(|a, b| self.compare_versions(&a.version, &b.version));
|
||||
|
||||
@@ -843,7 +832,6 @@ mod tests {
|
||||
std::fs::write(&state_file, json).unwrap();
|
||||
|
||||
// Dismiss notification (remove from pending updates)
|
||||
let mut state = state;
|
||||
state
|
||||
.pending_updates
|
||||
.retain(|n| n.id != "test_notification");
|
||||
|
||||
+18
-23
@@ -80,7 +80,7 @@ impl Browser for FirefoxBrowser {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("Browser app not found")?;
|
||||
|
||||
// Construct the browser executable path
|
||||
@@ -148,13 +148,11 @@ impl Browser for FirefoxBrowser {
|
||||
if browser_dir.exists() {
|
||||
println!("Directory exists, checking for .app files...");
|
||||
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().map_or(false, |ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
return true;
|
||||
}
|
||||
for entry in entries.flatten() {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -186,7 +184,7 @@ impl Browser for ChromiumBrowser {
|
||||
// Find the .app directory
|
||||
let app_path = std::fs::read_dir(install_dir)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("Browser app not found")?;
|
||||
|
||||
// Construct the browser executable path
|
||||
@@ -226,7 +224,6 @@ impl Browser for ChromiumBrowser {
|
||||
// Add proxy configuration if provided
|
||||
if let Some(proxy) = proxy_settings {
|
||||
if proxy.enabled {
|
||||
// Read PAC file and encode it as base64
|
||||
let pac_path = Path::new(profile_path).join("proxy.pac");
|
||||
if pac_path.exists() {
|
||||
let pac_content = fs::read(&pac_path)?;
|
||||
@@ -260,18 +257,16 @@ impl Browser for ChromiumBrowser {
|
||||
if browser_dir.exists() {
|
||||
println!("Directory exists, checking for .app files...");
|
||||
if let Ok(entries) = std::fs::read_dir(&browser_dir) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().map_or(false, |ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
// Try to get the executable path as a final verification
|
||||
if self.get_executable_path(&browser_dir).is_ok() {
|
||||
println!(" Executable path verification successful");
|
||||
return true;
|
||||
} else {
|
||||
println!(" Executable path verification failed");
|
||||
}
|
||||
for entry in entries.flatten() {
|
||||
println!(" Found entry: {:?}", entry.path());
|
||||
if entry.path().extension().is_some_and(|ext| ext == "app") {
|
||||
println!(" Found .app file: {:?}", entry.path());
|
||||
// Try to get the executable path as a final verification
|
||||
if self.get_executable_path(&browser_dir).is_ok() {
|
||||
println!(" Executable path verification successful");
|
||||
return true;
|
||||
} else {
|
||||
println!(" Executable path verification failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -535,7 +530,7 @@ mod tests {
|
||||
let chromium_dir = binaries_dir.join("chromium").join("1465660");
|
||||
fs::create_dir_all(&chromium_dir).unwrap();
|
||||
let chromium_app_dir = chromium_dir.join("Chromium.app");
|
||||
fs::create_dir_all(&chromium_app_dir.join("Contents").join("MacOS")).unwrap();
|
||||
fs::create_dir_all(chromium_app_dir.join("Contents").join("MacOS")).unwrap();
|
||||
|
||||
// Create a mock executable
|
||||
let executable_path = chromium_app_dir
|
||||
|
||||
@@ -92,8 +92,8 @@ impl BrowserRunner {
|
||||
|
||||
// Check if this is the correct browser type
|
||||
let is_correct_browser = match profile.browser.as_str() {
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"),
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
|
||||
_ => return false,
|
||||
};
|
||||
|
||||
@@ -108,12 +108,12 @@ impl BrowserRunner {
|
||||
// Check profile path match
|
||||
let profile_path_match = cmd.iter().any(|s| {
|
||||
let arg = s.to_str().unwrap_or("");
|
||||
arg == &profile.profile_path
|
||||
arg == profile.profile_path
|
||||
|| arg == format!("-profile={}", profile.profile_path)
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path))
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile.profile_path))
|
||||
});
|
||||
|
||||
if !profile_path_match {
|
||||
@@ -128,10 +128,10 @@ impl BrowserRunner {
|
||||
"PID {} validated successfully for {} profile {}",
|
||||
pid, profile.browser, profile.name
|
||||
);
|
||||
return true;
|
||||
true
|
||||
} else {
|
||||
println!("PID {} does not exist", pid);
|
||||
return false;
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn get_binaries_dir(&self) -> PathBuf {
|
||||
@@ -436,7 +436,7 @@ impl BrowserRunner {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map_or(false, |ext| ext == "json") {
|
||||
if path.extension().is_some_and(|ext| ext == "json") {
|
||||
let content = fs::read_to_string(path)?;
|
||||
let profile: BrowserProfile = serde_json::from_str(&content)?;
|
||||
profiles.push(profile);
|
||||
@@ -505,9 +505,9 @@ impl BrowserRunner {
|
||||
let exe_name = process.name().to_string_lossy().to_lowercase();
|
||||
let is_correct_browser = match profile.browser.as_str() {
|
||||
"mullvad-browser" => {
|
||||
self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser")
|
||||
self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser")
|
||||
}
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
|
||||
_ => false,
|
||||
};
|
||||
|
||||
@@ -518,12 +518,12 @@ impl BrowserRunner {
|
||||
// Check for profile path match
|
||||
let profile_path_match = cmd.iter().any(|s| {
|
||||
let arg = s.to_str().unwrap_or("");
|
||||
arg == &profile.profile_path
|
||||
arg == profile.profile_path
|
||||
|| arg == format!("-profile={}", profile.profile_path)
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path))
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile.profile_path))
|
||||
});
|
||||
|
||||
if profile_path_match {
|
||||
@@ -1200,10 +1200,10 @@ end try
|
||||
// and can't have multiple instances with the same profile
|
||||
match final_profile.browser.as_str() {
|
||||
"mullvad-browser" | "tor-browser" => {
|
||||
return Err(format!(
|
||||
Err(format!(
|
||||
"Failed to open URL in existing {} browser. Cannot launch new instance due to profile conflict: {}",
|
||||
final_profile.browser, e
|
||||
).into());
|
||||
).into())
|
||||
}
|
||||
_ => {
|
||||
println!(
|
||||
@@ -1375,16 +1375,16 @@ end try
|
||||
|| profile.browser == "mullvad-browser"
|
||||
|| profile.browser == "zen"
|
||||
{
|
||||
arg == &profile.profile_path
|
||||
arg == profile.profile_path
|
||||
|| arg == format!("-profile={}", profile.profile_path)
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path))
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile.profile_path))
|
||||
} else {
|
||||
// For Chromium-based browsers, check for user-data-dir
|
||||
arg.contains(&format!("--user-data-dir={}", profile.profile_path))
|
||||
|| arg == &profile.profile_path
|
||||
|| arg == profile.profile_path
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1421,8 +1421,8 @@ end try
|
||||
&& !exe_name.contains("mullvad")
|
||||
}
|
||||
"firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"),
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"),
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
|
||||
"zen" => exe_name.contains("zen"),
|
||||
"chromium" => exe_name.contains("chromium"),
|
||||
"brave" => exe_name.contains("brave"),
|
||||
@@ -1443,16 +1443,16 @@ end try
|
||||
|| profile.browser == "mullvad-browser"
|
||||
|| profile.browser == "zen"
|
||||
{
|
||||
arg == &profile.profile_path
|
||||
arg == profile.profile_path
|
||||
|| arg == format!("-profile={}", profile.profile_path)
|
||||
|| (arg == "-profile"
|
||||
&& cmd
|
||||
.iter()
|
||||
.any(|s2| s2.to_str().unwrap_or("") == &profile.profile_path))
|
||||
.any(|s2| s2.to_str().unwrap_or("") == profile.profile_path))
|
||||
} else {
|
||||
// For Chromium-based browsers, check for user-data-dir
|
||||
arg.contains(&format!("--user-data-dir={}", profile.profile_path))
|
||||
|| arg == &profile.profile_path
|
||||
|| arg == profile.profile_path
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1572,8 +1572,8 @@ end try
|
||||
&& !exe_name.contains("mullvad")
|
||||
}
|
||||
"firefox-developer" => exe_name.contains("firefox") && exe_name.contains("developer"),
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, &cmd, "tor-browser"),
|
||||
"mullvad-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "mullvad-browser"),
|
||||
"tor-browser" => self.is_tor_or_mullvad_browser(&exe_name, cmd, "tor-browser"),
|
||||
"zen" => exe_name.contains("zen"),
|
||||
"chromium" => exe_name.contains("chromium"),
|
||||
"brave" => exe_name.contains("brave"),
|
||||
@@ -1594,11 +1594,11 @@ end try
|
||||
|| profile.browser == "mullvad-browser"
|
||||
|| profile.browser == "zen"
|
||||
{
|
||||
arg == &profile.profile_path || arg == format!("-profile={}", profile.profile_path)
|
||||
arg == profile.profile_path || arg == format!("-profile={}", profile.profile_path)
|
||||
} else {
|
||||
// For Chromium-based browsers, check for user-data-dir
|
||||
arg.contains(&format!("--user-data-dir={}", profile.profile_path))
|
||||
|| arg == &profile.profile_path
|
||||
|| arg == profile.profile_path
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -551,7 +551,6 @@ mod tests {
|
||||
async fn test_browser_version_service_creation() {
|
||||
let _service = BrowserVersionService::new();
|
||||
// Test passes if we can create the service without panicking
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -180,8 +180,8 @@ impl Downloader {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let eta = if speed > 0.0 && total_size.is_some() {
|
||||
Some((total_size.unwrap() - downloaded) as f64 / speed)
|
||||
let eta = if speed > 0.0 {
|
||||
total_size.map(|total| (total - downloaded) as f64 / speed)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@@ -209,7 +209,6 @@ impl Downloader {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tokio;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_resolve_brave_download_url() {
|
||||
|
||||
@@ -66,7 +66,7 @@ impl DownloadedBrowsersRegistry {
|
||||
self
|
||||
.browsers
|
||||
.entry(info.browser.clone())
|
||||
.or_insert_with(HashMap::new)
|
||||
.or_default()
|
||||
.insert(info.version.clone(), info);
|
||||
}
|
||||
|
||||
|
||||
+32
-41
@@ -85,7 +85,7 @@ impl Extractor {
|
||||
// Find the .app directory in the mount point
|
||||
let app_entry = fs::read_dir(&mount_point)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("No .app found in DMG")?;
|
||||
|
||||
// Copy the .app to the destination
|
||||
@@ -179,35 +179,31 @@ impl Extractor {
|
||||
|
||||
// First, try to find any .app file in the destination directory
|
||||
if let Ok(entries) = fs::read_dir(dest_dir) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.extension().map_or(false, |ext| ext == "app") {
|
||||
app_path = Some(path);
|
||||
break;
|
||||
}
|
||||
// For Chromium, check subdirectories (chrome-mac folder)
|
||||
if path.is_dir() {
|
||||
if let Ok(sub_entries) = fs::read_dir(&path) {
|
||||
for sub_entry in sub_entries {
|
||||
if let Ok(sub_entry) = sub_entry {
|
||||
let sub_path = sub_entry.path();
|
||||
if sub_path.extension().map_or(false, |ext| ext == "app") {
|
||||
// Move the app to the root destination directory
|
||||
let target_path = dest_dir.join(sub_path.file_name().unwrap());
|
||||
fs::rename(&sub_path, &target_path)?;
|
||||
app_path = Some(target_path);
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.extension().is_some_and(|ext| ext == "app") {
|
||||
app_path = Some(path);
|
||||
break;
|
||||
}
|
||||
// For Chromium, check subdirectories (chrome-mac folder)
|
||||
if path.is_dir() {
|
||||
if let Ok(sub_entries) = fs::read_dir(&path) {
|
||||
for sub_entry in sub_entries.flatten() {
|
||||
let sub_path = sub_entry.path();
|
||||
if sub_path.extension().is_some_and(|ext| ext == "app") {
|
||||
// Move the app to the root destination directory
|
||||
let target_path = dest_dir.join(sub_path.file_name().unwrap());
|
||||
fs::rename(&sub_path, &target_path)?;
|
||||
app_path = Some(target_path);
|
||||
|
||||
// Clean up the now-empty subdirectory
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if app_path.is_some() {
|
||||
// Clean up the now-empty subdirectory
|
||||
let _ = fs::remove_dir_all(&path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if app_path.is_some() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,7 +234,6 @@ mod tests {
|
||||
fn test_extractor_creation() {
|
||||
let _extractor = Extractor::new();
|
||||
// Just verify we can create an extractor instance
|
||||
assert!(true);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -327,7 +322,7 @@ mod tests {
|
||||
let entries: Vec<_> = fs::read_dir(temp_dir.path())
|
||||
.unwrap()
|
||||
.filter_map(Result::ok)
|
||||
.filter(|entry| entry.path().extension().map_or(false, |ext| ext == "app"))
|
||||
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.collect();
|
||||
|
||||
assert_eq!(entries.len(), 1);
|
||||
@@ -349,19 +344,15 @@ mod tests {
|
||||
let mut found_app = false;
|
||||
|
||||
if let Ok(entries) = fs::read_dir(temp_dir.path()) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Ok(sub_entries) = fs::read_dir(&path) {
|
||||
for sub_entry in sub_entries {
|
||||
if let Ok(sub_entry) = sub_entry {
|
||||
let sub_path = sub_entry.path();
|
||||
if sub_path.extension().map_or(false, |ext| ext == "app") {
|
||||
found_app = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
if let Ok(sub_entries) = fs::read_dir(&path) {
|
||||
for sub_entry in sub_entries.flatten() {
|
||||
let sub_path = sub_entry.path();
|
||||
if sub_path.extension().is_some_and(|ext| ext == "app") {
|
||||
found_app = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -259,7 +259,7 @@ impl VersionUpdater {
|
||||
) -> Result<Vec<BackgroundUpdateResult>, Box<dyn std::error::Error + Send + Sync>> {
|
||||
println!("Starting background version update for all browsers");
|
||||
|
||||
let browsers = vec![
|
||||
let browsers = [
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"mullvad-browser",
|
||||
@@ -416,11 +416,7 @@ impl VersionUpdater {
|
||||
let elapsed = current_time.saturating_sub(state.last_update_time);
|
||||
let update_interval_secs = state.update_interval_hours * 60 * 60;
|
||||
|
||||
if elapsed >= update_interval_secs {
|
||||
0 // Update overdue
|
||||
} else {
|
||||
update_interval_secs - elapsed
|
||||
}
|
||||
update_interval_secs.saturating_sub(elapsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
-20
@@ -6,7 +6,6 @@ import { ProfilesDataTable } from "@/components/profile-data-table";
|
||||
import { ProfileSelectorDialog } from "@/components/profile-selector-dialog";
|
||||
import { ProxySettingsDialog } from "@/components/proxy-settings-dialog";
|
||||
import { SettingsDialog } from "@/components/settings-dialog";
|
||||
import { useUpdateNotifications } from "@/components/update-notification";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
@@ -14,12 +13,13 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useUpdateNotifications } from "@/hooks/use-update-notifications";
|
||||
import { showErrorToast } from "@/lib/toast-utils";
|
||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { GoGear, GoPlus } from "react-icons/go";
|
||||
import { showErrorToast } from "@/components/custom-toast";
|
||||
|
||||
type BrowserTypeString =
|
||||
| "mullvad-browser"
|
||||
@@ -177,26 +177,14 @@ export default function Home() {
|
||||
});
|
||||
console.log("Smart URL opening succeeded:", result);
|
||||
// URL was handled successfully
|
||||
} catch (error: any) {
|
||||
} catch (error: unknown) {
|
||||
console.log(
|
||||
"Smart URL opening failed or requires profile selection:",
|
||||
error
|
||||
);
|
||||
|
||||
// Check if it's the special error cases
|
||||
if (error === "show_selector") {
|
||||
// Show profile selector
|
||||
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
|
||||
} else if (error === "no_profiles") {
|
||||
// No profiles available, show error message
|
||||
setError(
|
||||
"No profiles available. Please create a profile first before opening URLs."
|
||||
);
|
||||
} else {
|
||||
// Some other error occurred
|
||||
console.error("Failed to open URL:", error);
|
||||
setError(`Failed to open URL: ${error}`);
|
||||
}
|
||||
// Show profile selector for manual selection
|
||||
setPendingUrls((prev) => [...prev, { id: Date.now().toString(), url }]);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -260,7 +248,9 @@ export default function Home() {
|
||||
|
||||
await loadProfiles();
|
||||
} catch (error) {
|
||||
setError(`Failed to create profile: ${error as any}`);
|
||||
setError(
|
||||
`Failed to create profile: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
@@ -347,9 +337,9 @@ export default function Home() {
|
||||
if (profiles.length === 0 || !isClient) return;
|
||||
|
||||
const interval = setInterval(() => {
|
||||
profiles.forEach((profile) => {
|
||||
for (const profile of profiles) {
|
||||
void checkBrowserStatus(profile);
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -16,8 +16,8 @@ import { VersionSelector } from "@/components/version-selector";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
|
||||
interface ChangeVersionDialogProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -43,12 +43,11 @@
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { toast as sonnerToast } from "sonner";
|
||||
import {
|
||||
LuCheckCheck,
|
||||
LuTriangleAlert,
|
||||
LuDownload,
|
||||
LuRefreshCw,
|
||||
LuTriangleAlert,
|
||||
} from "react-icons/lu";
|
||||
|
||||
interface BaseToastProps {
|
||||
@@ -123,7 +122,6 @@ function getToastIcon(type: ToastProps["type"], stage?: string) {
|
||||
return (
|
||||
<LuRefreshCw className="h-4 w-4 text-blue-500 animate-spin flex-shrink-0" />
|
||||
);
|
||||
case "loading":
|
||||
default:
|
||||
return (
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-2 border-blue-500 border-t-transparent flex-shrink-0" />
|
||||
@@ -214,204 +212,3 @@ export function UnifiedToast(props: ToastProps) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Unified toast function
|
||||
export function showToast(props: ToastProps & { id?: string }) {
|
||||
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
|
||||
|
||||
// Improved duration logic - make toasts disappear more quickly
|
||||
let duration: number;
|
||||
if (props.duration !== undefined) {
|
||||
duration = props.duration;
|
||||
} else {
|
||||
switch (props.type) {
|
||||
case "loading":
|
||||
case "fetching":
|
||||
duration = 10000; // 10 seconds instead of infinite
|
||||
break;
|
||||
case "download":
|
||||
// Only keep infinite for active downloading, others get shorter durations
|
||||
if ("stage" in props && props.stage === "downloading") {
|
||||
duration = Number.POSITIVE_INFINITY;
|
||||
} else if ("stage" in props && props.stage === "completed") {
|
||||
duration = 3000; // Shorter duration for completed downloads
|
||||
} else {
|
||||
duration = 8000; // 8 seconds for extracting/verifying
|
||||
}
|
||||
break;
|
||||
case "version-update":
|
||||
duration = 15000; // 15 seconds instead of infinite
|
||||
break;
|
||||
case "success":
|
||||
duration = 3000; // Shorter success duration
|
||||
break;
|
||||
case "error":
|
||||
duration = 5000; // Reasonable error duration
|
||||
break;
|
||||
default:
|
||||
duration = 4000;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === "success") {
|
||||
sonnerToast.success(<UnifiedToast {...props} />, {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
} else if (props.type === "error") {
|
||||
sonnerToast.error(<UnifiedToast {...props} />, {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
sonnerToast.custom((id) => <UnifiedToast {...props} />, {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
// Convenience functions for common use cases
|
||||
export function showLoadingToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "loading",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showDownloadToast(
|
||||
browserName: string,
|
||||
version: string,
|
||||
stage: "downloading" | "extracting" | "verifying" | "completed",
|
||||
progress?: { percentage: number; speed?: string; eta?: string },
|
||||
options?: { suppressCompletionToast?: boolean }
|
||||
) {
|
||||
const title =
|
||||
stage === "completed"
|
||||
? `${browserName} ${version} downloaded successfully!`
|
||||
: stage === "downloading"
|
||||
? `Downloading ${browserName} ${version}`
|
||||
: stage === "extracting"
|
||||
? `Extracting ${browserName} ${version}`
|
||||
: `Verifying ${browserName} ${version}`;
|
||||
|
||||
// Don't show completion toast if suppressed (for auto-update scenarios)
|
||||
if (stage === "completed" && options?.suppressCompletionToast) {
|
||||
dismissToast(`download-${browserName.toLowerCase()}-${version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return showToast({
|
||||
type: "download",
|
||||
title,
|
||||
stage,
|
||||
progress,
|
||||
id: `download-${browserName.toLowerCase()}-${version}`,
|
||||
});
|
||||
}
|
||||
|
||||
export function showVersionUpdateToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
progress?: {
|
||||
current: number;
|
||||
total: number;
|
||||
found: number;
|
||||
};
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "version-update",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showFetchingToast(
|
||||
browserName: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "fetching",
|
||||
title: `Checking for new ${browserName} versions...`,
|
||||
description:
|
||||
options?.description ?? "Fetching latest release information...",
|
||||
browserName,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showSuccessToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "success",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showErrorToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "error",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Generic helper for dismissing toasts
|
||||
export function dismissToast(id: string) {
|
||||
sonnerToast.dismiss(id);
|
||||
}
|
||||
|
||||
// Dismiss all toasts
|
||||
export function dismissAllToasts() {
|
||||
sonnerToast.dismiss();
|
||||
}
|
||||
|
||||
@@ -31,6 +31,8 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTableSorting } from "@/hooks/use-table-sorting";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import {
|
||||
type ColumnDef,
|
||||
@@ -40,14 +42,12 @@ import {
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
|
||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||
import * as React from "react";
|
||||
import { CiCircleCheck } from "react-icons/ci";
|
||||
import { useTableSorting } from "@/hooks/use-table-sorting";
|
||||
import { IoEllipsisHorizontal } from "react-icons/io5";
|
||||
import { LuChevronDown, LuChevronUp } from "react-icons/lu";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
|
||||
interface ProfilesDataTableProps {
|
||||
data: BrowserProfile[];
|
||||
@@ -392,7 +392,17 @@ export function ProfilesDataTable({
|
||||
},
|
||||
},
|
||||
],
|
||||
[isClient, runningProfiles, isUpdating, data]
|
||||
[
|
||||
isClient,
|
||||
runningProfiles,
|
||||
isUpdating,
|
||||
data,
|
||||
onLaunchProfile,
|
||||
onKillProfile,
|
||||
onProxySettings,
|
||||
onDeleteProfile,
|
||||
onChangeVersion,
|
||||
]
|
||||
);
|
||||
|
||||
const table = useReactTable({
|
||||
|
||||
@@ -23,12 +23,12 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { LuCopy } from "react-icons/lu";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LuCopy } from "react-icons/lu";
|
||||
import { toast } from "sonner";
|
||||
import { getBrowserDisplayName, getBrowserIcon } from "@/lib/browser-utils";
|
||||
|
||||
interface ProfileSelectorDialogProps {
|
||||
isOpen: boolean;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
||||
import { LuCheck } from "react-icons/lu";
|
||||
import type * as React from "react";
|
||||
import { LuCheck } from "react-icons/lu";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu";
|
||||
import type * as React from "react";
|
||||
import { LuCheck, LuChevronRight, LuCircle } from "react-icons/lu";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu";
|
||||
import type * as React from "react";
|
||||
import { LuCheck, LuChevronDown, LuChevronUp } from "react-icons/lu";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
|
||||
@@ -2,14 +2,12 @@
|
||||
|
||||
/* eslint-disable @typescript-eslint/no-misused-promises */
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { toast } from "sonner";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { FaDownload, FaTimes } from "react-icons/fa";
|
||||
import { showToast } from "@/components/custom-toast";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import React from "react";
|
||||
import { FaDownload, FaTimes } from "react-icons/fa";
|
||||
import { LuDownload } from "react-icons/lu";
|
||||
|
||||
interface UpdateNotification {
|
||||
id: string;
|
||||
@@ -28,7 +26,7 @@ interface UpdateNotificationProps {
|
||||
isUpdating?: boolean;
|
||||
}
|
||||
|
||||
function UpdateNotificationComponent({
|
||||
export function UpdateNotificationComponent({
|
||||
notification,
|
||||
onUpdate,
|
||||
onDismiss,
|
||||
@@ -108,198 +106,3 @@ function UpdateNotificationComponent({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
|
||||
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
// Ensure we're on the client side to prevent hydration mismatches
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
if (!isClient) return; // Only run on client side
|
||||
|
||||
try {
|
||||
const updates = await invoke<UpdateNotification[]>(
|
||||
"check_for_browser_updates"
|
||||
);
|
||||
setNotifications(updates);
|
||||
|
||||
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
|
||||
// to avoid circular dependencies
|
||||
} catch (error) {
|
||||
console.error("Failed to check for updates:", error);
|
||||
}
|
||||
}, [isClient]);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (browser: string, newVersion: string) => {
|
||||
try {
|
||||
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
|
||||
const browserDisplayName = getBrowserDisplayName(browser);
|
||||
|
||||
// Dismiss all notifications for this browser first
|
||||
const browserNotifications = notifications.filter(
|
||||
(n) => n.browser === browser
|
||||
);
|
||||
for (const notification of browserNotifications) {
|
||||
toast.dismiss(notification.id);
|
||||
await invoke("dismiss_update_notification", {
|
||||
notificationId: notification.id,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if browser already exists before downloading
|
||||
const isDownloaded = await invoke<boolean>("check_browser_exists", {
|
||||
browserStr: browser,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
if (isDownloaded) {
|
||||
// Browser already exists, skip download and go straight to profile update
|
||||
console.log(
|
||||
`${browserDisplayName} ${newVersion} already exists, skipping download`
|
||||
);
|
||||
} else {
|
||||
// Mark download as auto-update in the backend for toast suppression
|
||||
await invoke("mark_auto_update_download", {
|
||||
browser,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
// Download the browser (progress will be handled by use-browser-download hook)
|
||||
await invoke("download_browser", {
|
||||
browserStr: browser,
|
||||
version: newVersion,
|
||||
});
|
||||
}
|
||||
|
||||
// Complete the update with auto-update of profile versions
|
||||
const updatedProfiles = await invoke<string[]>(
|
||||
"complete_browser_update_with_auto_update",
|
||||
{
|
||||
browser,
|
||||
newVersion,
|
||||
}
|
||||
);
|
||||
|
||||
// Show success message based on whether profiles were updated
|
||||
if (updatedProfiles.length > 0) {
|
||||
const profileText =
|
||||
updatedProfiles.length === 1
|
||||
? `Profile "${updatedProfiles[0]}" has been updated`
|
||||
: `${updatedProfiles.length} profiles have been updated`;
|
||||
|
||||
showToast({
|
||||
type: "success",
|
||||
title: `${browserDisplayName} update completed`,
|
||||
description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`,
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
type: "success",
|
||||
title: `${browserDisplayName} update ready`,
|
||||
description:
|
||||
"All affected profiles are currently running. Stop them and manually update their versions to use the new version.",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.error("Failed to download browser:", downloadError);
|
||||
|
||||
// Clean up auto-update tracking on error
|
||||
try {
|
||||
await invoke("remove_auto_update_download", {
|
||||
browser,
|
||||
version: newVersion,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to clean up auto-update tracking:", e);
|
||||
}
|
||||
|
||||
showToast({
|
||||
type: "error",
|
||||
title: `Failed to download ${browserDisplayName} ${newVersion}`,
|
||||
description: String(downloadError),
|
||||
duration: 6000,
|
||||
});
|
||||
throw downloadError;
|
||||
}
|
||||
|
||||
// Refresh notifications to clear any remaining ones
|
||||
await checkForUpdates();
|
||||
} catch (error) {
|
||||
console.error("Failed to start update:", error);
|
||||
const browserDisplayName = getBrowserDisplayName(browser);
|
||||
showToast({
|
||||
type: "error",
|
||||
title: `Failed to update ${browserDisplayName}`,
|
||||
description: String(error),
|
||||
duration: 6000,
|
||||
});
|
||||
} finally {
|
||||
setUpdatingBrowsers((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(browser);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[notifications, checkForUpdates]
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
async (notificationId: string) => {
|
||||
if (!isClient) return; // Only run on client side
|
||||
|
||||
try {
|
||||
toast.dismiss(notificationId);
|
||||
await invoke("dismiss_update_notification", { notificationId });
|
||||
await checkForUpdates();
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss notification:", error);
|
||||
}
|
||||
},
|
||||
[checkForUpdates, isClient]
|
||||
);
|
||||
|
||||
// Separate effect to show toasts when notifications change
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
notifications.forEach((notification) => {
|
||||
const isUpdating = updatingBrowsers.has(notification.browser);
|
||||
|
||||
toast.custom(
|
||||
() => (
|
||||
<UpdateNotificationComponent
|
||||
notification={notification}
|
||||
onUpdate={handleUpdate}
|
||||
onDismiss={handleDismiss}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
),
|
||||
{
|
||||
id: notification.id,
|
||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||
position: "top-right",
|
||||
// Remove transparent styling to fix background issue
|
||||
style: undefined,
|
||||
}
|
||||
);
|
||||
});
|
||||
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
checkForUpdates,
|
||||
isUpdating: (browser: string) => updatingBrowsers.has(browser),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { LuDownload } from "react-icons/lu";
|
||||
import { useState } from "react";
|
||||
import { LuDownload } from "react-icons/lu";
|
||||
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
|
||||
import { ScrollArea } from "./ui/scroll-area";
|
||||
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { useVersionUpdater } from "@/hooks/use-version-updater";
|
||||
import {
|
||||
LuRefreshCw,
|
||||
LuClock,
|
||||
LuCheckCheck,
|
||||
LuCircleAlert,
|
||||
LuClock,
|
||||
LuRefreshCw,
|
||||
} from "react-icons/lu";
|
||||
|
||||
export function VersionUpdateSettings() {
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import {
|
||||
dismissToast,
|
||||
showDownloadToast,
|
||||
showErrorToast,
|
||||
showFetchingToast,
|
||||
showSuccessToast,
|
||||
} from "@/lib/toast-utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
showDownloadToast,
|
||||
showFetchingToast,
|
||||
showSuccessToast,
|
||||
showErrorToast,
|
||||
dismissToast,
|
||||
} from "../components/custom-toast";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
|
||||
interface GithubRelease {
|
||||
tag_name: string;
|
||||
@@ -192,15 +192,15 @@ export function useBrowserDownload() {
|
||||
const formatTime = (seconds: number): string => {
|
||||
if (seconds < 60) {
|
||||
return `${Math.round(seconds)}s`;
|
||||
} else if (seconds < 3600) {
|
||||
}
|
||||
if (seconds < 3600) {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = Math.round(seconds % 60);
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
} else {
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
}
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
return `${hours}h ${minutes}m`;
|
||||
};
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
@@ -208,9 +208,7 @@ export function useBrowserDownload() {
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${Number.parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${
|
||||
sizes[i]
|
||||
}`;
|
||||
return `${Number.parseFloat((bytes / k ** i).toFixed(1))} ${sizes[i]}`;
|
||||
};
|
||||
|
||||
const loadVersions = useCallback(async (browserStr: string) => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import type { TableSortingSettings } from "@/types";
|
||||
import type { SortingState } from "@tanstack/react-table";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export function useTableSorting() {
|
||||
const [sortingSettings, setSortingSettings] = useState<TableSortingSettings>({
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { UpdateNotificationComponent } from "@/components/update-notification";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import { showToast } from "@/lib/toast-utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
interface UpdateNotification {
|
||||
id: string;
|
||||
browser: string;
|
||||
current_version: string;
|
||||
new_version: string;
|
||||
affected_profiles: string[];
|
||||
is_stable_update: boolean;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export function useUpdateNotifications() {
|
||||
const [notifications, setNotifications] = useState<UpdateNotification[]>([]);
|
||||
const [updatingBrowsers, setUpdatingBrowsers] = useState<Set<string>>(
|
||||
new Set()
|
||||
);
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
|
||||
// Ensure we're on the client side to prevent hydration mismatches
|
||||
useEffect(() => {
|
||||
setIsClient(true);
|
||||
}, []);
|
||||
|
||||
const checkForUpdates = useCallback(async () => {
|
||||
if (!isClient) return; // Only run on client side
|
||||
|
||||
try {
|
||||
const updates = await invoke<UpdateNotification[]>(
|
||||
"check_for_browser_updates"
|
||||
);
|
||||
setNotifications(updates);
|
||||
|
||||
// Show toasts for new notifications - we'll define handleUpdate and handleDismiss separately
|
||||
// to avoid circular dependencies
|
||||
} catch (error) {
|
||||
console.error("Failed to check for updates:", error);
|
||||
}
|
||||
}, [isClient]);
|
||||
|
||||
const handleUpdate = useCallback(
|
||||
async (browser: string, newVersion: string) => {
|
||||
try {
|
||||
setUpdatingBrowsers((prev) => new Set(prev).add(browser));
|
||||
const browserDisplayName = getBrowserDisplayName(browser);
|
||||
|
||||
// Dismiss all notifications for this browser first
|
||||
const browserNotifications = notifications.filter(
|
||||
(n) => n.browser === browser
|
||||
);
|
||||
for (const notification of browserNotifications) {
|
||||
toast.dismiss(notification.id);
|
||||
await invoke("dismiss_update_notification", {
|
||||
notificationId: notification.id,
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if browser already exists before downloading
|
||||
const isDownloaded = await invoke<boolean>("check_browser_exists", {
|
||||
browserStr: browser,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
if (isDownloaded) {
|
||||
// Browser already exists, skip download and go straight to profile update
|
||||
console.log(
|
||||
`${browserDisplayName} ${newVersion} already exists, skipping download`
|
||||
);
|
||||
} else {
|
||||
// Mark download as auto-update in the backend for toast suppression
|
||||
await invoke("mark_auto_update_download", {
|
||||
browser,
|
||||
version: newVersion,
|
||||
});
|
||||
|
||||
// Download the browser (progress will be handled by use-browser-download hook)
|
||||
await invoke("download_browser", {
|
||||
browserStr: browser,
|
||||
version: newVersion,
|
||||
});
|
||||
}
|
||||
|
||||
// Complete the update with auto-update of profile versions
|
||||
const updatedProfiles = await invoke<string[]>(
|
||||
"complete_browser_update_with_auto_update",
|
||||
{
|
||||
browser,
|
||||
newVersion,
|
||||
}
|
||||
);
|
||||
|
||||
// Show success message based on whether profiles were updated
|
||||
if (updatedProfiles.length > 0) {
|
||||
const profileText =
|
||||
updatedProfiles.length === 1
|
||||
? `Profile "${updatedProfiles[0]}" has been updated`
|
||||
: `${updatedProfiles.length} profiles have been updated`;
|
||||
|
||||
showToast({
|
||||
type: "success",
|
||||
title: `${browserDisplayName} update completed`,
|
||||
description: `${profileText} to version ${newVersion}. Running profiles were not updated and can be updated manually.`,
|
||||
duration: 5000,
|
||||
});
|
||||
} else {
|
||||
showToast({
|
||||
type: "success",
|
||||
title: `${browserDisplayName} update ready`,
|
||||
description:
|
||||
"All affected profiles are currently running. Stop them and manually update their versions to use the new version.",
|
||||
duration: 5000,
|
||||
});
|
||||
}
|
||||
} catch (downloadError) {
|
||||
console.error("Failed to download browser:", downloadError);
|
||||
|
||||
// Clean up auto-update tracking on error
|
||||
try {
|
||||
await invoke("remove_auto_update_download", {
|
||||
browser,
|
||||
version: newVersion,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Failed to clean up auto-update tracking:", e);
|
||||
}
|
||||
|
||||
showToast({
|
||||
type: "error",
|
||||
title: `Failed to download ${browserDisplayName} ${newVersion}`,
|
||||
description: String(downloadError),
|
||||
duration: 6000,
|
||||
});
|
||||
throw downloadError;
|
||||
}
|
||||
|
||||
// Refresh notifications to clear any remaining ones
|
||||
await checkForUpdates();
|
||||
} catch (error) {
|
||||
console.error("Failed to start update:", error);
|
||||
const browserDisplayName = getBrowserDisplayName(browser);
|
||||
showToast({
|
||||
type: "error",
|
||||
title: `Failed to update ${browserDisplayName}`,
|
||||
description: String(error),
|
||||
duration: 6000,
|
||||
});
|
||||
} finally {
|
||||
setUpdatingBrowsers((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(browser);
|
||||
return next;
|
||||
});
|
||||
}
|
||||
},
|
||||
[notifications, checkForUpdates]
|
||||
);
|
||||
|
||||
const handleDismiss = useCallback(
|
||||
async (notificationId: string) => {
|
||||
if (!isClient) return; // Only run on client side
|
||||
|
||||
try {
|
||||
toast.dismiss(notificationId);
|
||||
await invoke("dismiss_update_notification", { notificationId });
|
||||
await checkForUpdates();
|
||||
} catch (error) {
|
||||
console.error("Failed to dismiss notification:", error);
|
||||
}
|
||||
},
|
||||
[checkForUpdates, isClient]
|
||||
);
|
||||
|
||||
// Separate effect to show toasts when notifications change
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
for (const notification of notifications) {
|
||||
const isUpdating = updatingBrowsers.has(notification.browser);
|
||||
|
||||
toast.custom(
|
||||
() => (
|
||||
<UpdateNotificationComponent
|
||||
notification={notification}
|
||||
onUpdate={handleUpdate}
|
||||
onDismiss={handleDismiss}
|
||||
isUpdating={isUpdating}
|
||||
/>
|
||||
),
|
||||
{
|
||||
id: notification.id,
|
||||
duration: Number.POSITIVE_INFINITY, // Persistent until user action
|
||||
position: "top-right",
|
||||
// Remove transparent styling to fix background issue
|
||||
style: undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
}, [notifications, updatingBrowsers, handleUpdate, handleDismiss, isClient]);
|
||||
|
||||
return {
|
||||
notifications,
|
||||
checkForUpdates,
|
||||
isUpdating: (browser: string) => updatingBrowsers.has(browser),
|
||||
};
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import {
|
||||
dismissToast,
|
||||
showLoadingToast,
|
||||
showVersionUpdateToast,
|
||||
} from "@/lib/toast-utils";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
showVersionUpdateToast,
|
||||
showLoadingToast,
|
||||
dismissToast,
|
||||
} from "../components/custom-toast";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
|
||||
interface VersionUpdateProgress {
|
||||
current_browser: string;
|
||||
@@ -158,7 +158,7 @@ export function useVersionUpdater() {
|
||||
).length;
|
||||
|
||||
if (failedUpdates > 0) {
|
||||
toast.warning(`Update completed with some errors`, {
|
||||
toast.warning("Update completed with some errors", {
|
||||
description: `${totalNewVersions} new versions found, ${failedUpdates} browsers failed to update`,
|
||||
duration: 5000,
|
||||
});
|
||||
@@ -226,11 +226,11 @@ export function useVersionUpdater() {
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m`;
|
||||
} else if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
} else {
|
||||
return "< 1m";
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m`;
|
||||
}
|
||||
return "< 1m";
|
||||
}, []);
|
||||
|
||||
const formatLastUpdateTime = useCallback(
|
||||
@@ -245,11 +245,11 @@ export function useVersionUpdater() {
|
||||
|
||||
if (diffHours > 0) {
|
||||
return `${diffHours}h ${diffMinutes}m ago`;
|
||||
} else if (diffMinutes > 0) {
|
||||
return `${diffMinutes}m ago`;
|
||||
} else {
|
||||
return "Just now";
|
||||
}
|
||||
if (diffMinutes > 0) {
|
||||
return `${diffMinutes}m ago`;
|
||||
}
|
||||
return "Just now";
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
* Centralized helpers for browser name mapping, icons, etc.
|
||||
*/
|
||||
|
||||
import { SiMullvad, SiBrave, SiTorbrowser } from "react-icons/si";
|
||||
import { FaChrome, FaFirefox } from "react-icons/fa";
|
||||
import { SiBrave, SiMullvad, SiTorbrowser } from "react-icons/si";
|
||||
|
||||
/**
|
||||
* Map internal browser names to display names
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { UnifiedToast } from "@/components/custom-toast";
|
||||
import React from "react";
|
||||
import { toast as sonnerToast } from "sonner";
|
||||
|
||||
// Define toast types locally
|
||||
export interface BaseToastProps {
|
||||
id?: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface LoadingToastProps extends BaseToastProps {
|
||||
type: "loading";
|
||||
}
|
||||
|
||||
export interface SuccessToastProps extends BaseToastProps {
|
||||
type: "success";
|
||||
}
|
||||
|
||||
export interface ErrorToastProps extends BaseToastProps {
|
||||
type: "error";
|
||||
}
|
||||
|
||||
export interface DownloadToastProps extends BaseToastProps {
|
||||
type: "download";
|
||||
stage?: "downloading" | "extracting" | "verifying" | "completed";
|
||||
progress?: {
|
||||
percentage: number;
|
||||
speed?: string;
|
||||
eta?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface VersionUpdateToastProps extends BaseToastProps {
|
||||
type: "version-update";
|
||||
progress?: {
|
||||
current: number;
|
||||
total: number;
|
||||
found: number;
|
||||
};
|
||||
}
|
||||
|
||||
export interface FetchingToastProps extends BaseToastProps {
|
||||
type: "fetching";
|
||||
browserName?: string;
|
||||
}
|
||||
|
||||
export type ToastProps =
|
||||
| LoadingToastProps
|
||||
| SuccessToastProps
|
||||
| ErrorToastProps
|
||||
| DownloadToastProps
|
||||
| VersionUpdateToastProps
|
||||
| FetchingToastProps;
|
||||
|
||||
// Unified toast function
|
||||
export function showToast(props: ToastProps & { id?: string }) {
|
||||
const toastId = props.id ?? `toast-${props.type}-${Date.now()}`;
|
||||
|
||||
// Improved duration logic - make toasts disappear more quickly
|
||||
let duration: number;
|
||||
if (props.duration !== undefined) {
|
||||
duration = props.duration;
|
||||
} else {
|
||||
switch (props.type) {
|
||||
case "loading":
|
||||
case "fetching":
|
||||
duration = 10000; // 10 seconds instead of infinite
|
||||
break;
|
||||
case "download":
|
||||
// Only keep infinite for active downloading, others get shorter durations
|
||||
if ("stage" in props && props.stage === "downloading") {
|
||||
duration = Number.POSITIVE_INFINITY;
|
||||
} else if ("stage" in props && props.stage === "completed") {
|
||||
duration = 3000; // Shorter duration for completed downloads
|
||||
} else {
|
||||
duration = 8000; // 8 seconds for extracting/verifying
|
||||
}
|
||||
break;
|
||||
case "version-update":
|
||||
duration = 15000; // 15 seconds instead of infinite
|
||||
break;
|
||||
case "success":
|
||||
duration = 3000; // Shorter success duration
|
||||
break;
|
||||
case "error":
|
||||
duration = 5000; // Reasonable error duration
|
||||
break;
|
||||
default:
|
||||
duration = 4000;
|
||||
}
|
||||
}
|
||||
|
||||
if (props.type === "success") {
|
||||
sonnerToast.success(React.createElement(UnifiedToast, props), {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
} else if (props.type === "error") {
|
||||
sonnerToast.error(React.createElement(UnifiedToast, props), {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
sonnerToast.custom((id) => React.createElement(UnifiedToast, props), {
|
||||
id: toastId,
|
||||
duration,
|
||||
style: {
|
||||
background: "transparent",
|
||||
border: "none",
|
||||
boxShadow: "none",
|
||||
padding: 0,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return toastId;
|
||||
}
|
||||
|
||||
// Convenience functions for common use cases
|
||||
export function showLoadingToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "loading",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showDownloadToast(
|
||||
browserName: string,
|
||||
version: string,
|
||||
stage: "downloading" | "extracting" | "verifying" | "completed",
|
||||
progress?: { percentage: number; speed?: string; eta?: string },
|
||||
options?: { suppressCompletionToast?: boolean }
|
||||
) {
|
||||
const title =
|
||||
stage === "completed"
|
||||
? `${browserName} ${version} downloaded successfully!`
|
||||
: stage === "downloading"
|
||||
? `Downloading ${browserName} ${version}`
|
||||
: stage === "extracting"
|
||||
? `Extracting ${browserName} ${version}`
|
||||
: `Verifying ${browserName} ${version}`;
|
||||
|
||||
// Don't show completion toast if suppressed (for auto-update scenarios)
|
||||
if (stage === "completed" && options?.suppressCompletionToast) {
|
||||
dismissToast(`download-${browserName.toLowerCase()}-${version}`);
|
||||
return;
|
||||
}
|
||||
|
||||
return showToast({
|
||||
type: "download",
|
||||
title,
|
||||
stage,
|
||||
progress,
|
||||
id: `download-${browserName.toLowerCase()}-${version}`,
|
||||
});
|
||||
}
|
||||
|
||||
export function showVersionUpdateToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
progress?: {
|
||||
current: number;
|
||||
total: number;
|
||||
found: number;
|
||||
};
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "version-update",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showFetchingToast(
|
||||
browserName: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "fetching",
|
||||
title: `Checking for new ${browserName} versions...`,
|
||||
description:
|
||||
options?.description ?? "Fetching latest release information...",
|
||||
browserName,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showSuccessToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "success",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
export function showErrorToast(
|
||||
title: string,
|
||||
options?: {
|
||||
id?: string;
|
||||
description?: string;
|
||||
duration?: number;
|
||||
}
|
||||
) {
|
||||
return showToast({
|
||||
type: "error",
|
||||
title,
|
||||
...options,
|
||||
});
|
||||
}
|
||||
|
||||
// Generic helper for dismissing toasts
|
||||
export function dismissToast(id: string) {
|
||||
sonnerToast.dismiss(id);
|
||||
}
|
||||
|
||||
// Dismiss all toasts
|
||||
export function dismissAllToasts() {
|
||||
sonnerToast.dismiss();
|
||||
}
|
||||
Reference in New Issue
Block a user