Compare commits

...

8 Commits

Author SHA1 Message Date
zhom edabfd0831 chore: download assets earlier 2026-02-17 00:00:58 +04:00
zhom 127912c68c chore: use correct dir for temp repo 2026-02-16 22:18:41 +04:00
zhom af2aa36ac6 feat: block launching profiles for incompatible systems 2026-02-16 22:18:11 +04:00
zhom d52493b7e4 docs: add email links 2026-02-16 21:08:28 +04:00
zhom dfc94c10ff chore: add latest release for nightly 2026-02-16 20:45:38 +04:00
zhom a008e11504 refactor: properly handle admin account 2026-02-16 19:58:23 +04:00
zhom 6f28ed3a47 chore: sign ad-hoc only if no env variables are set 2026-02-16 19:57:41 +04:00
zhom c30a44a13d docs: update preview 2026-02-16 19:11:45 +04:00
26 changed files with 384 additions and 24 deletions
+14
View File
@@ -230,3 +230,17 @@ jobs:
# with:
# branch: main
# commit_message: "docs: update CHANGELOG.md for ${{ github.ref_name }} [skip ci]"
bump-homebrew-cask:
needs: [release]
runs-on: macos-latest
permissions:
contents: read
steps:
- name: Bump Homebrew cask
env:
HOMEBREW_GITHUB_API_TOKEN: ${{ secrets.HOMEBREW_GITHUB_API_TOKEN }}
run: |
VERSION="${GITHUB_REF_NAME#v}"
brew tap --force homebrew/cask
brew bump-cask-pr --version "$VERSION" --no-browse donutbrowser
+52
View File
@@ -234,3 +234,55 @@ jobs:
run: |
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true
rm -f $RUNNER_TEMP/build_certificate.p12 || true
update-nightly-release:
needs: [rolling-release]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 #v6.0.1
- name: Generate nightly tag
id: tag
run: |
TIMESTAMP=$(date -u +"%Y-%m-%d")
COMMIT_HASH=$(echo "${GITHUB_SHA}" | cut -c1-7)
echo "nightly_tag=nightly-${TIMESTAMP}-${COMMIT_HASH}" >> $GITHUB_OUTPUT
- name: Update rolling nightly release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
NIGHTLY_TAG="${{ steps.tag.outputs.nightly_tag }}"
ASSETS_DIR="/tmp/nightly-assets"
# Download all assets from the per-commit nightly release
mkdir -p "$ASSETS_DIR"
gh release download "$NIGHTLY_TAG" --dir "$ASSETS_DIR" --clobber
# Rename versioned filenames to stable nightly names
cd "$ASSETS_DIR"
for f in Donut_*_aarch64.dmg; do [ -f "$f" ] && mv "$f" Donut_nightly_aarch64.dmg; done
for f in Donut_*_x64.dmg; do [ -f "$f" ] && mv "$f" Donut_nightly_x64.dmg; done
for f in Donut_*_x64-setup.exe; do [ -f "$f" ] && mv "$f" Donut_nightly_x64-setup.exe; done
for f in Donut_*_aarch64.AppImage; do [ -f "$f" ] && mv "$f" Donut_nightly_aarch64.AppImage; done
for f in Donut_*_amd64.AppImage; do [ -f "$f" ] && mv "$f" Donut_nightly_amd64.AppImage; done
for f in Donut_*_amd64.deb; do [ -f "$f" ] && mv "$f" Donut_nightly_amd64.deb; done
for f in Donut_*_arm64.deb; do [ -f "$f" ] && mv "$f" Donut_nightly_arm64.deb; done
for f in Donut-*.x86_64.rpm; do [ -f "$f" ] && mv "$f" Donut_nightly_x86_64.rpm; done
for f in Donut-*.aarch64.rpm; do [ -f "$f" ] && mv "$f" Donut_nightly_aarch64.rpm; done
cd "$GITHUB_WORKSPACE"
# Delete existing rolling nightly release and tag
gh release delete nightly --yes 2>/dev/null || true
git push --delete origin nightly 2>/dev/null || true
# Create new rolling nightly release with all assets
gh release create nightly \
"$ASSETS_DIR"/Donut_nightly_* \
"$ASSETS_DIR"/Donut_aarch64.app.tar.gz \
"$ASSETS_DIR"/Donut_x64.app.tar.gz \
--title "Donut Browser Nightly" \
--notes "Automatically updated nightly build from the latest main branch. Install via \`brew install --cask donutbrowser@nightly\`.\n\nCommit: ${GITHUB_SHA}" \
--prerelease
+1 -1
View File
@@ -23,6 +23,6 @@ Examples of unacceptable behavior by participants include:
## Enforcement
Violations of the Code of Conduct may be reported to contact at donutbrowser dot com. All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
Violations of the Code of Conduct may be reported to [contact@donutbrowser.com](mailto:contact@donutbrowser.com). All reports will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Further details of specific enforcement policies may be posted separately.
We hold the right and responsibility to remove comments or other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any members for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+2 -6
View File
@@ -25,11 +25,7 @@
</a>
</p>
<picture>
<source media="(prefers-color-scheme: dark)" srcset="assets/preview-dark.png" />
<source media="(prefers-color-scheme: light)" srcset="assets/preview.png" />
<img alt="Preview" src="assets/preview.png" />
</picture>
<img alt="Donut Browser Preview" src="assets/donut-preview.png" />
## Features
@@ -117,7 +113,7 @@ Have questions or want to contribute? We'd love to hear from you!
## Contact
Have an urgent question or want to report a security vulnerability? Send an email to contact at donutbrowser dot com and we'll get back to you as fast as possible.
Have an urgent question or want to report a security vulnerability? Send an email to [contact@donutbrowser.com](mailto:contact@donutbrowser.com) and we'll get back to you as fast as possible.
## License
+2 -2
View File
@@ -8,7 +8,7 @@ We take the security of Donut Browser seriously. If you believe you have found a
**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.**
Instead, please send an email to **contact at donutbrowser dot com** with the subject line "Security Vulnerability Report".
Instead, please send an email to **[contact@donutbrowser.com](mailto:contact@donutbrowser.com)** with the subject line "Security Vulnerability Report".
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
@@ -32,7 +32,7 @@ This information will help us triage your report more quickly.
## Contact
For urgent security matters, please contact us at **contact at donutbrowser dot com**.
For urgent security matters, please contact us at **[contact@donutbrowser.com](mailto:contact@donutbrowser.com)**.
For general questions about this security policy, you can also reach out through:
Binary file not shown.

