mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-05-02 00:25:11 +02:00
style: only allow user to switch between releases
This commit is contained in:
@@ -509,6 +509,7 @@ mod tests {
|
||||
process_id: None,
|
||||
proxy: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,12 @@ pub struct BrowserProfile {
|
||||
pub process_id: Option<u32>,
|
||||
#[serde(default)]
|
||||
pub last_launch: Option<u64>,
|
||||
#[serde(default = "default_release_type")]
|
||||
pub release_type: String, // "stable" or "nightly"
|
||||
}
|
||||
|
||||
fn default_release_type() -> String {
|
||||
"stable".to_string()
|
||||
}
|
||||
|
||||
// Platform-specific modules
|
||||
@@ -1049,6 +1055,7 @@ impl BrowserRunner {
|
||||
name: &str,
|
||||
browser: &str,
|
||||
version: &str,
|
||||
release_type: &str,
|
||||
proxy: Option<ProxySettings>,
|
||||
) -> Result<BrowserProfile, Box<dyn std::error::Error>> {
|
||||
// Check if a profile with this name already exists (case insensitive)
|
||||
@@ -1075,6 +1082,7 @@ impl BrowserRunner {
|
||||
proxy: proxy.clone(),
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: release_type.to_string(),
|
||||
};
|
||||
|
||||
// Save profile info
|
||||
@@ -1245,6 +1253,14 @@ impl BrowserRunner {
|
||||
// Update version
|
||||
profile.version = version.to_string();
|
||||
|
||||
// Update the release_type based on the version and browser
|
||||
profile.release_type =
|
||||
if crate::api_client::is_browser_version_nightly(&profile.browser, version, None) {
|
||||
"nightly".to_string()
|
||||
} else {
|
||||
"stable".to_string()
|
||||
};
|
||||
|
||||
// Save the updated profile
|
||||
self.save_profile(&profile)?;
|
||||
|
||||
@@ -2195,11 +2211,12 @@ pub fn create_browser_profile(
|
||||
name: String,
|
||||
browser: String,
|
||||
version: String,
|
||||
release_type: String,
|
||||
proxy: Option<ProxySettings>,
|
||||
) -> Result<BrowserProfile, String> {
|
||||
let browser_runner = BrowserRunner::new();
|
||||
browser_runner
|
||||
.create_profile(&name, &browser, &version, proxy)
|
||||
.create_profile(&name, &browser, &version, &release_type, proxy)
|
||||
.map_err(|e| format!("Failed to create profile: {e}"))
|
||||
}
|
||||
|
||||
@@ -2638,11 +2655,18 @@ pub fn create_browser_profile_new(
|
||||
name: String,
|
||||
browser_str: String,
|
||||
version: String,
|
||||
release_type: String,
|
||||
proxy: Option<ProxySettings>,
|
||||
) -> Result<BrowserProfile, String> {
|
||||
let browser_type =
|
||||
BrowserType::from_str(&browser_str).map_err(|e| format!("Invalid browser type: {e}"))?;
|
||||
create_browser_profile(name, browser_type.as_str().to_string(), version, proxy)
|
||||
create_browser_profile(
|
||||
name,
|
||||
browser_type.as_str().to_string(),
|
||||
version,
|
||||
release_type,
|
||||
proxy,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
@@ -2663,6 +2687,17 @@ pub fn get_downloaded_browser_versions(browser_str: String) -> Result<Vec<String
|
||||
Ok(registry.get_downloaded_versions(&browser_str))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn get_browser_release_types(
|
||||
browser_str: String,
|
||||
) -> Result<crate::browser_version_service::BrowserReleaseTypes, String> {
|
||||
let service = BrowserVersionService::new();
|
||||
service
|
||||
.get_browser_release_types(&browser_str)
|
||||
.await
|
||||
.map_err(|e| format!("Failed to get browser release types: {e}"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -2708,7 +2743,7 @@ mod tests {
|
||||
let (runner, _temp_dir) = create_test_browser_runner();
|
||||
|
||||
let profile = runner
|
||||
.create_profile("Test Profile", "firefox", "139.0", None)
|
||||
.create_profile("Test Profile", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(profile.name, "Test Profile");
|
||||
@@ -2736,6 +2771,7 @@ mod tests {
|
||||
"Test Profile with Proxy",
|
||||
"firefox",
|
||||
"139.0",
|
||||
"stable",
|
||||
Some(proxy.clone()),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -2753,7 +2789,7 @@ mod tests {
|
||||
let (runner, _temp_dir) = create_test_browser_runner();
|
||||
|
||||
let profile = runner
|
||||
.create_profile("Test Save Load", "firefox", "139.0", None)
|
||||
.create_profile("Test Save Load", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// Save the profile
|
||||
@@ -2773,7 +2809,7 @@ mod tests {
|
||||
|
||||
// Create profile
|
||||
let _ = runner
|
||||
.create_profile("Original Name", "firefox", "139.0", None)
|
||||
.create_profile("Original Name", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// Rename profile
|
||||
@@ -2793,7 +2829,7 @@ mod tests {
|
||||
|
||||
// Create profile
|
||||
let _ = runner
|
||||
.create_profile("To Delete", "firefox", "139.0", None)
|
||||
.create_profile("To Delete", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// Verify profile exists
|
||||
@@ -2814,7 +2850,13 @@ mod tests {
|
||||
|
||||
// Create profile with spaces and special characters
|
||||
let profile = runner
|
||||
.create_profile("Test Profile With Spaces", "firefox", "139.0", None)
|
||||
.create_profile(
|
||||
"Test Profile With Spaces",
|
||||
"firefox",
|
||||
"139.0",
|
||||
"stable",
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Profile path should use snake_case
|
||||
@@ -2827,13 +2869,13 @@ mod tests {
|
||||
|
||||
// Create multiple profiles
|
||||
let _ = runner
|
||||
.create_profile("Profile 1", "firefox", "139.0", None)
|
||||
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
let _ = runner
|
||||
.create_profile("Profile 2", "chromium", "1465660", None)
|
||||
.create_profile("Profile 2", "chromium", "1465660", "stable", None)
|
||||
.unwrap();
|
||||
let _ = runner
|
||||
.create_profile("Profile 3", "brave", "v1.81.9", None)
|
||||
.create_profile("Profile 3", "brave", "v1.81.9", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// List profiles
|
||||
@@ -2852,10 +2894,10 @@ mod tests {
|
||||
|
||||
// Test that we can't rename to an existing profile name
|
||||
let _ = runner
|
||||
.create_profile("Profile 1", "firefox", "139.0", None)
|
||||
.create_profile("Profile 1", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
let _ = runner
|
||||
.create_profile("Profile 2", "firefox", "139.0", None)
|
||||
.create_profile("Profile 2", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// Try to rename profile2 to profile1's name (should fail)
|
||||
@@ -2870,7 +2912,7 @@ mod tests {
|
||||
|
||||
// Create profile without proxy
|
||||
let profile = runner
|
||||
.create_profile("Test Firefox Prefs", "firefox", "139.0", None)
|
||||
.create_profile("Test Firefox Prefs", "firefox", "139.0", "stable", None)
|
||||
.unwrap();
|
||||
|
||||
// Check that user.js file was created with default browser preference
|
||||
@@ -2896,7 +2938,13 @@ mod tests {
|
||||
};
|
||||
|
||||
let profile_with_proxy = runner
|
||||
.create_profile("Test Firefox Prefs Proxy", "firefox", "139.0", Some(proxy))
|
||||
.create_profile(
|
||||
"Test Firefox Prefs Proxy",
|
||||
"firefox",
|
||||
"139.0",
|
||||
"stable",
|
||||
Some(proxy),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Check that user.js file contains both proxy settings and default browser preference
|
||||
|
||||
@@ -17,6 +17,12 @@ pub struct BrowserVersionsResult {
|
||||
pub total_versions_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BrowserReleaseTypes {
|
||||
pub stable: Option<String>,
|
||||
pub nightly: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct DownloadInfo {
|
||||
pub url: String,
|
||||
@@ -136,6 +142,39 @@ impl BrowserVersionService {
|
||||
self.api_client.is_cache_expired(browser)
|
||||
}
|
||||
|
||||
/// Get latest stable and nightly versions for a browser
|
||||
pub async fn get_browser_release_types(
|
||||
&self,
|
||||
browser: &str,
|
||||
) -> Result<BrowserReleaseTypes, Box<dyn std::error::Error + Send + Sync>> {
|
||||
// For Chromium, only return stable since all releases are stable
|
||||
if browser == "chromium" {
|
||||
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
|
||||
let latest_stable = detailed_versions.first().map(|v| v.version.clone());
|
||||
return Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: None,
|
||||
});
|
||||
}
|
||||
|
||||
let detailed_versions = self.fetch_browser_versions_detailed(browser, false).await?;
|
||||
|
||||
let latest_stable = detailed_versions
|
||||
.iter()
|
||||
.find(|v| !v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
let latest_nightly = detailed_versions
|
||||
.iter()
|
||||
.find(|v| v.is_prerelease)
|
||||
.map(|v| v.version.clone());
|
||||
|
||||
Ok(BrowserReleaseTypes {
|
||||
stable: latest_stable,
|
||||
nightly: latest_nightly,
|
||||
})
|
||||
}
|
||||
|
||||
/// Fetch browser versions with optional caching
|
||||
pub async fn fetch_browser_versions(
|
||||
&self,
|
||||
|
||||
@@ -28,10 +28,10 @@ extern crate lazy_static;
|
||||
use browser_runner::{
|
||||
check_browser_exists, check_browser_status, create_browser_profile_new, delete_profile,
|
||||
download_browser, fetch_browser_versions_cached_first, fetch_browser_versions_with_count,
|
||||
fetch_browser_versions_with_count_cached_first, get_downloaded_browser_versions,
|
||||
get_supported_browsers, is_browser_supported_on_platform, kill_browser_profile,
|
||||
launch_browser_profile, list_browser_profiles, rename_profile, update_profile_proxy,
|
||||
update_profile_version,
|
||||
fetch_browser_versions_with_count_cached_first, get_browser_release_types,
|
||||
get_downloaded_browser_versions, get_supported_browsers, is_browser_supported_on_platform,
|
||||
kill_browser_profile, launch_browser_profile, list_browser_profiles, rename_profile,
|
||||
update_profile_proxy, update_profile_version,
|
||||
};
|
||||
|
||||
use settings_manager::{
|
||||
@@ -331,6 +331,7 @@ pub fn run() {
|
||||
fetch_browser_versions_cached_first,
|
||||
fetch_browser_versions_with_count_cached_first,
|
||||
get_downloaded_browser_versions,
|
||||
get_browser_release_types,
|
||||
update_profile_proxy,
|
||||
update_profile_version,
|
||||
check_browser_status,
|
||||
|
||||
@@ -686,6 +686,7 @@ impl ProfileImporter {
|
||||
proxy: None,
|
||||
process_id: None,
|
||||
last_launch: None,
|
||||
release_type: "stable".to_string(),
|
||||
};
|
||||
|
||||
// Save the profile metadata
|
||||
|
||||
+4
-2
@@ -301,6 +301,7 @@ export default function Home() {
|
||||
name: string;
|
||||
browserStr: BrowserTypeString;
|
||||
version: string;
|
||||
releaseType: string;
|
||||
proxy?: ProxySettings;
|
||||
}) => {
|
||||
setError(null);
|
||||
@@ -312,6 +313,7 @@ export default function Home() {
|
||||
name: profileData.name,
|
||||
browserStr: profileData.browserStr,
|
||||
version: profileData.version,
|
||||
releaseType: profileData.releaseType,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -474,7 +476,7 @@ export default function Home() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 gap-8 sm:p-12 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen gap-8 font-[family-name:var(--font-geist-sans)] bg-white dark:bg-black">
|
||||
<main className="flex flex-col row-start-2 gap-8 items-center w-full max-w-3xl">
|
||||
<Card className="w-full">
|
||||
<CardHeader>
|
||||
@@ -527,7 +529,7 @@ export default function Home() {
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<CardContent>
|
||||
<ProfilesDataTable
|
||||
data={profiles}
|
||||
onLaunchProfile={launchProfile}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
@@ -12,9 +13,8 @@ import {
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { VersionSelector } from "@/components/version-selector";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import type { BrowserProfile } from "@/types";
|
||||
import type { BrowserProfile, BrowserReleaseTypes } from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LuTriangleAlert } from "react-icons/lu";
|
||||
@@ -32,16 +32,18 @@ export function ChangeVersionDialog({
|
||||
profile,
|
||||
onVersionChanged,
|
||||
}: ChangeVersionDialogProps) {
|
||||
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
||||
const [selectedReleaseType, setSelectedReleaseType] = useState<
|
||||
"stable" | "nightly" | null
|
||||
>(null);
|
||||
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({});
|
||||
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [showDowngradeWarning, setShowDowngradeWarning] = useState(false);
|
||||
const [acknowledgeDowngrade, setAcknowledgeDowngrade] = useState(false);
|
||||
|
||||
const {
|
||||
availableVersions,
|
||||
downloadedVersions,
|
||||
isDownloading,
|
||||
loadVersions,
|
||||
loadDownloadedVersions,
|
||||
downloadBrowser,
|
||||
isVersionDownloaded,
|
||||
@@ -49,49 +51,73 @@ export function ChangeVersionDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && profile) {
|
||||
setSelectedVersion(profile.version);
|
||||
// Set current release type based on profile
|
||||
setSelectedReleaseType(profile.release_type as "stable" | "nightly");
|
||||
setAcknowledgeDowngrade(false);
|
||||
void loadVersions(profile.browser);
|
||||
void loadReleaseTypes(profile.browser);
|
||||
void loadDownloadedVersions(profile.browser);
|
||||
}
|
||||
}, [isOpen, profile, loadVersions, loadDownloadedVersions]);
|
||||
}, [isOpen, profile, loadDownloadedVersions]);
|
||||
|
||||
const loadReleaseTypes = async (browser: string) => {
|
||||
setIsLoadingReleaseTypes(true);
|
||||
try {
|
||||
const releaseTypes = await invoke<BrowserReleaseTypes>(
|
||||
"get_browser_release_types",
|
||||
{ browserStr: browser },
|
||||
);
|
||||
setReleaseTypes(releaseTypes);
|
||||
} catch (error) {
|
||||
console.error("Failed to load release types:", error);
|
||||
} finally {
|
||||
setIsLoadingReleaseTypes(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (profile && selectedVersion) {
|
||||
// Check if this is a downgrade
|
||||
const currentVersionIndex = availableVersions.findIndex(
|
||||
(v) => v.tag_name === profile.version,
|
||||
);
|
||||
const selectedVersionIndex = availableVersions.findIndex(
|
||||
(v) => v.tag_name === selectedVersion,
|
||||
);
|
||||
|
||||
// If selected version has a higher index, it's older (downgrade)
|
||||
if (
|
||||
profile &&
|
||||
selectedReleaseType &&
|
||||
selectedReleaseType !== profile.release_type
|
||||
) {
|
||||
// For simplicity, we'll show downgrade warning when switching from stable to nightly
|
||||
// since nightly versions might be considered "downgrades" in terms of stability
|
||||
const isDowngrade =
|
||||
currentVersionIndex !== -1 &&
|
||||
selectedVersionIndex !== -1 &&
|
||||
selectedVersionIndex > currentVersionIndex;
|
||||
profile.release_type === "stable" && selectedReleaseType === "nightly";
|
||||
setShowDowngradeWarning(isDowngrade);
|
||||
|
||||
if (!isDowngrade) {
|
||||
setAcknowledgeDowngrade(false);
|
||||
}
|
||||
}
|
||||
}, [selectedVersion, profile, availableVersions]);
|
||||
}, [selectedReleaseType, profile]);
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!profile || !selectedVersion) return;
|
||||
await downloadBrowser(profile.browser, selectedVersion);
|
||||
if (!profile || !selectedReleaseType) return;
|
||||
|
||||
const version =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
if (!version) return;
|
||||
|
||||
await downloadBrowser(profile.browser, version);
|
||||
};
|
||||
|
||||
const handleVersionChange = async () => {
|
||||
if (!profile || !selectedVersion) return;
|
||||
if (!profile || !selectedReleaseType) return;
|
||||
|
||||
const version =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
if (!version) return;
|
||||
|
||||
setIsUpdating(true);
|
||||
try {
|
||||
await invoke("update_profile_version", {
|
||||
profileName: profile.name,
|
||||
version: selectedVersion,
|
||||
version,
|
||||
});
|
||||
onVersionChanged();
|
||||
onClose();
|
||||
@@ -102,10 +128,15 @@ export function ChangeVersionDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const selectedVersion =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
|
||||
const canUpdate =
|
||||
profile &&
|
||||
selectedVersion &&
|
||||
selectedVersion !== profile.version &&
|
||||
selectedReleaseType &&
|
||||
selectedReleaseType !== profile.release_type &&
|
||||
selectedVersion &&
|
||||
isVersionDownloaded(selectedVersion) &&
|
||||
(!showDowngradeWarning || acknowledgeDowngrade);
|
||||
@@ -116,7 +147,7 @@ export function ChangeVersionDialog({
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Change Browser Version</DialogTitle>
|
||||
<DialogTitle>Change Release Type</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-4 py-4">
|
||||
@@ -126,26 +157,33 @@ export function ChangeVersionDialog({
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label className="text-sm font-medium">Current Version:</Label>
|
||||
<div className="p-2 bg-muted rounded text-sm">
|
||||
{profile.version}
|
||||
<Label className="text-sm font-medium">Current Release:</Label>
|
||||
<div className="p-2 bg-muted rounded text-sm capitalize">
|
||||
{profile.release_type} ({profile.version})
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Version Selection */}
|
||||
{/* Release Type Selection */}
|
||||
<div className="grid gap-2">
|
||||
<Label>New Version</Label>
|
||||
<VersionSelector
|
||||
selectedVersion={selectedVersion}
|
||||
onVersionSelect={setSelectedVersion}
|
||||
availableVersions={availableVersions}
|
||||
downloadedVersions={downloadedVersions}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select version..."
|
||||
/>
|
||||
<Label>New Release Type</Label>
|
||||
{isLoadingReleaseTypes ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading release types...
|
||||
</div>
|
||||
) : (
|
||||
<ReleaseTypeSelector
|
||||
selectedReleaseType={selectedReleaseType}
|
||||
onReleaseTypeSelect={setSelectedReleaseType}
|
||||
availableReleaseTypes={releaseTypes}
|
||||
browser={profile.browser}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select release type..."
|
||||
downloadedVersions={downloadedVersions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Downgrade Warning */}
|
||||
@@ -153,12 +191,12 @@ export function ChangeVersionDialog({
|
||||
<Alert className="border-orange-700">
|
||||
<LuTriangleAlert className="h-4 w-4 text-orange-700" />
|
||||
<AlertTitle className="text-orange-700">
|
||||
Downgrade Warning
|
||||
Stability Warning
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-orange-700">
|
||||
You are about to downgrade from version {profile.version} to{" "}
|
||||
{selectedVersion}. This may lead to compatibility issues, data
|
||||
loss, or unexpected behavior.
|
||||
You are about to switch from stable to nightly releases. Nightly
|
||||
versions may be less stable and could contain bugs or incomplete
|
||||
features.
|
||||
<div className="flex items-center space-x-2 mt-3">
|
||||
<Checkbox
|
||||
id="acknowledge-downgrade"
|
||||
@@ -187,7 +225,7 @@ export function ChangeVersionDialog({
|
||||
}}
|
||||
disabled={!canUpdate}
|
||||
>
|
||||
{isUpdating ? "Updating..." : "Update Version"}
|
||||
{isUpdating ? "Updating..." : "Update Release Type"}
|
||||
</LoadingButton>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { ReleaseTypeSelector } from "@/components/release-type-selector";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import {
|
||||
@@ -24,11 +25,14 @@ import {
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { VersionSelector } from "@/components/version-selector";
|
||||
import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { useBrowserSupport } from "@/hooks/use-browser-support";
|
||||
import { getBrowserDisplayName } from "@/lib/browser-utils";
|
||||
import type { BrowserProfile, ProxySettings } from "@/types";
|
||||
import type {
|
||||
BrowserProfile,
|
||||
BrowserReleaseTypes,
|
||||
ProxySettings,
|
||||
} from "@/types";
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
@@ -49,6 +53,7 @@ interface CreateProfileDialogProps {
|
||||
name: string;
|
||||
browserStr: BrowserTypeString;
|
||||
version: string;
|
||||
releaseType: string;
|
||||
proxy?: ProxySettings;
|
||||
}) => Promise<void>;
|
||||
}
|
||||
@@ -61,11 +66,18 @@ export function CreateProfileDialog({
|
||||
const [profileName, setProfileName] = useState("");
|
||||
const [selectedBrowser, setSelectedBrowser] =
|
||||
useState<BrowserTypeString | null>("mullvad-browser");
|
||||
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
||||
const [selectedReleaseType, setSelectedReleaseType] = useState<
|
||||
"stable" | "nightly" | null
|
||||
>(null);
|
||||
const [releaseTypes, setReleaseTypes] = useState<BrowserReleaseTypes>({
|
||||
stable: undefined,
|
||||
nightly: undefined,
|
||||
});
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
|
||||
[],
|
||||
);
|
||||
const [isLoadingReleaseTypes, setIsLoadingReleaseTypes] = useState(false);
|
||||
|
||||
// Proxy settings
|
||||
const [proxyEnabled, setProxyEnabled] = useState(false);
|
||||
@@ -76,13 +88,10 @@ export function CreateProfileDialog({
|
||||
const [proxyPassword, setProxyPassword] = useState("");
|
||||
|
||||
const {
|
||||
availableVersions,
|
||||
downloadedVersions,
|
||||
isDownloading,
|
||||
loadVersions,
|
||||
loadDownloadedVersions,
|
||||
downloadBrowser,
|
||||
isVersionDownloaded,
|
||||
isDownloading,
|
||||
downloadedVersions,
|
||||
loadDownloadedVersions,
|
||||
} = useBrowserDownload();
|
||||
|
||||
const {
|
||||
@@ -110,29 +119,26 @@ export function CreateProfileDialog({
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && selectedBrowser) {
|
||||
// Reset selected version when browser changes
|
||||
setSelectedVersion(null);
|
||||
void loadVersions(selectedBrowser);
|
||||
// Reset selected release type when browser changes
|
||||
setSelectedReleaseType(null);
|
||||
void loadReleaseTypes(selectedBrowser);
|
||||
void loadDownloadedVersions(selectedBrowser);
|
||||
}
|
||||
}, [isOpen, selectedBrowser, loadVersions, loadDownloadedVersions]);
|
||||
}, [isOpen, selectedBrowser, loadDownloadedVersions]);
|
||||
|
||||
// Set default version when versions are loaded and no version is selected
|
||||
// Set default release type when release types are loaded
|
||||
useEffect(() => {
|
||||
if (availableVersions.length > 0 && selectedBrowser) {
|
||||
// Always reset version when browser changes or versions are loaded
|
||||
// Find the latest stable version (not alpha/beta)
|
||||
const stableVersions = availableVersions.filter((v) => !v.is_nightly);
|
||||
|
||||
if (stableVersions.length > 0) {
|
||||
// Select the first stable version (they're already sorted newest first)
|
||||
setSelectedVersion(stableVersions[0].tag_name);
|
||||
} else if (availableVersions.length > 0) {
|
||||
// If no stable version found, select the first available version
|
||||
setSelectedVersion(availableVersions[0].tag_name);
|
||||
if (!selectedReleaseType && Object.keys(releaseTypes).length > 0) {
|
||||
// First try to set stable if it exists
|
||||
if (releaseTypes.stable) {
|
||||
setSelectedReleaseType("stable");
|
||||
}
|
||||
// If stable doesn't exist but nightly does, set nightly as default
|
||||
else if (releaseTypes.nightly && selectedBrowser !== "chromium") {
|
||||
setSelectedReleaseType("nightly");
|
||||
}
|
||||
}
|
||||
}, [availableVersions, selectedBrowser]);
|
||||
}, [releaseTypes, selectedReleaseType, selectedBrowser]);
|
||||
|
||||
const loadExistingProfiles = async () => {
|
||||
try {
|
||||
@@ -143,9 +149,34 @@ export function CreateProfileDialog({
|
||||
}
|
||||
};
|
||||
|
||||
const loadReleaseTypes = async (browser: string) => {
|
||||
try {
|
||||
setIsLoadingReleaseTypes(true);
|
||||
const types = await invoke<BrowserReleaseTypes>(
|
||||
"get_browser_release_types",
|
||||
{
|
||||
browserStr: browser,
|
||||
},
|
||||
);
|
||||
setReleaseTypes(types);
|
||||
} catch (error) {
|
||||
console.error("Failed to load release types:", error);
|
||||
toast.error("Failed to load available versions");
|
||||
} finally {
|
||||
setIsLoadingReleaseTypes(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDownload = async () => {
|
||||
if (!selectedBrowser || !selectedVersion) return;
|
||||
await downloadBrowser(selectedBrowser, selectedVersion);
|
||||
if (!selectedBrowser || !selectedReleaseType) return;
|
||||
|
||||
const version =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
if (!version) return;
|
||||
|
||||
await downloadBrowser(selectedBrowser, version);
|
||||
};
|
||||
|
||||
const validateProfileName = (name: string): string | null => {
|
||||
@@ -178,7 +209,7 @@ export function CreateProfileDialog({
|
||||
}, [selectedBrowser, proxyEnabled]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
if (!profileName.trim() || !selectedBrowser || !selectedVersion) return;
|
||||
if (!profileName.trim() || !selectedBrowser || !selectedReleaseType) return;
|
||||
|
||||
// Validate profile name
|
||||
const nameError = validateProfileName(profileName);
|
||||
@@ -187,6 +218,15 @@ export function CreateProfileDialog({
|
||||
return;
|
||||
}
|
||||
|
||||
const version =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
if (!version) {
|
||||
toast.error("Selected release type is not available");
|
||||
return;
|
||||
}
|
||||
|
||||
setIsCreating(true);
|
||||
try {
|
||||
const proxy =
|
||||
@@ -204,13 +244,14 @@ export function CreateProfileDialog({
|
||||
await onCreateProfile({
|
||||
name: profileName.trim(),
|
||||
browserStr: selectedBrowser,
|
||||
version: selectedVersion,
|
||||
version,
|
||||
releaseType: selectedReleaseType,
|
||||
proxy,
|
||||
});
|
||||
|
||||
// Reset form
|
||||
setProfileName("");
|
||||
setSelectedVersion(null);
|
||||
setSelectedReleaseType(null);
|
||||
setProxyEnabled(false);
|
||||
setProxyHost("");
|
||||
setProxyPort(8080);
|
||||
@@ -227,11 +268,17 @@ export function CreateProfileDialog({
|
||||
const nameError = profileName.trim()
|
||||
? validateProfileName(profileName)
|
||||
: null;
|
||||
|
||||
const selectedVersion =
|
||||
selectedReleaseType === "stable"
|
||||
? releaseTypes.stable
|
||||
: releaseTypes.nightly;
|
||||
|
||||
const canCreate =
|
||||
profileName.trim() &&
|
||||
selectedBrowser &&
|
||||
selectedReleaseType &&
|
||||
selectedVersion &&
|
||||
isVersionDownloaded(selectedVersion) &&
|
||||
(!proxyEnabled || isProxyDisabled || (proxyHost && proxyPort)) &&
|
||||
!nameError;
|
||||
|
||||
@@ -322,20 +369,27 @@ export function CreateProfileDialog({
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Version Selection */}
|
||||
{/* Release Type Selection */}
|
||||
<div className="grid gap-2">
|
||||
<Label>Version</Label>
|
||||
<VersionSelector
|
||||
selectedVersion={selectedVersion}
|
||||
onVersionSelect={setSelectedVersion}
|
||||
availableVersions={availableVersions}
|
||||
downloadedVersions={downloadedVersions}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select version..."
|
||||
/>
|
||||
<Label>Release Type</Label>
|
||||
{isLoadingReleaseTypes ? (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
Loading release types...
|
||||
</div>
|
||||
) : (
|
||||
<ReleaseTypeSelector
|
||||
selectedReleaseType={selectedReleaseType}
|
||||
onReleaseTypeSelect={setSelectedReleaseType}
|
||||
availableReleaseTypes={releaseTypes}
|
||||
browser={selectedBrowser ?? ""}
|
||||
isDownloading={isDownloading}
|
||||
onDownload={() => {
|
||||
void handleDownload();
|
||||
}}
|
||||
placeholder="Select release type..."
|
||||
downloadedVersions={downloadedVersions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Proxy Settings */}
|
||||
|
||||
@@ -260,10 +260,21 @@ export function ProfilesDataTable({
|
||||
cell: ({ row }) => {
|
||||
const browser: string = row.getValue("browser");
|
||||
const IconComponent = getBrowserIcon(browser);
|
||||
return (
|
||||
const browserDisplayName = getBrowserDisplayName(browser);
|
||||
return browserDisplayName.length > 15 ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex gap-2 items-center">
|
||||
{IconComponent && <IconComponent className="w-4 h-4" />}
|
||||
<span>{browserDisplayName.slice(0, 15)}...</span>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{browserDisplayName}</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<div className="flex gap-2 items-center">
|
||||
{IconComponent && <IconComponent className="w-4 h-4" />}
|
||||
<span>{getBrowserDisplayName(browser)}</span>
|
||||
<span>{browserDisplayName}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -274,6 +285,36 @@ export function ProfilesDataTable({
|
||||
return browserA.localeCompare(browserB);
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: "release_type",
|
||||
header: "Release",
|
||||
cell: ({ row }) => {
|
||||
const releaseType: string = row.getValue("release_type");
|
||||
const isNightly = releaseType === "nightly";
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className={`inline-flex items-center px-2 py-1 rounded-full text-xs font-medium ${
|
||||
isNightly
|
||||
? "text-yellow-800 bg-yellow-100 dark:bg-yellow-900 dark:text-yellow-200"
|
||||
: "text-green-800 bg-green-100 dark:bg-green-900 dark:text-green-200"
|
||||
}`}
|
||||
>
|
||||
{isNightly ? "Nightly" : "Stable"}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
enableSorting: true,
|
||||
sortingFn: (rowA, rowB, columnId) => {
|
||||
const releaseA: string = rowA.getValue(columnId);
|
||||
const releaseB: string = rowB.getValue(columnId);
|
||||
// Sort with "stable" before "nightly"
|
||||
if (releaseA === "stable" && releaseB === "nightly") return -1;
|
||||
if (releaseA === "nightly" && releaseB === "stable") return 1;
|
||||
return 0;
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "proxy",
|
||||
header: "Proxy",
|
||||
@@ -346,7 +387,7 @@ export function ProfilesDataTable({
|
||||
}}
|
||||
disabled={!isClient || isRunning || isBrowserUpdating}
|
||||
>
|
||||
Change version
|
||||
Switch Release
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
@@ -527,6 +568,11 @@ export function ProfilesDataTable({
|
||||
setDeleteConfirmationName(e.target.value);
|
||||
setDeleteError(null);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
void handleDelete();
|
||||
}
|
||||
}}
|
||||
placeholder="Type the profile name here"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"use client";
|
||||
|
||||
import { LoadingButton } from "@/components/loading-button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/components/ui/command";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { BrowserReleaseTypes } from "@/types";
|
||||
import { useState } from "react";
|
||||
import { LuDownload } from "react-icons/lu";
|
||||
import { LuCheck, LuChevronsUpDown } from "react-icons/lu";
|
||||
|
||||
interface ReleaseTypeSelectorProps {
|
||||
selectedReleaseType: "stable" | "nightly" | null;
|
||||
onReleaseTypeSelect: (releaseType: "stable" | "nightly" | null) => void;
|
||||
availableReleaseTypes: BrowserReleaseTypes;
|
||||
browser: string;
|
||||
isDownloading: boolean;
|
||||
onDownload: () => void;
|
||||
placeholder?: string;
|
||||
showDownloadButton?: boolean;
|
||||
downloadedVersions?: string[];
|
||||
}
|
||||
|
||||
export function ReleaseTypeSelector({
|
||||
selectedReleaseType,
|
||||
onReleaseTypeSelect,
|
||||
availableReleaseTypes,
|
||||
browser,
|
||||
isDownloading,
|
||||
onDownload,
|
||||
placeholder = "Select release type...",
|
||||
showDownloadButton = true,
|
||||
downloadedVersions = [],
|
||||
}: ReleaseTypeSelectorProps) {
|
||||
const [popoverOpen, setPopoverOpen] = useState(false);
|
||||
|
||||
const releaseOptions = [
|
||||
...(availableReleaseTypes.stable
|
||||
? [{ type: "stable" as const, version: availableReleaseTypes.stable }]
|
||||
: []),
|
||||
...(availableReleaseTypes.nightly && browser !== "chromium"
|
||||
? [{ type: "nightly" as const, version: availableReleaseTypes.nightly }]
|
||||
: []),
|
||||
];
|
||||
|
||||
const selectedDisplayText = selectedReleaseType
|
||||
? selectedReleaseType === "stable"
|
||||
? "Stable"
|
||||
: "Nightly"
|
||||
: placeholder;
|
||||
|
||||
const selectedVersion =
|
||||
selectedReleaseType === "stable"
|
||||
? availableReleaseTypes.stable
|
||||
: selectedReleaseType === "nightly"
|
||||
? availableReleaseTypes.nightly
|
||||
: null;
|
||||
|
||||
const isVersionDownloaded =
|
||||
selectedVersion && downloadedVersions.includes(selectedVersion);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Popover open={popoverOpen} onOpenChange={setPopoverOpen} modal={true}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={popoverOpen}
|
||||
className="justify-between w-full"
|
||||
>
|
||||
{selectedDisplayText}
|
||||
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
<Command>
|
||||
<CommandEmpty>No release types available.</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
{releaseOptions.map((option) => {
|
||||
const isDownloaded = downloadedVersions.includes(
|
||||
option.version,
|
||||
);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.type}
|
||||
value={option.type}
|
||||
onSelect={(currentValue) => {
|
||||
const selectedType = currentValue as
|
||||
| "stable"
|
||||
| "nightly";
|
||||
onReleaseTypeSelect(
|
||||
selectedType === selectedReleaseType
|
||||
? null
|
||||
: selectedType,
|
||||
);
|
||||
setPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<LuCheck
|
||||
className={cn(
|
||||
"mr-2 h-4 w-4",
|
||||
selectedReleaseType === option.type
|
||||
? "opacity-100"
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex gap-2 items-center">
|
||||
<span className="capitalize">{option.type}</span>
|
||||
{option.type === "nightly" && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Nightly
|
||||
</Badge>
|
||||
)}
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{option.version}
|
||||
</Badge>
|
||||
{isDownloaded && (
|
||||
<Badge variant="default" className="text-xs">
|
||||
Downloaded
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
|
||||
{showDownloadButton &&
|
||||
selectedReleaseType &&
|
||||
selectedVersion &&
|
||||
!isVersionDownloaded && (
|
||||
<LoadingButton
|
||||
isLoading={isDownloading}
|
||||
onClick={() => {
|
||||
onDownload();
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<LuDownload className="mr-2 w-4 h-4" />
|
||||
{isDownloading ? "Downloading..." : "Download Browser"}
|
||||
</LoadingButton>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ export interface BrowserProfile {
|
||||
proxy?: ProxySettings;
|
||||
process_id?: number;
|
||||
last_launch?: number;
|
||||
release_type: string; // "stable" or "nightly"
|
||||
}
|
||||
|
||||
export interface DetectedProfile {
|
||||
@@ -29,6 +30,11 @@ export interface DetectedProfile {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface BrowserReleaseTypes {
|
||||
stable?: string;
|
||||
nightly?: string;
|
||||
}
|
||||
|
||||
export interface AppUpdateInfo {
|
||||
current_version: string;
|
||||
new_version: string;
|
||||
|
||||
Reference in New Issue
Block a user