mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-06 23:13:58 +02:00
feat: block launching profiles for incompatible systems
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,5 +481,10 @@
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "異なるオペレーティングシステムの偽装はより困難です。システムレベルのAPIはマスクしにくく、ウェブサイトが矛盾を検出しやすくなります。どのアンチディテクトブラウザも、異なるOS間のすべての詳細を完璧に偽装することはできません。"
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "{{os}}で作成 - 閲覧のみ",
|
||||
"cannotLaunch": "{{os}}で作成されました。{{os}}でのみ起動できます。",
|
||||
"cannotModify": "他のOSのプロファイルの同期設定は変更できません"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,5 +481,10 @@
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "Подмена другой операционной системы сложнее — системные API труднее замаскировать, что упрощает обнаружение несоответствий веб-сайтами. Ни один антидетект-браузер не может идеально подменить все детали при смене операционной системы."
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "Создан на {{os}} - только просмотр",
|
||||
"cannotLaunch": "Создан на {{os}}. Может быть запущен только на {{os}}.",
|
||||
"cannotModify": "Невозможно изменить настройки синхронизации профиля другой ОС"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -481,5 +481,10 @@
|
||||
},
|
||||
"fingerprint": {
|
||||
"crossOsWarning": "伪装不同的操作系统更加困难——系统级API更难以掩盖,使网站更容易检测到不一致之处。没有任何反检测浏览器能够完美伪装跨操作系统的所有细节。"
|
||||
},
|
||||
"crossOs": {
|
||||
"viewOnly": "在 {{os}} 上创建 - 仅查看",
|
||||
"cannotLaunch": "在 {{os}} 上创建。只能在 {{os}} 上启动。",
|
||||
"cannotModify": "无法修改跨操作系统配置文件的同步设置"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user