mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-10-19 05:57:31 +02:00
style: cleanup
This commit is contained in:
1 parent
18a891f709
commit
be78a6b722
5 files changed
+262
-89
No files matched your search
+45
-1
@@ -194,6 +194,50 @@ test("profile creation stays compact and saves basic, protected, and ephemeral p
|
||||
en.createProfile.proxy.noProxy,
|
||||
]);
|
||||
await app.capture("profile-create-compact-wide");
|
||||
// The device's operating system is chosen on the dialog itself. Without
|
||||
// a plan that includes cross-OS fingerprints, only this computer's
|
||||
// system can be picked.
|
||||
const hostOs =
|
||||
{ darwin: "macOS", win32: "Windows" }[process.platform] ?? "Linux";
|
||||
assert.equal(
|
||||
await app.execute(
|
||||
`return document.querySelector('#profile-os').textContent.trim();`,
|
||||
),
|
||||
hostOs,
|
||||
);
|
||||
await app.clickSelector("#profile-os");
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`return document.querySelectorAll('[cmdk-item]').length;`,
|
||||
),
|
||||
{ description: "operating system options" },
|
||||
);
|
||||
const osOptions = await app.execute(`
|
||||
return [...document.querySelectorAll('[cmdk-item]')].map((item) => ({
|
||||
text: item.textContent.trim(),
|
||||
disabled: item.getAttribute('aria-disabled') === 'true',
|
||||
}));
|
||||
`);
|
||||
assert.equal(osOptions.length, 5, JSON.stringify(osOptions));
|
||||
assert.deepEqual(
|
||||
osOptions.filter((option) => !option.disabled).map((o) => o.text),
|
||||
[hostOs],
|
||||
);
|
||||
assert.ok(
|
||||
osOptions
|
||||
.filter((option) => option.disabled)
|
||||
.every((option) => option.text.endsWith("PRO")),
|
||||
JSON.stringify(osOptions),
|
||||
);
|
||||
await app.pressShortcut({ key: "Escape" });
|
||||
await app.waitFor(
|
||||
() =>
|
||||
app.execute(
|
||||
`return !document.querySelector('[cmdk-item]') && Boolean(document.querySelector('#profile-name'));`,
|
||||
),
|
||||
{ description: "the OS menu to close and the dialog to stay" },
|
||||
);
|
||||
// Adding a route is part of the route list, and stays there when the
|
||||
// search finds nothing.
|
||||
await app.clickSelector("#profile-proxy");
|
||||
@@ -1350,7 +1394,7 @@ test("soft table and creation surfaces stay quiet and readable in every theme",
|
||||
const colors = await paintedColors(app, {
|
||||
off: ["#ephemeral"],
|
||||
on: ["#enable-password"],
|
||||
name: ['label[for="profile-name"]'],
|
||||
name: ['[data-slot="profile-name-field"]'],
|
||||
password: ["#profile-password"],
|
||||
});
|
||||
assertQuietTint(colors.off, colors.page, 1.35, `${theme.id} option`);
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { ProBadge } from "@/components/ui/pro-badge";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -69,6 +70,7 @@ import { useBrowserDownload } from "@/hooks/use-browser-download";
|
||||
import { useInputModality } from "@/hooks/use-input-modality";
|
||||
import { useProxyEvents } from "@/hooks/use-proxy-events";
|
||||
import { useVpnEvents } from "@/hooks/use-vpn-events";
|
||||
import { getOSDisplayName, getOSIcon } from "@/lib/browser-utils";
|
||||
import { DNS_BLOCKLIST_LEVELS } from "@/lib/dns-blocklist-levels";
|
||||
import { MOTION_EASE_OUT } from "@/lib/motion";
|
||||
import { cn } from "@/lib/utils";
|
||||
@@ -76,6 +78,8 @@ import type { BrowserReleaseTypes, WayfernConfig, WayfernOS } from "@/types";
|
||||
|
||||
const PASSWORD_MIN_LEN = 8;
|
||||
|
||||
const OS_OPTIONS: WayfernOS[] = ["windows", "macos", "linux", "android", "ios"];
|
||||
|
||||
const getCurrentOS = (): WayfernOS => {
|
||||
if (typeof navigator === "undefined") return "linux";
|
||||
const platform = navigator.platform.toLowerCase();
|
||||
@@ -125,6 +129,9 @@ export function CreateProfileDialog({
|
||||
const proxyListboxId = useId();
|
||||
const proxyLabelId = useId();
|
||||
const proxyValueId = useId();
|
||||
const osListboxId = useId();
|
||||
const osLabelId = useId();
|
||||
const osValueId = useId();
|
||||
const advancedOptionsId = useId();
|
||||
const reduceMotion = useReducedMotion() ?? false;
|
||||
const inputModality = useInputModality();
|
||||
@@ -132,6 +139,7 @@ export function CreateProfileDialog({
|
||||
const [advancedOpen, setAdvancedOpen] = useState(false);
|
||||
const [selectedProxyId, setSelectedProxyId] = useState<string>();
|
||||
const [proxyPopoverOpen, setProxyPopoverOpen] = useState(false);
|
||||
const [osPopoverOpen, setOsPopoverOpen] = useState(false);
|
||||
const [showProxyForm, setShowProxyForm] = useState(false);
|
||||
const [dnsBlocklist, setDnsBlocklist] = useState("");
|
||||
const [launchHook, setLaunchHook] = useState("");
|
||||
@@ -271,6 +279,10 @@ export function CreateProfileDialog({
|
||||
const proxyLabel = selectedVpn
|
||||
? `WG — ${selectedVpn.name}`
|
||||
: (selectedProxy?.name ?? t("createProfile.proxy.noProxy"));
|
||||
const hostOs = getCurrentOS();
|
||||
const selectedOs = wayfernConfig.os ?? hostOs;
|
||||
const crossOs = selectedOs !== hostOs;
|
||||
const SelectedOsIcon = getOSIcon(selectedOs);
|
||||
|
||||
const clearPassword = () => {
|
||||
setEnablePassword(false);
|
||||
@@ -285,6 +297,7 @@ export function CreateProfileDialog({
|
||||
setAdvancedOpen(false);
|
||||
setSelectedProxyId(undefined);
|
||||
setProxyPopoverOpen(false);
|
||||
setOsPopoverOpen(false);
|
||||
setShowProxyForm(false);
|
||||
setDnsBlocklist("");
|
||||
setLaunchHook("");
|
||||
@@ -417,38 +430,115 @@ export function CreateProfileDialog({
|
||||
data-slot="profile-create-fields"
|
||||
className="-mx-1 flex min-h-0 flex-col gap-3 overflow-y-auto px-1 pt-1 pb-2"
|
||||
>
|
||||
<motion.label
|
||||
htmlFor="profile-name"
|
||||
className="flex cursor-text items-center gap-2.5 rounded-xl bg-foreground/4 p-2 transition-[background-color,box-shadow] duration-150 focus-within:bg-foreground/6 focus-within:ring-2 focus-within:ring-foreground/10 hover:bg-foreground/5"
|
||||
<motion.div
|
||||
data-slot="profile-name-field"
|
||||
className="flex items-center gap-2 rounded-xl bg-foreground/4 p-2 transition-[background-color,box-shadow] duration-150 focus-within:bg-foreground/6 focus-within:ring-2 focus-within:ring-foreground/10 hover:bg-foreground/5"
|
||||
{...rise(0)}
|
||||
>
|
||||
<ProfileGlyph
|
||||
seed={profileName}
|
||||
routed={!!selectedProxyId}
|
||||
locked={enablePassword}
|
||||
ephemeral={ephemeral}
|
||||
busy={isCreating}
|
||||
/>
|
||||
<span className="sr-only">{t("createProfile.profileName")}</span>
|
||||
<Input
|
||||
id="profile-name"
|
||||
variant="bare"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={profileName}
|
||||
onChange={(event) => setProfileName(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
event.preventDefault();
|
||||
void handleCreate();
|
||||
}
|
||||
}}
|
||||
placeholder={t("createProfile.profileNamePlaceholder")}
|
||||
disabled={isCreating}
|
||||
className="h-9 text-base font-medium md:text-base"
|
||||
/>
|
||||
</motion.label>
|
||||
<label
|
||||
htmlFor="profile-name"
|
||||
className="flex min-w-0 flex-1 cursor-text items-center gap-2.5"
|
||||
>
|
||||
<ProfileGlyph
|
||||
seed={profileName}
|
||||
routed={!!selectedProxyId}
|
||||
locked={enablePassword}
|
||||
ephemeral={ephemeral}
|
||||
busy={isCreating}
|
||||
/>
|
||||
<span className="sr-only">{t("createProfile.profileName")}</span>
|
||||
<Input
|
||||
id="profile-name"
|
||||
variant="bare"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
value={profileName}
|
||||
onChange={(event) => setProfileName(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "Enter" && !event.nativeEvent.isComposing) {
|
||||
event.preventDefault();
|
||||
void handleCreate();
|
||||
}
|
||||
}}
|
||||
placeholder={t("createProfile.profileNamePlaceholder")}
|
||||
disabled={isCreating}
|
||||
className="h-9 text-base font-medium md:text-base"
|
||||
/>
|
||||
</label>
|
||||
<span id={osLabelId} className="sr-only">
|
||||
{t("fingerprint.osLabel")}
|
||||
</span>
|
||||
<Popover open={osPopoverOpen} onOpenChange={setOsPopoverOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<button
|
||||
id="profile-os"
|
||||
type="button"
|
||||
role="combobox"
|
||||
aria-expanded={osPopoverOpen}
|
||||
aria-controls={osListboxId}
|
||||
aria-labelledby={`${osLabelId} ${osValueId}`}
|
||||
className={cn(optionPill(crossOs), "shrink-0")}
|
||||
disabled={isCreating}
|
||||
>
|
||||
<PillIcon on={crossOs} animate={animateEntry}>
|
||||
<SelectedOsIcon />
|
||||
</PillIcon>
|
||||
<PillLabel
|
||||
id={osValueId}
|
||||
label={getOSDisplayName(selectedOs)}
|
||||
animate={animateEntry}
|
||||
/>
|
||||
<LuChevronDown
|
||||
className={cn(
|
||||
"-mr-0.5 opacity-60 transition-transform duration-200 motion-reduce:transition-none",
|
||||
osPopoverOpen && "rotate-180",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-52 p-0" align="end" sideOffset={6}>
|
||||
<Command>
|
||||
<CommandList id={osListboxId}>
|
||||
<CommandGroup>
|
||||
{OS_OPTIONS.map((os) => {
|
||||
const locked = os !== hostOs && !crossOsUnlocked;
|
||||
const Icon = getOSIcon(os);
|
||||
return (
|
||||
<CommandItem
|
||||
key={os}
|
||||
value={os}
|
||||
disabled={locked}
|
||||
onSelect={() => {
|
||||
updateWayfernConfig("os", os);
|
||||
setOsPopoverOpen(false);
|
||||
}}
|
||||
>
|
||||
<LuCheck
|
||||
className={cn(
|
||||
"size-4 shrink-0",
|
||||
selectedOs === os ? "opacity-100" : "opacity-0",
|
||||
)}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<Icon
|
||||
className="size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span className="truncate">
|
||||
{getOSDisplayName(os)}
|
||||
</span>
|
||||
{locked && <ProBadge className="ml-auto" />}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
className="flex flex-wrap items-center gap-1.5"
|
||||
@@ -688,6 +778,20 @@ export function CreateProfileDialog({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{crossOs && (
|
||||
<motion.p
|
||||
role="status"
|
||||
className={cn(STATUS_CLASS, "items-start")}
|
||||
{...reveal}
|
||||
>
|
||||
<LuTriangleAlert
|
||||
className="mt-0.5 size-3.5 shrink-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{t("fingerprint.crossOsWarning")}
|
||||
</motion.p>
|
||||
)}
|
||||
|
||||
{downloading ? (
|
||||
<motion.p role="status" className={STATUS_CLASS} {...reveal}>
|
||||
<LuLoaderCircle
|
||||
@@ -920,6 +1024,7 @@ export function CreateProfileDialog({
|
||||
readOnly={isCreating}
|
||||
crossOsUnlocked={crossOsUnlocked}
|
||||
limitedMode={!crossOsUnlocked}
|
||||
osPicker={false}
|
||||
profileVersion={creatableVersion}
|
||||
profileBrowser="wayfern"
|
||||
/>
|
||||
|
||||
@@ -19,7 +19,6 @@ import { emit, listen } from "@tauri-apps/api/event";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
|
||||
import { FiWifi } from "react-icons/fi";
|
||||
import {
|
||||
LuArrowDown,
|
||||
@@ -128,6 +127,7 @@ import { parseBackendError, translateBackendError } from "@/lib/backend-errors";
|
||||
import {
|
||||
getBrowserDisplayName,
|
||||
getOSDisplayName,
|
||||
getOSIcon,
|
||||
getProfileIcon,
|
||||
isCrossOsProfile,
|
||||
} from "@/lib/browser-utils";
|
||||
@@ -2543,13 +2543,7 @@ export function ProfilesDataTable({
|
||||
const profile = row.original;
|
||||
const crossOs = isCrossOsProfile(profile);
|
||||
const os = profile.host_os || profile.wayfern_config?.os || "";
|
||||
const Icon = crossOs
|
||||
? os === "macos"
|
||||
? FaApple
|
||||
: os === "windows"
|
||||
? FaWindows
|
||||
: FaLinux
|
||||
: getProfileIcon(profile);
|
||||
const Icon = crossOs ? getOSIcon(os) : getProfileIcon(profile);
|
||||
const message = crossOs
|
||||
? t("crossOs.viewOnly", { os: getOSDisplayName(os) })
|
||||
: meta.runningProfiles.has(profile.id)
|
||||
|
||||
@@ -176,6 +176,8 @@ interface WayfernConfigFormProps {
|
||||
readOnly?: boolean;
|
||||
crossOsUnlocked?: boolean;
|
||||
limitedMode?: boolean;
|
||||
/** False when the caller shows its own operating system picker. */
|
||||
osPicker?: boolean;
|
||||
profileVersion?: string;
|
||||
profileBrowser?: string;
|
||||
/// The saved profile whose persona is edited here. Absent while a profile is
|
||||
@@ -275,6 +277,7 @@ export function WayfernConfigForm({
|
||||
readOnly = false,
|
||||
crossOsUnlocked = false,
|
||||
limitedMode = false,
|
||||
osPicker = true,
|
||||
profileVersion,
|
||||
profileBrowser,
|
||||
profileId,
|
||||
@@ -428,60 +431,65 @@ export function WayfernConfigForm({
|
||||
* whereas their absence is the truth. */
|
||||
const isIdentityDerived = config.identity_id != null;
|
||||
|
||||
const generateButton = profileVersion && (!isCreating || crossOsUnlocked) && (
|
||||
<LoadingButton
|
||||
isLoading={isGeneratingFingerprint}
|
||||
onClick={handleRegenerateClick}
|
||||
disabled={readOnly}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{isCreating
|
||||
? t("fingerprint.generateFingerprint")
|
||||
: t("fingerprint.regenerateFingerprint")}
|
||||
</LoadingButton>
|
||||
);
|
||||
|
||||
const renderAdvancedForm = () => (
|
||||
<div className="space-y-6">
|
||||
{/* Operating System Selection */}
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("fingerprint.osLabel")}</Label>
|
||||
{profileVersion && (!isCreating || crossOsUnlocked) && (
|
||||
<LoadingButton
|
||||
isLoading={isGeneratingFingerprint}
|
||||
onClick={handleRegenerateClick}
|
||||
disabled={readOnly}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
>
|
||||
{isCreating
|
||||
? t("fingerprint.generateFingerprint")
|
||||
: t("fingerprint.regenerateFingerprint")}
|
||||
</LoadingButton>
|
||||
{!osPicker ? (
|
||||
generateButton && <div className="flex">{generateButton}</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label>{t("fingerprint.osLabel")}</Label>
|
||||
{generateButton}
|
||||
</div>
|
||||
<Select
|
||||
value={selectedOS}
|
||||
onValueChange={(value: WayfernOS) => {
|
||||
onConfigChange("os", value);
|
||||
}}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("fingerprint.selectOSPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
|
||||
).map((os) => {
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
{osLabels[os]}
|
||||
{isDisabled && <ProBadge />}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
{t("fingerprint.crossOsWarning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
<Select
|
||||
value={selectedOS}
|
||||
onValueChange={(value: WayfernOS) => {
|
||||
onConfigChange("os", value);
|
||||
}}
|
||||
disabled={readOnly}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder={t("fingerprint.selectOSPlaceholder")} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{(
|
||||
["windows", "macos", "linux", "android", "ios"] as WayfernOS[]
|
||||
).map((os) => {
|
||||
const isDisabled = os !== currentOS && !crossOsUnlocked;
|
||||
return (
|
||||
<SelectItem key={os} value={os} disabled={isDisabled}>
|
||||
<span className="flex items-center gap-2">
|
||||
{osLabels[os]}
|
||||
{isDisabled && <ProBadge />}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{selectedOS !== currentOS && crossOsUnlocked && (
|
||||
<Alert className="mt-2">
|
||||
<AlertDescription>
|
||||
{t("fingerprint.crossOsWarning")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Randomize Fingerprint Option */}
|
||||
<div className="space-y-3 rounded-lg bg-foreground/4 p-4">
|
||||
|
||||
@@ -6,12 +6,16 @@ import { SiBrave, SiOpera, SiVivaldi } from "react-icons/si";
|
||||
*/
|
||||
|
||||
import {
|
||||
FaAndroid,
|
||||
FaApple,
|
||||
FaChrome,
|
||||
FaEdge,
|
||||
FaExclamationTriangle,
|
||||
FaFire,
|
||||
FaFirefox,
|
||||
FaLinux,
|
||||
FaSafari,
|
||||
FaWindows,
|
||||
} from "react-icons/fa";
|
||||
import { LuLock } from "react-icons/lu";
|
||||
import { getCurrentOS } from "@/lib/platform";
|
||||
@@ -103,7 +107,25 @@ export function getOSDisplayName(os: string): string {
|
||||
return "Windows";
|
||||
case "linux":
|
||||
return "Linux";
|
||||
case "android":
|
||||
return "Android";
|
||||
case "ios":
|
||||
return "iOS";
|
||||
default:
|
||||
return os;
|
||||
}
|
||||
}
|
||||
|
||||
export function getOSIcon(os: string) {
|
||||
switch (os) {
|
||||
case "macos":
|
||||
case "ios":
|
||||
return FaApple;
|
||||
case "windows":
|
||||
return FaWindows;
|
||||
case "android":
|
||||
return FaAndroid;
|
||||
default:
|
||||
return FaLinux;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user