After

Width:  |  Height:  |  Size: 623 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

+1
View File
@@ -520,6 +520,7 @@ mod tests {
note: None,
sync_enabled: false,
last_sync: None,
host_os: None,
}
}
+9 -5
View File
@@ -3,7 +3,7 @@ use crate::browser::{create_browser, BrowserType, ProxySettings};
use crate::camoufox_manager::CamoufoxConfig;
use crate::downloaded_browsers_registry::DownloadedBrowsersRegistry;
use crate::events;
use crate::profile::types::BrowserProfile;
use crate::profile::types::{get_host_os, BrowserProfile};
use crate::proxy_manager::PROXY_MANAGER;
use crate::wayfern_manager::WayfernConfig;
use directories::BaseDirs;
@@ -173,6 +173,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
host_os: None,
};
match self
@@ -286,6 +287,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
host_os: None,
};
match self
@@ -331,6 +333,7 @@ impl ProfileManager {
note: None,
sync_enabled: false,
last_sync: None,
host_os: Some(get_host_os()),
};
// Save profile info
@@ -466,8 +469,8 @@ impl ProfileManager {
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
// Check if browser is running
if profile.process_id.is_some() {
// Check if browser is running (cross-OS profiles can't be running locally)
if profile.process_id.is_some() && !profile.is_cross_os() {
return Err(
"Cannot delete profile while browser is running. Please stop the browser first.".into(),
);
@@ -733,8 +736,8 @@ impl ProfileManager {
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
// Check if browser is running
if profile.process_id.is_some() {
// Check if browser is running (cross-OS profiles can't be running locally)
if profile.process_id.is_some() && !profile.is_cross_os() {
return Err(
format!(
"Cannot delete profile '{}' while browser is running. Please stop the browser first.",
@@ -847,6 +850,7 @@ impl ProfileManager {
note: source.note,
sync_enabled: false,
last_sync: None,
host_os: Some(get_host_os()),
};
self.save_profile(&new_profile)?;
+21
View File
@@ -41,15 +41,36 @@ pub struct BrowserProfile {
pub sync_enabled: bool, // Whether sync is enabled for this profile
#[serde(default)]
pub last_sync: Option<u64>, // Timestamp of last successful sync (epoch seconds)
#[serde(default)]
pub host_os: Option<String>, // OS where profile was created ("macos", "windows", "linux")
}
pub fn default_release_type() -> String {
"stable".to_string()
}
pub fn get_host_os() -> String {
if cfg!(target_os = "macos") {
"macos".to_string()
} else if cfg!(target_os = "windows") {
"windows".to_string()
} else {
"linux".to_string()
}
}
impl BrowserProfile {
/// Get the path to the profile data directory (profiles/{uuid}/profile)
pub fn get_profile_data_path(&self, profiles_dir: &Path) -> PathBuf {
profiles_dir.join(self.id.to_string()).join("profile")
}
/// Returns true when the profile was created on a different OS than the current host.
/// Profiles without an `os` field (backward compat) are treated as native.
pub fn is_cross_os(&self) -> bool {
match &self.host_os {
Some(host_os) => host_os != &get_host_os(),
None => false,
}
}
}
+1
View File
@@ -555,6 +555,7 @@ impl ProfileImporter {
note: None,
sync_enabled: false,
last_sync: None,
host_os: Some(crate::profile::types::get_host_os()),
};
// Save the profile metadata
+107
View File
@@ -59,6 +59,15 @@ impl SyncEngine {
app_handle: &tauri::AppHandle,
profile: &BrowserProfile,
) -> SyncResult<()> {
if profile.is_cross_os() {
log::info!(
"Skipping file sync for cross-OS profile: {} ({})",
profile.name,
profile.id
);
return Ok(());
}
let profile_manager = ProfileManager::instance();
let profiles_dir = profile_manager.get_profiles_dir();
let profile_dir = profiles_dir.join(profile.id.to_string());
@@ -832,6 +841,49 @@ impl SyncEngine {
let mut profile: BrowserProfile = serde_json::from_slice(&metadata_data)
.map_err(|e| SyncError::SerializationError(format!("Failed to parse metadata: {e}")))?;
// Cross-OS profile: save metadata only, skip manifest + file downloads
if profile.is_cross_os() {
log::info!(
"Profile {} is cross-OS (host_os={:?}), downloading metadata only",
profile_id,
profile.host_os
);
fs::create_dir_all(&profile_dir).map_err(|e| {
SyncError::IoError(format!(
"Failed to create profile directory {}: {e}",
profile_dir.display()
))
})?;
profile.sync_enabled = true;
profile.last_sync = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
profile_manager
.save_profile(&profile)
.map_err(|e| SyncError::IoError(format!("Failed to save cross-OS profile: {e}")))?;
let _ = events::emit("profiles-changed", ());
let _ = events::emit(
"profile-sync-status",
serde_json::json!({
"profile_id": profile_id,
"status": "synced"
}),
);
log::info!(
"Cross-OS profile {} metadata downloaded successfully",
profile_id
);
return Ok(true);
}
// Download manifest
let manifest = self.download_manifest(&manifest_key).await?;
let Some(manifest) = manifest else {
@@ -940,6 +992,57 @@ impl SyncEngine {
log::info!("No missing profiles found");
}
// Refresh metadata for local cross-OS profiles (propagate renames, tags, notes from originating device)
let profile_manager = ProfileManager::instance();
// Collect cross-OS profiles before async operations to avoid holding non-Send Result across await
let cross_os_profiles: Vec<(String, bool)> = profile_manager
.list_profiles()
.unwrap_or_default()
.iter()
.filter(|p| p.is_cross_os() && p.sync_enabled)
.map(|p| (p.id.to_string(), p.sync_enabled))
.collect();
if !cross_os_profiles.is_empty() {
for (pid, sync_enabled) in &cross_os_profiles {
let metadata_key = format!("profiles/{}/metadata.json", pid);
match self.client.stat(&metadata_key).await {
Ok(stat) if stat.exists => match self.client.presign_download(&metadata_key).await {
Ok(presign) => match self.client.download_bytes(&presign.url).await {
Ok(data) => {
if let Ok(mut remote_profile) = serde_json::from_slice::<BrowserProfile>(&data) {
remote_profile.sync_enabled = *sync_enabled;
remote_profile.last_sync = Some(
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs(),
);
if let Err(e) = profile_manager.save_profile(&remote_profile) {
log::warn!("Failed to refresh cross-OS profile {} metadata: {}", pid, e);
} else {
log::debug!("Refreshed cross-OS profile {} metadata", pid);
}
}
}
Err(e) => {
log::warn!(
"Failed to download cross-OS profile {} metadata: {}",
pid,
e
);
}
},
Err(e) => {
log::warn!("Failed to presign cross-OS profile {} metadata: {}", pid, e);
}
},
_ => {}
}
}
let _ = events::emit("profiles-changed", ());
}
Ok(downloaded)
}
}
@@ -1048,6 +1151,10 @@ pub async fn set_profile_sync_enabled(
.find(|p| p.id == profile_uuid)
.ok_or_else(|| format!("Profile with ID '{profile_id}' not found"))?;
if profile.is_cross_os() {
return Err("Cannot modify sync settings for a cross-OS profile".to_string());
}
// If enabling, first check that sync settings are configured
if enabled {
// Cloud auth provides sync settings dynamically — skip local checks
+1 -1
View File
@@ -32,7 +32,7 @@
"frameworks": [],
"minimumSystemVersion": "10.13",
"exceptionDomain": "",
"signingIdentity": "-",
"signingIdentity": null,
"providerShortName": null,
"entitlements": "entitlements.plist",
"files": {
+3 -1
View File
@@ -92,7 +92,9 @@ export default function Home() {
// Cloud auth for cross-OS unlock
const { user: cloudUser } = useCloudAuth();
const crossOsUnlocked =
cloudUser?.plan !== "free" && cloudUser?.subscriptionStatus === "active";
cloudUser?.plan !== "free" &&
(cloudUser?.subscriptionStatus === "active" ||
cloudUser?.planPeriod === "lifetime");
const [createProfileDialogOpen, setCreateProfileDialogOpen] = useState(false);
const [settingsDialogOpen, setSettingsDialogOpen] = useState(false);
+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";