feat: block launching profiles for incompatible systems

This commit is contained in:
zhom
2026-02-16 22:18:11 +04:00
parent d52493b7e4
commit af2aa36ac6
16 changed files with 309 additions and 13 deletions
+104 -7
View File
@@ -13,6 +13,7 @@ import { invoke } from "@tauri-apps/api/core";
import { emit, listen } from "@tauri-apps/api/event";
import type { Dispatch, SetStateAction } from "react";
import * as React from "react";
import { FaApple, FaLinux, FaWindows } from "react-icons/fa";
import { FiWifi } from "react-icons/fi";
import { IoEllipsisHorizontal } from "react-icons/io5";
import {
@@ -68,6 +69,8 @@ import {
getBrowserDisplayName,
getBrowserIcon,
getCurrentOS,
getOSDisplayName,
isCrossOsProfile,
} from "@/lib/browser-utils";
import { formatRelativeTime } from "@/lib/flag-utils";
import { trimName } from "@/lib/name-utils";
@@ -1464,6 +1467,7 @@ export function ProfilesDataTable({
const profile = row.original;
const browser = profile.browser;
const IconComponent = getBrowserIcon(browser);
const isCrossOs = isCrossOsProfile(profile);
const isSelected = meta.isProfileSelected(profile.id);
const isRunning =
@@ -1474,6 +1478,66 @@ export function ProfilesDataTable({
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
// Cross-OS profiles: show OS icon when checkboxes aren't visible, show checkbox when they are
if (isCrossOs && !meta.showCheckboxes && !isSelected) {
const osName = profile.host_os
? getOSDisplayName(profile.host_os)
: "another OS";
const OsIcon =
profile.host_os === "macos"
? FaApple
: profile.host_os === "windows"
? FaWindows
: FaLinux;
return (
<Tooltip>
<TooltipTrigger asChild>
<span className="flex justify-center items-center w-4 h-4">
<button
type="button"
className="flex justify-center items-center p-0 border-none cursor-pointer"
onClick={() => meta.handleIconClick(profile.id)}
aria-label="Select profile"
>
<span className="w-4 h-4 group">
<OsIcon className="w-4 h-4 text-muted-foreground group-hover:hidden" />
<span className="peer border-input dark:bg-input/30 dark:data-[state=checked]:bg-primary size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none w-4 h-4 hidden group-hover:block pointer-events-none items-center justify-center duration-200" />
</span>
</button>
</span>
</TooltipTrigger>
<TooltipContent>
<p>Created on {osName} - view only</p>
</TooltipContent>
</Tooltip>
);
}
// Cross-OS profiles with checkboxes visible: show checkbox (selectable for bulk delete)
if (isCrossOs && (meta.showCheckboxes || isSelected)) {
const osName = profile.host_os
? getOSDisplayName(profile.host_os)
: "another OS";
return (
<NonHoverableTooltip
content={<p>Created on {osName} - view only</p>}
sideOffset={4}
horizontalOffset={8}
>
<span className="flex justify-center items-center w-4 h-4">
<Checkbox
checked={isSelected}
onCheckedChange={(value) =>
meta.handleCheckboxChange(profile.id, !!value)
}
aria-label="Select row"
className="w-4 h-4"
/>
</span>
</NonHoverableTooltip>
);
}
if (isDisabled) {
const tooltipMessage = isRunning
? "Can't modify running profile"
@@ -1718,13 +1782,18 @@ export function ProfilesDataTable({
</Tooltip>
);
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
return (
<button
@@ -1762,13 +1831,18 @@ export function ProfilesDataTable({
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
return (
<TagsCell
@@ -1790,13 +1864,18 @@ export function ProfilesDataTable({
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
return (
<NoteCell
@@ -1816,13 +1895,18 @@ export function ProfilesDataTable({
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isBrowserUpdating = meta.isUpdating(profile.browser);
const isDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
const hasOverride = Object.hasOwn(meta.proxyOverrides, profile.id);
const effectiveProxyId = hasOverride
@@ -2050,6 +2134,7 @@ export function ProfilesDataTable({
cell: ({ row, table }) => {
const meta = table.options.meta as TableMeta;
const profile = row.original;
const isCrossOs = isCrossOsProfile(profile);
const isRunning =
meta.isClient && meta.runningProfiles.has(profile.id);
const isBrowserUpdating =
@@ -2057,6 +2142,12 @@ export function ProfilesDataTable({
const isLaunching = meta.launchingProfiles.has(profile.id);
const isStopping = meta.stoppingProfiles.has(profile.id);
const isDisabled =
isRunning ||
isLaunching ||
isStopping ||
isBrowserUpdating ||
isCrossOs;
const isDeleteDisabled =
isRunning || isLaunching || isStopping || isBrowserUpdating;
return (
@@ -2077,6 +2168,7 @@ export function ProfilesDataTable({
onClick={() => {
meta.onOpenTrafficDialog?.(profile.id);
}}
disabled={isCrossOs}
>
View Network
</DropdownMenuItem>
@@ -2086,7 +2178,7 @@ export function ProfilesDataTable({
meta.onToggleProfileSync?.(profile);
}
}}
disabled={!meta.crossOsUnlocked}
disabled={!meta.crossOsUnlocked || isCrossOs}
>
<span className="flex items-center gap-2">
{profile.sync_enabled ? "Disable Sync" : "Enable Sync"}
@@ -2110,6 +2202,7 @@ export function ProfilesDataTable({
onClick={() => {
meta.onConfigureCamoufox?.(profile);
}}
disabled={isDisabled}
>
Change Fingerprint
</DropdownMenuItem>
@@ -2121,6 +2214,7 @@ export function ProfilesDataTable({
onClick={() => {
meta.onCopyCookiesToProfile?.(profile);
}}
disabled={isDisabled}
>
Copy Cookies to Profile
</DropdownMenuItem>
@@ -2137,7 +2231,7 @@ export function ProfilesDataTable({
onClick={() => {
setProfileToDelete(profile);
}}
disabled={isDisabled}
disabled={isDeleteDisabled}
>
Delete
</DropdownMenuItem>
@@ -2210,7 +2304,10 @@ export function ProfilesDataTable({
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
className="overflow-visible hover:bg-accent/50"
className={cn(
"overflow-visible hover:bg-accent/50",
isCrossOsProfile(row.original) && "opacity-60",
)}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id} className="overflow-visible">
+12 -1
View File
@@ -1,5 +1,9 @@
import { useCallback, useEffect, useState } from "react";
import { getBrowserDisplayName } from "@/lib/browser-utils";
import {
getBrowserDisplayName,
getOSDisplayName,
isCrossOsProfile,
} from "@/lib/browser-utils";
import type { BrowserProfile } from "@/types";
/**
@@ -48,6 +52,8 @@ export function useBrowserState(
(profile: BrowserProfile): boolean => {
if (!isClient) return false;
if (isCrossOsProfile(profile)) return false;
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
@@ -166,6 +172,11 @@ export function useBrowserState(
(profile: BrowserProfile): string => {
if (!isClient) return "Loading...";
if (isCrossOsProfile(profile) && profile.host_os) {
const osName = getOSDisplayName(profile.host_os);
return `Created on ${osName}. Can only be launched on ${osName}.`;
}
const isRunning = runningProfiles.has(profile.id);
const isLaunching = launchingProfiles.has(profile.id);
const isStopping = stoppingProfiles.has(profile.id);
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "Spoofing a different operating system is harder — system-level APIs are more difficult to mask, making it easier for websites to detect inconsistencies. No anti-detect browser can perfectly spoof every detail across operating systems."
},
"crossOs": {
"viewOnly": "Created on {{os}} - view only",
"cannotLaunch": "Created on {{os}}. Can only be launched on {{os}}.",
"cannotModify": "Cannot modify sync settings for a cross-OS profile"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "Suplantar un sistema operativo diferente es más difícil: las API a nivel de sistema son más difíciles de enmascarar, lo que facilita que los sitios web detecten inconsistencias. Ningún navegador antidetección puede suplantar perfectamente cada detalle entre sistemas operativos."
},
"crossOs": {
"viewOnly": "Creado en {{os}} - solo lectura",
"cannotLaunch": "Creado en {{os}}. Solo se puede iniciar en {{os}}.",
"cannotModify": "No se pueden modificar los ajustes de sincronización de un perfil de otro sistema operativo"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "Usurper un système d'exploitation différent est plus difficile : les API au niveau du système sont plus difficiles à masquer, ce qui permet aux sites web de détecter plus facilement les incohérences. Aucun navigateur anti-détection ne peut parfaitement usurper chaque détail d'un système d'exploitation à l'autre."
},
"crossOs": {
"viewOnly": "Créé sur {{os}} - lecture seule",
"cannotLaunch": "Créé sur {{os}}. Ne peut être lancé que sur {{os}}.",
"cannotModify": "Impossible de modifier les paramètres de synchronisation d'un profil d'un autre système d'exploitation"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "異なるオペレーティングシステムの偽装はより困難です。システムレベルのAPIはマスクしにくく、ウェブサイトが矛盾を検出しやすくなります。どのアンチディテクトブラウザも、異なるOS間のすべての詳細を完璧に偽装することはできません。"
},
"crossOs": {
"viewOnly": "{{os}}で作成 - 閲覧のみ",
"cannotLaunch": "{{os}}で作成されました。{{os}}でのみ起動できます。",
"cannotModify": "他のOSのプロファイルの同期設定は変更できません"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "Falsificar um sistema operacional diferente é mais difícil: as APIs de nível de sistema são mais difíceis de mascarar, facilitando a detecção de inconsistências pelos sites. Nenhum navegador antidetecção consegue falsificar perfeitamente todos os detalhes entre sistemas operacionais."
},
"crossOs": {
"viewOnly": "Criado em {{os}} - somente leitura",
"cannotLaunch": "Criado em {{os}}. Só pode ser iniciado em {{os}}.",
"cannotModify": "Não é possível modificar as configurações de sincronização de um perfil de outro sistema operacional"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "Подмена другой операционной системы сложнее — системные API труднее замаскировать, что упрощает обнаружение несоответствий веб-сайтами. Ни один антидетект-браузер не может идеально подменить все детали при смене операционной системы."
},
"crossOs": {
"viewOnly": "Создан на {{os}} - только просмотр",
"cannotLaunch": "Создан на {{os}}. Может быть запущен только на {{os}}.",
"cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС"
}
}
+5
View File
@@ -481,5 +481,10 @@
},
"fingerprint": {
"crossOsWarning": "伪装不同的操作系统更加困难——系统级API更难以掩盖,使网站更容易检测到不一致之处。没有任何反检测浏览器能够完美伪装跨操作系统的所有细节。"
},
"crossOs": {
"viewOnly": "在 {{os}} 上创建 - 仅查看",
"cannotLaunch": "在 {{os}} 上创建。只能在 {{os}} 上启动。",
"cannotModify": "无法修改跨操作系统配置文件的同步设置"
}
}
+18
View File
@@ -48,3 +48,21 @@ export const getCurrentOS = () => {
}
return "unknown";
};
export function isCrossOsProfile(profile: { host_os?: string }): boolean {
if (!profile.host_os) return false;
return profile.host_os !== getCurrentOS();
}
export function getOSDisplayName(os: string): string {
switch (os) {
case "macos":
return "macOS";
case "windows":
return "Windows";
case "linux":
return "Linux";
default:
return os;
}
}
+1
View File
@@ -27,6 +27,7 @@ export interface BrowserProfile {
note?: string; // User note
sync_enabled?: boolean; // Whether sync is enabled for this profile
last_sync?: number; // Timestamp of last successful sync (epoch seconds)
host_os?: string; // OS where profile was created ("macos", "windows", "linux")
}
export type SyncStatus = "Disabled" | "Syncing" | "Synced" | "Error";