mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-28 01:19:58 +02:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| edabfd0831 | |||
| 127912c68c | |||
| af2aa36ac6 | |||
| d52493b7e4 | |||
| dfc94c10ff | |||
| a008e11504 | |||
| 6f28ed3a47 | |||
| c30a44a13d |
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
@@ -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 |
@@ -520,6 +520,7 @@ mod tests {
|
||||
note: None,
|
||||
sync_enabled: false,
|
||||
last_sync: None,
|
||||
host_os: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
"frameworks": [],
|
||||
"minimumSystemVersion": "10.13",
|
||||
"exceptionDomain": "",
|
||||
"signingIdentity": "-",
|
||||
"signingIdentity": null,
|
||||
"providerShortName": null,
|
||||
"entitlements": "entitlements.plist",
|
||||
"files": {
|
||||
|
||||
+3
-1
@@ -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);
|
||||
|
||||
@@ -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