mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-18 20:50:06 +02:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3af581c4ab | |||
| 7a85edfb8a | |||
| 141a5f06a4 | |||
| 7a3857c06a | |||
| ed26786fdb | |||
| 966268ff05 | |||
| 87ae696d7a | |||
| 7e92b290b6 | |||
| eb62e0abf9 | |||
| 0ed5adf2ba | |||
| dd91aaeea0 | |||
| 6a3407796d | |||
| eaa1a823db | |||
| 63900bd0ad |
@@ -126,10 +126,11 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REF_NAME: ${{ github.ref_name }}
|
||||
with:
|
||||
tagName: ${{ github.ref_name }}
|
||||
releaseName: "Donut Browser ${{ github.ref_name }}"
|
||||
releaseBody: "See the assets to download this version and install."
|
||||
releaseDraft: true
|
||||
releaseDraft: false
|
||||
prerelease: false
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
@@ -94,6 +94,9 @@ jobs:
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BUILD_TAG: "nightly-${{ steps.commit.outputs.hash }}"
|
||||
GITHUB_REF_NAME: "nightly-${{ steps.commit.outputs.hash }}"
|
||||
GITHUB_SHA: ${{ github.sha }}
|
||||
with:
|
||||
tagName: "nightly-${{ steps.commit.outputs.hash }}"
|
||||
releaseName: "Donut Browser Nightly (Build ${{ steps.commit.outputs.hash }})"
|
||||
|
||||
@@ -27,6 +27,8 @@
|
||||
|
||||
## Download
|
||||
|
||||
> As of right now, the app is not signed by Apple. You need to have Gatekeeper disabled to run it. The app automatically checks for updates on each launch.
|
||||
|
||||
The app can be downloaded from the [releases page](https://github.com/zhom/donutbrowser/releases/latest).
|
||||
|
||||
## Supported Platforms
|
||||
|
||||
+2
-2
@@ -1,14 +1,14 @@
|
||||
{
|
||||
"name": "donutbrowser",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.3",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check src/ && tsc --noEmit && next lint",
|
||||
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all",
|
||||
"lint:rust": "cd src-tauri && cargo clippy --all-targets --all-features -- -D warnings -D clippy::all && cargo fmt --all",
|
||||
"tauri": "tauri",
|
||||
"shadcn:add": "pnpm dlx shadcn@latest add",
|
||||
"prepare": "husky",
|
||||
|
||||
Generated
+1
-1
@@ -973,7 +973,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "donutbrowser"
|
||||
version = "0.1.0"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "donutbrowser"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
version = "0.2.3"
|
||||
description = "Browser Orchestrator"
|
||||
authors = ["zhom@github"]
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>0.1.0</string>
|
||||
<string>0.2.3</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
|
||||
@@ -5,5 +5,26 @@ fn main() {
|
||||
println!("cargo:rustc-link-lib=framework=CoreServices");
|
||||
}
|
||||
|
||||
// Inject build version based on environment variables set by CI
|
||||
if let Ok(build_tag) = std::env::var("BUILD_TAG") {
|
||||
// Custom BUILD_TAG takes highest priority (used for nightly builds)
|
||||
println!("cargo:rustc-env=BUILD_VERSION={build_tag}");
|
||||
} else if let Ok(tag_name) = std::env::var("GITHUB_REF_NAME") {
|
||||
// This is set by GitHub Actions to the tag name (e.g., "v1.0.0")
|
||||
println!("cargo:rustc-env=BUILD_VERSION={tag_name}");
|
||||
} else if std::env::var("STABLE_RELEASE").is_ok() {
|
||||
// Fallback for stable releases - use CARGO_PKG_VERSION with 'v' prefix
|
||||
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
|
||||
println!("cargo:rustc-env=BUILD_VERSION=v{version}");
|
||||
} else if let Ok(commit_hash) = std::env::var("GITHUB_SHA") {
|
||||
// For nightly builds, use commit hash
|
||||
let short_hash = &commit_hash[0..7.min(commit_hash.len())];
|
||||
println!("cargo:rustc-env=BUILD_VERSION=nightly-{short_hash}");
|
||||
} else {
|
||||
// Development build fallback
|
||||
let version = std::env::var("CARGO_PKG_VERSION").unwrap_or_else(|_| "0.1.0".to_string());
|
||||
println!("cargo:rustc-env=BUILD_VERSION=dev-{version}");
|
||||
}
|
||||
|
||||
tauri_build::build()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::path::{Path, PathBuf};
|
||||
use std::process::Command;
|
||||
use tauri::Emitter;
|
||||
|
||||
use crate::extraction::Extractor;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct AppReleaseAsset {
|
||||
pub name: String,
|
||||
@@ -47,13 +49,25 @@ impl AppAutoUpdater {
|
||||
/// Check if running a nightly build based on environment variable
|
||||
pub fn is_nightly_build() -> bool {
|
||||
// If STABLE_RELEASE env var is set at compile time, it's a stable build
|
||||
// Otherwise, it's a nightly build
|
||||
option_env!("STABLE_RELEASE").is_none()
|
||||
if option_env!("STABLE_RELEASE").is_some() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Also check if the current version starts with "nightly-"
|
||||
let current_version = Self::get_current_version();
|
||||
if current_version.starts_with("nightly-") {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If STABLE_RELEASE is not set and version doesn't start with "nightly-",
|
||||
// it's still considered a nightly build (dev builds, main branch builds, etc.)
|
||||
true
|
||||
}
|
||||
|
||||
/// Get current app version from Cargo.toml
|
||||
/// Get current app version from build-time injection
|
||||
pub fn get_current_version() -> String {
|
||||
env!("CARGO_PKG_VERSION").to_string()
|
||||
// Use build-time injected version instead of CARGO_PKG_VERSION
|
||||
env!("BUILD_VERSION").to_string()
|
||||
}
|
||||
|
||||
/// Check for app updates
|
||||
@@ -63,37 +77,51 @@ impl AppAutoUpdater {
|
||||
let current_version = Self::get_current_version();
|
||||
let is_nightly = Self::is_nightly_build();
|
||||
|
||||
println!("Checking for updates - Current version: {current_version}, Is nightly: {is_nightly}");
|
||||
println!("=== App Update Check ===");
|
||||
println!("Current version: {current_version}");
|
||||
println!("Is nightly build: {is_nightly}");
|
||||
println!("STABLE_RELEASE env: {:?}", option_env!("STABLE_RELEASE"));
|
||||
|
||||
let releases = self.fetch_app_releases().await?;
|
||||
println!("Fetched {} releases from GitHub", releases.len());
|
||||
|
||||
// Filter releases based on build type
|
||||
let filtered_releases: Vec<&AppRelease> = if is_nightly {
|
||||
// For nightly builds, look for nightly releases
|
||||
releases
|
||||
let nightly_releases: Vec<&AppRelease> = releases
|
||||
.iter()
|
||||
.filter(|release| release.tag_name.starts_with("nightly-"))
|
||||
.collect()
|
||||
.collect();
|
||||
println!("Found {} nightly releases", nightly_releases.len());
|
||||
nightly_releases
|
||||
} else {
|
||||
// For stable builds, look for stable releases (semver format)
|
||||
releases
|
||||
let stable_releases: Vec<&AppRelease> = releases
|
||||
.iter()
|
||||
.filter(|release| {
|
||||
release.tag_name.starts_with('v') && !release.tag_name.starts_with("nightly-")
|
||||
})
|
||||
.collect()
|
||||
.collect();
|
||||
println!("Found {} stable releases", stable_releases.len());
|
||||
stable_releases
|
||||
};
|
||||
|
||||
if filtered_releases.is_empty() {
|
||||
println!("No releases found for build type");
|
||||
println!("No releases found for build type (nightly: {is_nightly})");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// Get the latest release
|
||||
let latest_release = filtered_releases[0];
|
||||
println!(
|
||||
"Latest release: {} ({})",
|
||||
latest_release.tag_name, latest_release.name
|
||||
);
|
||||
|
||||
// Check if we need to update
|
||||
if self.should_update(¤t_version, &latest_release.tag_name, is_nightly) {
|
||||
println!("Update available!");
|
||||
|
||||
// Find the appropriate asset for current platform
|
||||
if let Some(download_url) = self.get_download_url_for_platform(&latest_release.assets) {
|
||||
let update_info = AppUpdateInfo {
|
||||
@@ -105,8 +133,16 @@ impl AppAutoUpdater {
|
||||
published_at: latest_release.published_at.clone(),
|
||||
};
|
||||
|
||||
println!(
|
||||
"Update info prepared: {} -> {}",
|
||||
update_info.current_version, update_info.new_version
|
||||
);
|
||||
return Ok(Some(update_info));
|
||||
} else {
|
||||
println!("No suitable download asset found for current platform");
|
||||
}
|
||||
} else {
|
||||
println!("No update needed");
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
@@ -134,21 +170,36 @@ impl AppAutoUpdater {
|
||||
|
||||
/// Determine if an update should be performed
|
||||
fn should_update(&self, current_version: &str, new_version: &str, is_nightly: bool) -> bool {
|
||||
println!(
|
||||
"Comparing versions: current={current_version}, new={new_version}, is_nightly={is_nightly}"
|
||||
);
|
||||
|
||||
if is_nightly {
|
||||
// For nightly builds, always update if there's a newer nightly
|
||||
// Compare the commit hashes (assuming format: nightly-<commit_hash>)
|
||||
if let (Some(current_hash), Some(new_hash)) = (
|
||||
current_version.strip_prefix("nightly-"),
|
||||
new_version.strip_prefix("nightly-"),
|
||||
) {
|
||||
return new_hash != current_hash;
|
||||
// Different commit hashes mean we should update
|
||||
let should_update = new_hash != current_hash;
|
||||
println!("Nightly comparison: current_hash={current_hash}, new_hash={new_hash}, should_update={should_update}");
|
||||
return should_update;
|
||||
}
|
||||
|
||||
// If current version doesn't have nightly prefix but we're in nightly mode,
|
||||
// this could be a dev build or stable build upgrading to nightly
|
||||
if !current_version.starts_with("nightly-") {
|
||||
println!("Upgrading from non-nightly to nightly: {new_version}");
|
||||
return true;
|
||||
}
|
||||
// If current version doesn't have nightly prefix, it's an upgrade from stable to nightly
|
||||
!current_version.starts_with("nightly-")
|
||||
} else {
|
||||
// For stable builds, use semantic versioning comparison
|
||||
self.is_version_newer(new_version, current_version)
|
||||
let should_update = self.is_version_newer(new_version, current_version);
|
||||
println!("Stable comparison: {new_version} > {current_version} = {should_update}");
|
||||
return should_update;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Compare semantic versions (returns true if version1 > version2)
|
||||
@@ -174,24 +225,68 @@ impl AppAutoUpdater {
|
||||
fn get_download_url_for_platform(&self, assets: &[AppReleaseAsset]) -> Option<String> {
|
||||
let arch = if cfg!(target_arch = "aarch64") {
|
||||
"aarch64"
|
||||
} else {
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x64"
|
||||
} else {
|
||||
"unknown"
|
||||
};
|
||||
|
||||
// Look for macOS DMG with the appropriate architecture
|
||||
println!("Looking for assets with architecture: {arch}");
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg") && asset.name.contains(arch) {
|
||||
println!("Found asset: {}", asset.name);
|
||||
}
|
||||
|
||||
// Priority 1: Look for exact architecture match in DMG
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
&& (asset.name.contains(&format!("_{arch}.dmg"))
|
||||
|| asset.name.contains(&format!("-{arch}.dmg"))
|
||||
|| asset.name.contains(&format!("_{arch}_"))
|
||||
|| asset.name.contains(&format!("-{arch}-")))
|
||||
{
|
||||
println!("Found exact architecture match: {}", asset.name);
|
||||
return Some(asset.browser_download_url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: look for any macOS DMG
|
||||
// Priority 2: Look for x86_64 variations if we're looking for x64
|
||||
if arch == "x64" {
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
&& (asset.name.contains("x86_64") || asset.name.contains("x86-64"))
|
||||
{
|
||||
println!("Found x86_64 variant: {}", asset.name);
|
||||
return Some(asset.browser_download_url.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 3: Look for arm64 variations if we're looking for aarch64
|
||||
if arch == "aarch64" {
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg")
|
||||
&& (asset.name.contains("arm64") || asset.name.contains("aarch64"))
|
||||
{
|
||||
println!("Found arm64 variant: {}", asset.name);
|
||||
return Some(asset.browser_download_url.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Priority 4: Fallback to any macOS DMG
|
||||
for asset in assets {
|
||||
if asset.name.contains(".dmg") {
|
||||
if asset.name.contains(".dmg")
|
||||
&& (asset.name.to_lowercase().contains("macos")
|
||||
|| asset.name.to_lowercase().contains("darwin")
|
||||
|| !asset.name.contains(".app.tar.gz"))
|
||||
{
|
||||
// Exclude app.tar.gz files
|
||||
println!("Found fallback DMG: {}", asset.name);
|
||||
return Some(asset.browser_download_url.clone());
|
||||
}
|
||||
}
|
||||
|
||||
println!("No suitable asset found for platform");
|
||||
None
|
||||
}
|
||||
|
||||
@@ -277,73 +372,24 @@ impl AppAutoUpdater {
|
||||
Ok(file_path)
|
||||
}
|
||||
|
||||
/// Extract the update (DMG on macOS)
|
||||
/// Extract the update using the extraction module
|
||||
async fn extract_update(
|
||||
&self,
|
||||
dmg_path: &Path,
|
||||
archive_path: &Path,
|
||||
dest_dir: &Path,
|
||||
) -> Result<PathBuf, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// For DMG files on macOS, we need to mount and copy the .app
|
||||
let mount_point = dest_dir.join("mount");
|
||||
fs::create_dir_all(&mount_point)?;
|
||||
let extractor = Extractor::new();
|
||||
|
||||
// Mount the DMG
|
||||
let output = Command::new("hdiutil")
|
||||
.args([
|
||||
"attach",
|
||||
"-nobrowse",
|
||||
"-mountpoint",
|
||||
mount_point.to_str().unwrap(),
|
||||
dmg_path.to_str().unwrap(),
|
||||
])
|
||||
.output()?;
|
||||
let extension = archive_path
|
||||
.extension()
|
||||
.and_then(|ext| ext.to_str())
|
||||
.unwrap_or("");
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to mount DMG: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
match extension {
|
||||
"dmg" => extractor.extract_dmg(archive_path, dest_dir).await,
|
||||
"zip" => extractor.extract_zip(archive_path, dest_dir).await,
|
||||
_ => Err(format!("Unsupported archive format: {extension}").into()),
|
||||
}
|
||||
|
||||
// Find the .app in the mount point
|
||||
let app_entry = fs::read_dir(&mount_point)?
|
||||
.filter_map(Result::ok)
|
||||
.find(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
|
||||
.ok_or("No .app found in DMG")?;
|
||||
|
||||
let app_path = dest_dir.join("extracted_app");
|
||||
if app_path.exists() {
|
||||
fs::remove_dir_all(&app_path)?;
|
||||
}
|
||||
|
||||
// Copy the .app to extraction directory
|
||||
let output = Command::new("cp")
|
||||
.args([
|
||||
"-R",
|
||||
app_entry.path().to_str().unwrap(),
|
||||
app_path.to_str().unwrap(),
|
||||
])
|
||||
.output()?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(
|
||||
format!(
|
||||
"Failed to copy app: {}",
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Unmount the DMG
|
||||
let _ = Command::new("hdiutil")
|
||||
.args(["detach", mount_point.to_str().unwrap()])
|
||||
.output();
|
||||
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
/// Install the update by replacing the current app
|
||||
@@ -405,14 +451,59 @@ impl AppAutoUpdater {
|
||||
/// Restart the application
|
||||
async fn restart_application(&self) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let app_path = self.get_current_app_path()?;
|
||||
let current_pid = std::process::id();
|
||||
|
||||
// Use open command to restart the app
|
||||
let _ = Command::new("open")
|
||||
.args([app_path.to_str().unwrap()])
|
||||
.spawn()?;
|
||||
// Create a temporary restart script
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let script_path = temp_dir.join("donut_restart.sh");
|
||||
|
||||
// Exit current process after a short delay
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
// Create the restart script content
|
||||
let script_content = format!(
|
||||
r#"#!/bin/bash
|
||||
# Wait for the current process to exit
|
||||
while kill -0 {} 2>/dev/null; do
|
||||
sleep 0.5
|
||||
done
|
||||
|
||||
# Wait a bit more to ensure clean exit
|
||||
sleep 1
|
||||
|
||||
# Start the new application
|
||||
open "{}"
|
||||
|
||||
# Clean up this script
|
||||
rm "{}"
|
||||
"#,
|
||||
current_pid,
|
||||
app_path.to_str().unwrap(),
|
||||
script_path.to_str().unwrap()
|
||||
);
|
||||
|
||||
// Write the script to file
|
||||
fs::write(&script_path, script_content)?;
|
||||
|
||||
// Make the script executable
|
||||
let _ = Command::new("chmod")
|
||||
.args(["+x", script_path.to_str().unwrap()])
|
||||
.output();
|
||||
|
||||
// Execute the restart script in the background
|
||||
let mut cmd = Command::new("bash");
|
||||
cmd.arg(script_path.to_str().unwrap());
|
||||
|
||||
// Detach the process completely
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::process::CommandExt;
|
||||
cmd.process_group(0);
|
||||
}
|
||||
|
||||
let _child = cmd.spawn()?;
|
||||
|
||||
// Give the script a moment to start
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Exit the current process
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
@@ -448,6 +539,16 @@ pub fn get_app_version_info() -> Result<(String, bool), String> {
|
||||
))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn check_for_app_updates_manual() -> Result<Option<AppUpdateInfo>, String> {
|
||||
println!("Manual app update check triggered");
|
||||
let updater = AppAutoUpdater::new();
|
||||
updater
|
||||
.check_for_updates()
|
||||
.await
|
||||
.map_err(|e| format!("Failed to check for app updates: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -457,6 +558,10 @@ mod tests {
|
||||
// This will depend on whether STABLE_RELEASE is set during test compilation
|
||||
let is_nightly = AppAutoUpdater::is_nightly_build();
|
||||
println!("Is nightly build: {is_nightly}");
|
||||
|
||||
// The result should be true for test builds since STABLE_RELEASE is not set
|
||||
// unless the test is run in a stable release environment
|
||||
assert!(is_nightly || option_env!("STABLE_RELEASE").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -502,6 +607,27 @@ mod tests {
|
||||
|
||||
// Upgrade from stable to nightly
|
||||
assert!(updater.should_update("v1.0.0", "nightly-abc123", true));
|
||||
|
||||
// Upgrade from dev to nightly
|
||||
assert!(updater.should_update("dev-0.1.0", "nightly-abc123", true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_update_edge_cases() {
|
||||
let updater = AppAutoUpdater::new();
|
||||
|
||||
// Test with different nightly formats
|
||||
assert!(updater.should_update("nightly-abc123", "nightly-def456", true));
|
||||
assert!(!updater.should_update("nightly-abc123", "nightly-abc123", true));
|
||||
|
||||
// Test stable version edge cases
|
||||
assert!(updater.should_update("v0.9.9", "v1.0.0", false));
|
||||
assert!(!updater.should_update("v1.0.0", "v0.9.9", false));
|
||||
assert!(!updater.should_update("v1.0.0", "v1.0.0", false));
|
||||
|
||||
// Test version without 'v' prefix
|
||||
assert!(updater.should_update("0.9.9", "v1.0.0", false));
|
||||
assert!(updater.should_update("v0.9.9", "1.0.0", false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -528,4 +654,28 @@ mod tests {
|
||||
let url = url.unwrap();
|
||||
assert!(url.contains(".dmg"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extract_update_uses_extractor() {
|
||||
// This test verifies that the extract_update method properly uses the Extractor
|
||||
// We can't run the actual extraction in unit tests without real DMG files,
|
||||
// but we can verify the method signature and basic logic
|
||||
let updater = AppAutoUpdater::new();
|
||||
|
||||
// Test that unsupported formats would be rejected
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let unsupported_file = temp_dir.join("test.rar");
|
||||
|
||||
// Create a mock runtime to test the logic
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
// This would fail because .rar is not supported, which proves
|
||||
// our method is using the Extractor logic
|
||||
let result = rt.block_on(async { updater.extract_update(&unsupported_file, &temp_dir).await });
|
||||
|
||||
// Should fail with unsupported format error
|
||||
assert!(result.is_err());
|
||||
let error_msg = result.unwrap_err().to_string();
|
||||
assert!(error_msg.contains("Unsupported archive format: rar"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,11 @@ impl BrowserVersionService {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn new_with_api_client(api_client: ApiClient) -> Self {
|
||||
Self { api_client }
|
||||
}
|
||||
|
||||
/// Get cached browser versions immediately (returns None if no cache exists)
|
||||
pub fn get_cached_browser_versions(&self, browser: &str) -> Option<Vec<String>> {
|
||||
self.api_client.load_cached_versions(browser)
|
||||
@@ -541,6 +546,335 @@ impl BrowserVersionService {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use wiremock::matchers::{header, method, path};
|
||||
use wiremock::{Mock, MockServer, ResponseTemplate};
|
||||
|
||||
async fn setup_mock_server() -> MockServer {
|
||||
MockServer::start().await
|
||||
}
|
||||
|
||||
fn create_test_api_client(server: &MockServer) -> ApiClient {
|
||||
let base_url = server.uri();
|
||||
ApiClient::new_with_base_urls(
|
||||
base_url.clone(), // firefox_api_base
|
||||
base_url.clone(), // firefox_dev_api_base
|
||||
base_url.clone(), // github_api_base
|
||||
base_url.clone(), // chromium_api_base
|
||||
base_url.clone(), // tor_archive_base
|
||||
base_url.clone(), // mozilla_download_base
|
||||
)
|
||||
}
|
||||
|
||||
fn create_test_service(api_client: ApiClient) -> BrowserVersionService {
|
||||
BrowserVersionService::new_with_api_client(api_client)
|
||||
}
|
||||
|
||||
async fn setup_firefox_mocks(server: &MockServer) {
|
||||
let mock_response = r#"{
|
||||
"releases": {
|
||||
"firefox-139.0": {
|
||||
"build_number": 1,
|
||||
"category": "major",
|
||||
"date": "2024-01-15",
|
||||
"description": "Firefox 139.0 Release",
|
||||
"is_security_driven": false,
|
||||
"product": "firefox",
|
||||
"version": "139.0"
|
||||
},
|
||||
"firefox-138.0": {
|
||||
"build_number": 1,
|
||||
"category": "major",
|
||||
"date": "2024-01-01",
|
||||
"description": "Firefox 138.0 Release",
|
||||
"is_security_driven": false,
|
||||
"product": "firefox",
|
||||
"version": "138.0"
|
||||
},
|
||||
"firefox-137.0": {
|
||||
"build_number": 1,
|
||||
"category": "major",
|
||||
"date": "2023-12-15",
|
||||
"description": "Firefox 137.0 Release",
|
||||
"is_security_driven": false,
|
||||
"product": "firefox",
|
||||
"version": "137.0"
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/firefox.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_firefox_dev_mocks(server: &MockServer) {
|
||||
let mock_response = r#"{
|
||||
"releases": {
|
||||
"devedition-140.0b1": {
|
||||
"build_number": 1,
|
||||
"category": "major",
|
||||
"date": "2024-01-20",
|
||||
"description": "Firefox Developer Edition 140.0b1",
|
||||
"is_security_driven": false,
|
||||
"product": "devedition",
|
||||
"version": "140.0b1"
|
||||
},
|
||||
"devedition-139.0b5": {
|
||||
"build_number": 1,
|
||||
"category": "major",
|
||||
"date": "2024-01-10",
|
||||
"description": "Firefox Developer Edition 139.0b5",
|
||||
"is_security_driven": false,
|
||||
"product": "devedition",
|
||||
"version": "139.0b5"
|
||||
}
|
||||
}
|
||||
}"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/devedition.json"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_mullvad_mocks(server: &MockServer) {
|
||||
let mock_response = r#"[
|
||||
{
|
||||
"tag_name": "14.5a6",
|
||||
"name": "Mullvad Browser 14.5a6",
|
||||
"prerelease": true,
|
||||
"published_at": "2024-01-15T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "mullvad-browser-macos-14.5a6.dmg",
|
||||
"browser_download_url": "https://example.com/mullvad-14.5a6.dmg",
|
||||
"size": 100000000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag_name": "14.5a5",
|
||||
"name": "Mullvad Browser 14.5a5",
|
||||
"prerelease": true,
|
||||
"published_at": "2024-01-10T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "mullvad-browser-macos-14.5a5.dmg",
|
||||
"browser_download_url": "https://example.com/mullvad-14.5a5.dmg",
|
||||
"size": 99000000
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/mullvad/mullvad-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_zen_mocks(server: &MockServer) {
|
||||
let mock_response = r#"[
|
||||
{
|
||||
"tag_name": "1.0.0-twilight",
|
||||
"name": "Zen Browser Twilight",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-15T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "zen.macos-universal.dmg",
|
||||
"browser_download_url": "https://example.com/zen-twilight.dmg",
|
||||
"size": 120000000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag_name": "1.11b",
|
||||
"name": "Zen Browser 1.11b",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-10T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "zen.macos-universal.dmg",
|
||||
"browser_download_url": "https://example.com/zen-1.11b.dmg",
|
||||
"size": 115000000
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/zen-browser/desktop/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_brave_mocks(server: &MockServer) {
|
||||
let mock_response = r#"[
|
||||
{
|
||||
"tag_name": "v1.81.9",
|
||||
"name": "Brave Release 1.81.9",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-15T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "brave-v1.81.9-universal.dmg",
|
||||
"browser_download_url": "https://example.com/brave-1.81.9-universal.dmg",
|
||||
"size": 200000000
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"tag_name": "v1.81.8",
|
||||
"name": "Brave Release 1.81.8",
|
||||
"prerelease": false,
|
||||
"published_at": "2024-01-10T10:00:00Z",
|
||||
"assets": [
|
||||
{
|
||||
"name": "brave-v1.81.8-universal.dmg",
|
||||
"browser_download_url": "https://example.com/brave-1.81.8-universal.dmg",
|
||||
"size": 199000000
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/repos/brave/brave-browser/releases"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_response)
|
||||
.insert_header("content-type", "application/json"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_chromium_mocks(server: &MockServer) {
|
||||
let arch = if cfg!(target_arch = "aarch64") {
|
||||
"Mac_Arm"
|
||||
} else {
|
||||
"Mac"
|
||||
};
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path(format!("/{arch}/LAST_CHANGE")))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string("1465660")
|
||||
.insert_header("content-type", "text/plain"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn setup_tor_mocks(server: &MockServer) {
|
||||
let mock_html = r#"
|
||||
<html>
|
||||
<body>
|
||||
<a href="../">../</a>
|
||||
<a href="14.0.4/">14.0.4/</a>
|
||||
<a href="14.0.3/">14.0.3/</a>
|
||||
<a href="14.0.2/">14.0.2/</a>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let version_html_144 = r#"
|
||||
<html>
|
||||
<body>
|
||||
<a href="tor-browser-macos-14.0.4.dmg">tor-browser-macos-14.0.4.dmg</a>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let version_html_143 = r#"
|
||||
<html>
|
||||
<body>
|
||||
<a href="tor-browser-macos-14.0.3.dmg">tor-browser-macos-14.0.3.dmg</a>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
let version_html_142 = r#"
|
||||
<html>
|
||||
<body>
|
||||
<a href="tor-browser-macos-14.0.2.dmg">tor-browser-macos-14.0.2.dmg</a>
|
||||
</body>
|
||||
</html>
|
||||
"#;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(mock_html)
|
||||
.insert_header("content-type", "text/html"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.4/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_144)
|
||||
.insert_header("content-type", "text/html"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.3/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_143)
|
||||
.insert_header("content-type", "text/html"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
|
||||
Mock::given(method("GET"))
|
||||
.and(path("/14.0.2/"))
|
||||
.and(header("user-agent", "donutbrowser"))
|
||||
.respond_with(
|
||||
ResponseTemplate::new(200)
|
||||
.set_body_string(version_html_142)
|
||||
.insert_header("content-type", "text/html"),
|
||||
)
|
||||
.mount(server)
|
||||
.await;
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_browser_version_service_creation() {
|
||||
@@ -550,7 +884,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_firefox_versions() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
// Test with caching
|
||||
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
||||
@@ -561,15 +899,13 @@ mod tests {
|
||||
|
||||
if let Ok(versions) = result_cached {
|
||||
assert!(!versions.is_empty(), "Should have Firefox versions");
|
||||
assert_eq!(versions[0], "139.0", "Should have latest version first");
|
||||
println!(
|
||||
"Firefox cached test passed. Found {versions_count} versions",
|
||||
versions_count = versions.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Test without caching
|
||||
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
||||
assert!(
|
||||
@@ -582,6 +918,7 @@ mod tests {
|
||||
!versions.is_empty(),
|
||||
"Should have Firefox versions without caching"
|
||||
);
|
||||
assert_eq!(versions[0], "139.0", "Should have latest version first");
|
||||
println!(
|
||||
"Firefox no-cache test passed. Found {versions_count} versions",
|
||||
versions_count = versions.len()
|
||||
@@ -591,7 +928,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_browser_versions_with_count() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
let result = service
|
||||
.fetch_browser_versions_with_count("firefox", false)
|
||||
@@ -605,6 +946,10 @@ mod tests {
|
||||
result.versions.len(),
|
||||
"Total count should match versions length"
|
||||
);
|
||||
assert_eq!(
|
||||
result.versions[0], "139.0",
|
||||
"Should have latest version first"
|
||||
);
|
||||
println!(
|
||||
"Firefox count test passed. Found {} versions, new: {}",
|
||||
result.total_versions_count,
|
||||
@@ -615,7 +960,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_fetch_detailed_versions() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
let result = service
|
||||
.fetch_browser_versions_detailed("firefox", false)
|
||||
@@ -631,6 +980,12 @@ mod tests {
|
||||
!first_version.version.is_empty(),
|
||||
"Version should not be empty"
|
||||
);
|
||||
assert_eq!(
|
||||
first_version.version, "139.0",
|
||||
"Should have latest version first"
|
||||
);
|
||||
assert_eq!(first_version.date, "2024-01-15", "Should have correct date");
|
||||
assert!(!first_version.is_prerelease, "Should be stable release");
|
||||
println!(
|
||||
"Firefox detailed test passed. Found {versions_count} detailed versions",
|
||||
versions_count = versions.len()
|
||||
@@ -640,7 +995,9 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_unsupported_browser() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
let result = service.fetch_browser_versions("unsupported", false).await;
|
||||
assert!(
|
||||
@@ -658,7 +1015,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_incremental_update() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
// This test might fail if there are no cached versions yet, which is fine
|
||||
let result = service
|
||||
@@ -678,7 +1039,20 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_all_supported_browsers() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
|
||||
// Setup all browser mocks
|
||||
setup_firefox_mocks(&server).await;
|
||||
setup_firefox_dev_mocks(&server).await;
|
||||
setup_mullvad_mocks(&server).await;
|
||||
setup_zen_mocks(&server).await;
|
||||
setup_brave_mocks(&server).await;
|
||||
setup_chromium_mocks(&server).await;
|
||||
setup_tor_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
let browsers = vec![
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
@@ -690,30 +1064,30 @@ mod tests {
|
||||
];
|
||||
|
||||
for browser in browsers {
|
||||
// Test that we can at least call the function without panicking
|
||||
let result = service.fetch_browser_versions(browser, false).await;
|
||||
|
||||
match result {
|
||||
Ok(versions) => {
|
||||
assert!(!versions.is_empty(), "Should have versions for {browser}");
|
||||
println!(
|
||||
"{browser} test passed. Found {versions_count} versions",
|
||||
versions_count = versions.len()
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
// Some browsers might fail due to network issues, but shouldn't panic
|
||||
println!("{browser} test failed (network issue): {e}");
|
||||
panic!("{browser} test failed: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
// Small delay between requests to avoid rate limiting
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(200)).await;
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_no_caching_parameter() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
// Test with caching enabled (default)
|
||||
let result_cached = service.fetch_browser_versions("firefox", false).await;
|
||||
@@ -722,9 +1096,6 @@ mod tests {
|
||||
"Should fetch Firefox versions with caching"
|
||||
);
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Test with caching disabled (no_caching = true)
|
||||
let result_no_cache = service.fetch_browser_versions("firefox", true).await;
|
||||
assert!(
|
||||
@@ -742,6 +1113,10 @@ mod tests {
|
||||
!no_cache_versions.is_empty(),
|
||||
"No-cache versions should not be empty"
|
||||
);
|
||||
assert_eq!(
|
||||
cached_versions, no_cache_versions,
|
||||
"Both should return same versions"
|
||||
);
|
||||
println!(
|
||||
"No-caching test passed. Cached: {} versions, No-cache: {} versions",
|
||||
cached_versions.len(),
|
||||
@@ -752,7 +1127,11 @@ mod tests {
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_detailed_versions_with_no_caching() {
|
||||
let service = BrowserVersionService::new();
|
||||
let server = setup_mock_server().await;
|
||||
setup_firefox_mocks(&server).await;
|
||||
|
||||
let api_client = create_test_api_client(&server);
|
||||
let service = create_test_service(api_client);
|
||||
|
||||
// Test detailed versions with caching
|
||||
let result_cached = service
|
||||
@@ -763,9 +1142,6 @@ mod tests {
|
||||
"Should fetch detailed Firefox versions with caching"
|
||||
);
|
||||
|
||||
// Small delay to avoid rate limiting
|
||||
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
|
||||
|
||||
// Test detailed versions without caching
|
||||
let result_no_cache = service
|
||||
.fetch_browser_versions_detailed("firefox", true)
|
||||
@@ -799,6 +1175,17 @@ mod tests {
|
||||
"No-cache version should not be empty"
|
||||
);
|
||||
|
||||
assert_eq!(first_cached.version, "139.0", "Should have correct version");
|
||||
assert_eq!(
|
||||
first_no_cache.version, "139.0",
|
||||
"Should have correct version"
|
||||
);
|
||||
assert_eq!(first_cached.date, "2024-01-15", "Should have correct date");
|
||||
assert_eq!(
|
||||
first_no_cache.date, "2024-01-15",
|
||||
"Should have correct date"
|
||||
);
|
||||
|
||||
println!(
|
||||
"Detailed no-caching test passed. Cached: {} versions, No-cache: {} versions",
|
||||
cached_versions.len(),
|
||||
|
||||
@@ -46,7 +46,7 @@ impl Extractor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn extract_dmg(
|
||||
pub async fn extract_dmg(
|
||||
&self,
|
||||
dmg_path: &Path,
|
||||
dest_dir: &Path,
|
||||
@@ -149,7 +149,7 @@ impl Extractor {
|
||||
Ok(app_path)
|
||||
}
|
||||
|
||||
async fn extract_zip(
|
||||
pub async fn extract_zip(
|
||||
&self,
|
||||
zip_path: &Path,
|
||||
dest_dir: &Path,
|
||||
|
||||
@@ -54,7 +54,8 @@ use auto_updater::{
|
||||
};
|
||||
|
||||
use app_auto_updater::{
|
||||
check_for_app_updates, download_and_install_app_update, get_app_version_info,
|
||||
check_for_app_updates, check_for_app_updates_manual, download_and_install_app_update,
|
||||
get_app_version_info,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -183,6 +184,7 @@ pub fn run() {
|
||||
// Add a small delay to ensure the app is fully loaded
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(3)).await;
|
||||
|
||||
println!("Starting app update check at startup...");
|
||||
let updater = app_auto_updater::AppAutoUpdater::new();
|
||||
match updater.check_for_updates().await {
|
||||
Ok(Some(update_info)) => {
|
||||
@@ -191,7 +193,11 @@ pub fn run() {
|
||||
update_info.current_version, update_info.new_version
|
||||
);
|
||||
// Emit update available event to the frontend
|
||||
let _ = app_handle_update.emit("app-update-available", &update_info);
|
||||
if let Err(e) = app_handle_update.emit("app-update-available", &update_info) {
|
||||
eprintln!("Failed to emit app update event: {e}");
|
||||
} else {
|
||||
println!("App update event emitted successfully");
|
||||
}
|
||||
}
|
||||
Ok(None) => {
|
||||
println!("No app updates available");
|
||||
@@ -255,6 +261,7 @@ pub fn run() {
|
||||
remove_auto_update_download,
|
||||
is_auto_update_download,
|
||||
check_for_app_updates,
|
||||
check_for_app_updates_manual,
|
||||
download_and_install_app_update,
|
||||
get_app_version_info,
|
||||
])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Donut Browser",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.3",
|
||||
"identifier": "com.donutbrowser",
|
||||
"build": {
|
||||
"beforeDevCommand": "pnpm dev",
|
||||
|
||||
@@ -56,6 +56,7 @@ export default function Home() {
|
||||
|
||||
// App auto-update functionality
|
||||
const appUpdateNotifications = useAppUpdateNotifications();
|
||||
const { checkForAppUpdatesManual } = appUpdateNotifications;
|
||||
|
||||
// Ensure we're on the client side to prevent hydration mismatches
|
||||
useEffect(() => {
|
||||
|
||||
@@ -27,6 +27,11 @@ function getSystemTheme(): string {
|
||||
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [defaultTheme, setDefaultTheme] = useState<string>("system");
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const loadTheme = async () => {
|
||||
@@ -65,11 +70,18 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
}, []);
|
||||
|
||||
if (isLoading) {
|
||||
// Detect system theme to show appropriate loading screen
|
||||
const systemTheme = getSystemTheme();
|
||||
const loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
||||
const spinnerColor =
|
||||
systemTheme === "dark" ? "border-white" : "border-gray-900";
|
||||
// Use a consistent loading screen that doesn't depend on system theme during SSR
|
||||
// This prevents hydration mismatch by ensuring server and client render the same initially
|
||||
let loadingBgColor = "bg-white";
|
||||
let spinnerColor = "border-gray-900";
|
||||
|
||||
// Only apply system theme detection after component is mounted (client-side only)
|
||||
if (mounted) {
|
||||
const systemTheme = getSystemTheme();
|
||||
loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
||||
spinnerColor =
|
||||
systemTheme === "dark" ? "border-white" : "border-gray-900";
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
|
||||
@@ -13,6 +13,7 @@ export function useAppUpdateNotifications() {
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [updateProgress, setUpdateProgress] = useState<string>("");
|
||||
const [isClient, setIsClient] = useState(false);
|
||||
const [dismissedVersion, setDismissedVersion] = useState<string | null>(null);
|
||||
|
||||
// Ensure we're on the client side to prevent hydration mismatches
|
||||
useEffect(() => {
|
||||
@@ -26,10 +27,33 @@ export function useAppUpdateNotifications() {
|
||||
const update = await invoke<AppUpdateInfo | null>(
|
||||
"check_for_app_updates",
|
||||
);
|
||||
setUpdateInfo(update);
|
||||
|
||||
// Don't show update if this version was already dismissed
|
||||
if (update && update.new_version !== dismissedVersion) {
|
||||
setUpdateInfo(update);
|
||||
} else if (update) {
|
||||
console.log("Update available but dismissed:", update.new_version);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check for app updates:", error);
|
||||
}
|
||||
}, [isClient, dismissedVersion]);
|
||||
|
||||
const checkForAppUpdatesManual = useCallback(async () => {
|
||||
if (!isClient) return;
|
||||
|
||||
try {
|
||||
console.log("Triggering manual app update check...");
|
||||
const update = await invoke<AppUpdateInfo | null>(
|
||||
"check_for_app_updates_manual",
|
||||
);
|
||||
console.log("Manual check result:", update);
|
||||
|
||||
// Always show manual check results, even if previously dismissed
|
||||
setUpdateInfo(update);
|
||||
} catch (error) {
|
||||
console.error("Failed to manually check for app updates:", error);
|
||||
}
|
||||
}, [isClient]);
|
||||
|
||||
const handleAppUpdate = useCallback(async (appUpdateInfo: AppUpdateInfo) => {
|
||||
@@ -56,9 +80,15 @@ export function useAppUpdateNotifications() {
|
||||
const dismissAppUpdate = useCallback(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
// Remember the dismissed version so we don't show it again
|
||||
if (updateInfo) {
|
||||
setDismissedVersion(updateInfo.new_version);
|
||||
console.log("Dismissed app update version:", updateInfo.new_version);
|
||||
}
|
||||
|
||||
setUpdateInfo(null);
|
||||
toast.dismiss("app-update");
|
||||
}, [isClient]);
|
||||
}, [isClient, updateInfo]);
|
||||
|
||||
// Listen for app update availability
|
||||
useEffect(() => {
|
||||
@@ -116,10 +146,19 @@ export function useAppUpdateNotifications() {
|
||||
isClient,
|
||||
]);
|
||||
|
||||
// Check for app updates on startup
|
||||
useEffect(() => {
|
||||
if (!isClient) return;
|
||||
|
||||
// Check for updates immediately on startup
|
||||
void checkForAppUpdates();
|
||||
}, [isClient, checkForAppUpdates]);
|
||||
|
||||
return {
|
||||
updateInfo,
|
||||
isUpdating,
|
||||
checkForAppUpdates,
|
||||
checkForAppUpdatesManual,
|
||||
dismissAppUpdate,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user