mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-07 15:33:57 +02:00
feat: linux support preview
This commit is contained in:
@@ -26,6 +26,8 @@ import {
|
||||
} 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 { invoke } from "@tauri-apps/api/core";
|
||||
import { useEffect, useState } from "react";
|
||||
@@ -60,9 +62,6 @@ export function CreateProfileDialog({
|
||||
const [selectedBrowser, setSelectedBrowser] =
|
||||
useState<BrowserTypeString | null>("mullvad-browser");
|
||||
const [selectedVersion, setSelectedVersion] = useState<string | null>(null);
|
||||
const [supportedBrowsers, setSupportedBrowsers] = useState<
|
||||
BrowserTypeString[]
|
||||
>([]);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [existingProfiles, setExistingProfiles] = useState<BrowserProfile[]>(
|
||||
[],
|
||||
@@ -84,13 +83,29 @@ export function CreateProfileDialog({
|
||||
isVersionDownloaded,
|
||||
} = useBrowserDownload();
|
||||
|
||||
const {
|
||||
supportedBrowsers,
|
||||
isLoading: isLoadingSupport,
|
||||
isBrowserSupported,
|
||||
} = useBrowserSupport();
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
void loadSupportedBrowsers();
|
||||
void loadExistingProfiles();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (supportedBrowsers.length > 0) {
|
||||
// Set default browser to first supported browser
|
||||
if (supportedBrowsers.includes("mullvad-browser")) {
|
||||
setSelectedBrowser("mullvad-browser");
|
||||
} else if (supportedBrowsers.length > 0) {
|
||||
setSelectedBrowser(supportedBrowsers[0] as BrowserTypeString);
|
||||
}
|
||||
}
|
||||
}, [supportedBrowsers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && selectedBrowser) {
|
||||
// Reset selected version when browser changes
|
||||
@@ -105,7 +120,7 @@ export function CreateProfileDialog({
|
||||
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_alpha);
|
||||
const stableVersions = availableVersions.filter((v) => !v.is_nightly);
|
||||
|
||||
if (stableVersions.length > 0) {
|
||||
// Select the first stable version (they're already sorted newest first)
|
||||
@@ -117,22 +132,6 @@ export function CreateProfileDialog({
|
||||
}
|
||||
}, [availableVersions, selectedBrowser]);
|
||||
|
||||
const loadSupportedBrowsers = async () => {
|
||||
try {
|
||||
const browsers = await invoke<BrowserTypeString[]>(
|
||||
"get_supported_browsers",
|
||||
);
|
||||
setSupportedBrowsers(browsers);
|
||||
if (browsers.includes("mullvad-browser")) {
|
||||
setSelectedBrowser("mullvad-browser");
|
||||
} else if (browsers.length > 0) {
|
||||
setSelectedBrowser(browsers[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to load supported browsers:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const loadExistingProfiles = async () => {
|
||||
try {
|
||||
const profiles = await invoke<BrowserProfile[]>("list_browser_profiles");
|
||||
@@ -261,21 +260,58 @@ export function CreateProfileDialog({
|
||||
onValueChange={(value) => {
|
||||
setSelectedBrowser(value as BrowserTypeString);
|
||||
}}
|
||||
disabled={isLoadingSupport}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select browser" />
|
||||
<SelectValue
|
||||
placeholder={
|
||||
isLoadingSupport ? "Loading browsers..." : "Select browser"
|
||||
}
|
||||
/>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{supportedBrowsers.map((browser) => (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{browser
|
||||
.split("-")
|
||||
.map(
|
||||
(word) => word.charAt(0).toUpperCase() + word.slice(1),
|
||||
)
|
||||
.join(" ")}
|
||||
</SelectItem>
|
||||
))}
|
||||
{(
|
||||
[
|
||||
"mullvad-browser",
|
||||
"firefox",
|
||||
"firefox-developer",
|
||||
"chromium",
|
||||
"brave",
|
||||
"zen",
|
||||
"tor-browser",
|
||||
] as BrowserTypeString[]
|
||||
).map((browser) => {
|
||||
const isSupported = isBrowserSupported(browser);
|
||||
const displayName = getBrowserDisplayName(browser);
|
||||
|
||||
if (!isSupported) {
|
||||
return (
|
||||
<Tooltip key={browser}>
|
||||
<TooltipTrigger asChild>
|
||||
<SelectItem
|
||||
value={browser}
|
||||
disabled={true}
|
||||
className="opacity-50"
|
||||
>
|
||||
{displayName} (Not supported on this platform)
|
||||
</SelectItem>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>
|
||||
{displayName} is not supported on your current
|
||||
platform or architecture.
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SelectItem key={browser} value={browser}>
|
||||
{displayName}
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,7 @@ interface AppSettings {
|
||||
show_settings_on_startup: boolean;
|
||||
theme: string;
|
||||
auto_updates_enabled: boolean;
|
||||
auto_delete_unused_binaries: boolean;
|
||||
}
|
||||
|
||||
interface SettingsDialogProps {
|
||||
@@ -41,17 +42,21 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
show_settings_on_startup: true,
|
||||
theme: "system",
|
||||
auto_updates_enabled: true,
|
||||
auto_delete_unused_binaries: true,
|
||||
});
|
||||
const [originalSettings, setOriginalSettings] = useState<AppSettings>({
|
||||
set_as_default_browser: false,
|
||||
show_settings_on_startup: true,
|
||||
theme: "system",
|
||||
auto_updates_enabled: true,
|
||||
auto_delete_unused_binaries: true,
|
||||
});
|
||||
const [isDefaultBrowser, setIsDefaultBrowser] = useState(false);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isSettingDefault, setIsSettingDefault] = useState(false);
|
||||
const [isClearingCache, setIsClearingCache] = useState(false);
|
||||
const [isCleaningBinaries, setIsCleaningBinaries] = useState(false);
|
||||
|
||||
const { setTheme } = useTheme();
|
||||
|
||||
@@ -106,6 +111,39 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
}
|
||||
};
|
||||
|
||||
const handleClearCache = async () => {
|
||||
setIsClearingCache(true);
|
||||
try {
|
||||
await invoke("clear_all_version_cache");
|
||||
// Optionally show a success message
|
||||
console.log("Cache cleared successfully");
|
||||
} catch (error) {
|
||||
console.error("Failed to clear cache:", error);
|
||||
} finally {
|
||||
setIsClearingCache(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCleanupBinaries = async () => {
|
||||
setIsCleaningBinaries(true);
|
||||
try {
|
||||
const cleanedUp = await invoke<string[]>("cleanup_unused_binaries");
|
||||
if (cleanedUp.length > 0) {
|
||||
console.log(
|
||||
`Cleaned up ${cleanedUp.length} unused binaries:`,
|
||||
cleanedUp,
|
||||
);
|
||||
// You could show a toast with the results
|
||||
} else {
|
||||
console.log("No unused binaries to clean up");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to cleanup unused binaries:", error);
|
||||
} finally {
|
||||
setIsCleaningBinaries(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setIsSaving(true);
|
||||
try {
|
||||
@@ -130,7 +168,9 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
settings.show_settings_on_startup !==
|
||||
originalSettings.show_settings_on_startup ||
|
||||
settings.theme !== originalSettings.theme ||
|
||||
settings.auto_updates_enabled !== originalSettings.auto_updates_enabled;
|
||||
settings.auto_updates_enabled !== originalSettings.auto_updates_enabled ||
|
||||
settings.auto_delete_unused_binaries !==
|
||||
originalSettings.auto_delete_unused_binaries;
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
@@ -216,9 +256,26 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="auto-delete-binaries"
|
||||
checked={settings.auto_delete_unused_binaries}
|
||||
onCheckedChange={(checked) => {
|
||||
updateSetting(
|
||||
"auto_delete_unused_binaries",
|
||||
checked as boolean,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<Label htmlFor="auto-delete-binaries" className="text-sm">
|
||||
Automatically delete unused browser binaries
|
||||
</Label>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
When enabled, Donut Browser will check for browser updates and
|
||||
notify you when updates are available for your profiles.
|
||||
notify you when updates are available for your profiles. Unused
|
||||
binaries will be automatically deleted to save disk space.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -244,6 +301,45 @@ export function SettingsDialog({ isOpen, onClose }: SettingsDialogProps) {
|
||||
starts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Advanced Section */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base font-medium">Advanced</Label>
|
||||
|
||||
<LoadingButton
|
||||
isLoading={isClearingCache}
|
||||
onClick={() => {
|
||||
void handleClearCache();
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Clear All Version Cache
|
||||
</LoadingButton>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Clear all cached browser version data. This will force a fresh
|
||||
download of version information on the next app restart or manual
|
||||
refresh.
|
||||
</p>
|
||||
|
||||
<LoadingButton
|
||||
isLoading={isCleaningBinaries}
|
||||
onClick={() => {
|
||||
void handleCleanupBinaries();
|
||||
}}
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
Clean Up Unused Binaries
|
||||
</LoadingButton>
|
||||
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Manually remove browser binaries that are not used by any profile.
|
||||
This can help free up disk space. Note: This will run
|
||||
automatically when the setting above is enabled.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter className="flex-shrink-0">
|
||||
|
||||
@@ -9,6 +9,10 @@ interface AppSettings {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface SystemTheme {
|
||||
theme: string;
|
||||
}
|
||||
|
||||
interface CustomThemeProviderProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
@@ -24,6 +28,25 @@ function getSystemTheme(): string {
|
||||
return "light";
|
||||
}
|
||||
|
||||
// Function to get native system theme (fallback to CSS media query)
|
||||
async function getNativeSystemTheme(): Promise<string> {
|
||||
try {
|
||||
const systemTheme = await invoke<SystemTheme>("get_system_theme");
|
||||
if (systemTheme.theme === "dark" || systemTheme.theme === "light") {
|
||||
return systemTheme.theme;
|
||||
}
|
||||
// Fallback to CSS media query if native detection returns "unknown"
|
||||
return getSystemTheme();
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
"Failed to get native system theme, falling back to CSS media query:",
|
||||
error,
|
||||
);
|
||||
// Fallback to CSS media query
|
||||
return getSystemTheme();
|
||||
}
|
||||
}
|
||||
|
||||
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [defaultTheme, setDefaultTheme] = useState<string>("system");
|
||||
@@ -41,7 +64,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
} catch (error) {
|
||||
console.error("Failed to load theme settings:", error);
|
||||
// For first-time users, detect system preference and apply it
|
||||
const systemTheme = getSystemTheme();
|
||||
const systemTheme = await getNativeSystemTheme();
|
||||
console.log(
|
||||
"First-time user detected, applying system theme:",
|
||||
systemTheme,
|
||||
@@ -69,6 +92,50 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
void loadTheme();
|
||||
}, []);
|
||||
|
||||
// Monitor system theme changes when using "system" theme
|
||||
useEffect(() => {
|
||||
if (!mounted || defaultTheme !== "system") {
|
||||
return;
|
||||
}
|
||||
|
||||
const checkSystemTheme = async () => {
|
||||
try {
|
||||
const currentSystemTheme = await getNativeSystemTheme();
|
||||
// Force re-evaluation by toggling the theme
|
||||
const html = document.documentElement;
|
||||
const currentClass = html.className;
|
||||
|
||||
// Apply the system theme class
|
||||
if (currentSystemTheme === "dark") {
|
||||
if (!html.classList.contains("dark")) {
|
||||
html.classList.add("dark");
|
||||
html.classList.remove("light");
|
||||
}
|
||||
} else {
|
||||
if (
|
||||
!html.classList.contains("light") ||
|
||||
html.classList.contains("dark")
|
||||
) {
|
||||
html.classList.add("light");
|
||||
html.classList.remove("dark");
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Failed to check system theme:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Check system theme every 2 seconds when using system theme
|
||||
const intervalId = setInterval(() => void checkSystemTheme(), 2000);
|
||||
|
||||
// Initial check
|
||||
void checkSystemTheme();
|
||||
|
||||
return () => {
|
||||
clearInterval(intervalId);
|
||||
};
|
||||
}, [mounted, defaultTheme]);
|
||||
|
||||
if (isLoading) {
|
||||
// Use a consistent loading screen that doesn't depend on system theme during SSR
|
||||
// This prevents hydration mismatch by ensuring server and client render the same initially
|
||||
@@ -77,6 +144,7 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
|
||||
// Only apply system theme detection after component is mounted (client-side only)
|
||||
if (mounted) {
|
||||
// Use CSS media query for loading screen since async call would complicate this
|
||||
const systemTheme = getSystemTheme();
|
||||
loadingBgColor = systemTheme === "dark" ? "bg-gray-900" : "bg-white";
|
||||
spinnerColor =
|
||||
@@ -85,10 +153,10 @@ export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`fixed inset-0 ${loadingBgColor} flex items-center justify-center`}
|
||||
className={`flex fixed inset-0 justify-center items-center ${loadingBgColor}`}
|
||||
>
|
||||
<div
|
||||
className={`animate-spin rounded-full h-8 w-8 border-2 ${spinnerColor} border-t-transparent`}
|
||||
className={`w-8 h-8 rounded-full border-2 animate-spin ${spinnerColor} border-t-transparent`}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -30,7 +30,7 @@ interface GithubRelease {
|
||||
hash?: string;
|
||||
}>;
|
||||
published_at: string;
|
||||
is_alpha: boolean;
|
||||
is_nightly: boolean;
|
||||
}
|
||||
|
||||
interface VersionSelectorProps {
|
||||
@@ -75,7 +75,7 @@ export function VersionSelector({
|
||||
className="justify-between w-full"
|
||||
>
|
||||
{selectedVersion ?? placeholder}
|
||||
<LuChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
|
||||
<LuChevronsUpDown className="ml-2 w-4 h-4 opacity-50 shrink-0" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0">
|
||||
@@ -114,11 +114,11 @@ export function VersionSelector({
|
||||
: "opacity-0",
|
||||
)}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex gap-2 items-center">
|
||||
<span>{version.tag_name}</span>
|
||||
{version.is_alpha && (
|
||||
{version.is_nightly && (
|
||||
<Badge variant="secondary" className="text-xs">
|
||||
Alpha
|
||||
Nightly
|
||||
</Badge>
|
||||
)}
|
||||
{isDownloaded && (
|
||||
@@ -147,7 +147,7 @@ export function VersionSelector({
|
||||
variant="outline"
|
||||
className="w-full"
|
||||
>
|
||||
<LuDownload className="mr-2 h-4 w-4" />
|
||||
<LuDownload className="mr-2 w-4 h-4" />
|
||||
{isDownloading ? "Downloading..." : "Download Browser"}
|
||||
</LoadingButton>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user