style: only allow user to switch between releases

This commit is contained in:
zhom
2025-06-14 21:55:18 +04:00
parent d3a63c37bf
commit 821cd4ea82
11 changed files with 518 additions and 118 deletions
+1
View File
@@ -509,6 +509,7 @@ mod tests {
process_id: None,
proxy: None,
last_launch: None,
release_type: "stable".to_string(),
}
}
+62 -14
View File
@@ -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
+39
View File
@@ -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,
+5 -4
View File
@@ -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,
+1
View File
@@ -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
View File
@@ -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}
+88 -50
View File
@@ -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>
+99 -45
View File
@@ -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 */}
+49 -3
View File
@@ -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>
+164
View File
@@ -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>
);
}
+6
View File
@@ -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;