From 821cd4ea82b49ba7064f117c6a9f6eb74e9c0aff Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sat, 14 Jun 2025 21:55:18 +0400 Subject: [PATCH] style: only allow user to switch between releases --- src-tauri/src/auto_updater.rs | 1 + src-tauri/src/browser_runner.rs | 76 +++++++++-- src-tauri/src/browser_version_service.rs | 39 ++++++ src-tauri/src/lib.rs | 9 +- src-tauri/src/profile_importer.rs | 1 + src/app/page.tsx | 6 +- src/components/change-version-dialog.tsx | 138 ++++++++++++------- src/components/create-profile-dialog.tsx | 144 +++++++++++++------- src/components/profile-data-table.tsx | 52 ++++++- src/components/release-type-selector.tsx | 164 +++++++++++++++++++++++ src/types.ts | 6 + 11 files changed, 518 insertions(+), 118 deletions(-) create mode 100644 src/components/release-type-selector.tsx diff --git a/src-tauri/src/auto_updater.rs b/src-tauri/src/auto_updater.rs index 6ef4285..7c104f0 100644 --- a/src-tauri/src/auto_updater.rs +++ b/src-tauri/src/auto_updater.rs @@ -509,6 +509,7 @@ mod tests { process_id: None, proxy: None, last_launch: None, + release_type: "stable".to_string(), } } diff --git a/src-tauri/src/browser_runner.rs b/src-tauri/src/browser_runner.rs index 0ae5a3d..c337b72 100644 --- a/src-tauri/src/browser_runner.rs +++ b/src-tauri/src/browser_runner.rs @@ -27,6 +27,12 @@ pub struct BrowserProfile { pub process_id: Option, #[serde(default)] pub last_launch: Option, + #[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, ) -> Result> { // 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, ) -> Result { 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, ) -> Result { 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 Result { + 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 diff --git a/src-tauri/src/browser_version_service.rs b/src-tauri/src/browser_version_service.rs index 1a91c3d..9cea9f3 100644 --- a/src-tauri/src/browser_version_service.rs +++ b/src-tauri/src/browser_version_service.rs @@ -17,6 +17,12 @@ pub struct BrowserVersionsResult { pub total_versions_count: usize, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct BrowserReleaseTypes { + pub stable: Option, + pub nightly: Option, +} + #[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> { + // 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, diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 9925dee..f0c6218 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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, diff --git a/src-tauri/src/profile_importer.rs b/src-tauri/src/profile_importer.rs index 596cd18..37bd0d7 100644 --- a/src-tauri/src/profile_importer.rs +++ b/src-tauri/src/profile_importer.rs @@ -686,6 +686,7 @@ impl ProfileImporter { proxy: None, process_id: None, last_launch: None, + release_type: "stable".to_string(), }; // Save the profile metadata diff --git a/src/app/page.tsx b/src/app/page.tsx index eb537f6..322cc60 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -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 ( -
+
@@ -527,7 +529,7 @@ export default function Home() {
- + (null); + const [selectedReleaseType, setSelectedReleaseType] = useState< + "stable" | "nightly" | null + >(null); + const [releaseTypes, setReleaseTypes] = useState({}); + 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( + "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({ - Change Browser Version + Change Release Type
@@ -126,26 +157,33 @@ export function ChangeVersionDialog({
- -
- {profile.version} + +
+ {profile.release_type} ({profile.version})
- {/* Version Selection */} + {/* Release Type Selection */}
- - { - void handleDownload(); - }} - placeholder="Select version..." - /> + + {isLoadingReleaseTypes ? ( +
+ Loading release types... +
+ ) : ( + { + void handleDownload(); + }} + placeholder="Select release type..." + downloadedVersions={downloadedVersions} + /> + )}
{/* Downgrade Warning */} @@ -153,12 +191,12 @@ export function ChangeVersionDialog({ - Downgrade Warning + Stability Warning - 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.
- {isUpdating ? "Updating..." : "Update Version"} + {isUpdating ? "Updating..." : "Update Release Type"} diff --git a/src/components/create-profile-dialog.tsx b/src/components/create-profile-dialog.tsx index c2988f7..20b0249 100644 --- a/src/components/create-profile-dialog.tsx +++ b/src/components/create-profile-dialog.tsx @@ -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; } @@ -61,11 +66,18 @@ export function CreateProfileDialog({ const [profileName, setProfileName] = useState(""); const [selectedBrowser, setSelectedBrowser] = useState("mullvad-browser"); - const [selectedVersion, setSelectedVersion] = useState(null); + const [selectedReleaseType, setSelectedReleaseType] = useState< + "stable" | "nightly" | null + >(null); + const [releaseTypes, setReleaseTypes] = useState({ + stable: undefined, + nightly: undefined, + }); const [isCreating, setIsCreating] = useState(false); const [existingProfiles, setExistingProfiles] = useState( [], ); + 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( + "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({
- {/* Version Selection */} + {/* Release Type Selection */}
- - { - void handleDownload(); - }} - placeholder="Select version..." - /> + + {isLoadingReleaseTypes ? ( +
+ Loading release types... +
+ ) : ( + { + void handleDownload(); + }} + placeholder="Select release type..." + downloadedVersions={downloadedVersions} + /> + )}
{/* Proxy Settings */} diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index 609d3a1..b70a3ce 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -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 ? ( + + +
+ {IconComponent && } + {browserDisplayName.slice(0, 15)}... +
+
+ {browserDisplayName} +
+ ) : (
{IconComponent && } - {getBrowserDisplayName(browser)} + {browserDisplayName}
); }, @@ -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 ( +
+ + {isNightly ? "Nightly" : "Stable"} + +
+ ); + }, + 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 { @@ -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" />
diff --git a/src/components/release-type-selector.tsx b/src/components/release-type-selector.tsx new file mode 100644 index 0000000..7492228 --- /dev/null +++ b/src/components/release-type-selector.tsx @@ -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 ( +
+ + + + + + + No release types available. + + + {releaseOptions.map((option) => { + const isDownloaded = downloadedVersions.includes( + option.version, + ); + return ( + { + const selectedType = currentValue as + | "stable" + | "nightly"; + onReleaseTypeSelect( + selectedType === selectedReleaseType + ? null + : selectedType, + ); + setPopoverOpen(false); + }} + > + +
+ {option.type} + {option.type === "nightly" && ( + + Nightly + + )} + + {option.version} + + {isDownloaded && ( + + Downloaded + + )} +
+
+ ); + })} +
+
+
+
+
+ + {showDownloadButton && + selectedReleaseType && + selectedVersion && + !isVersionDownloaded && ( + { + onDownload(); + }} + variant="outline" + className="w-full" + > + + {isDownloading ? "Downloading..." : "Download Browser"} + + )} +
+ ); +} diff --git a/src/types.ts b/src/types.ts index de256b1..7cea563 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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;