diff --git a/pythonlib/README.md b/pythonlib/README.md
index 8162c13..7704c34 100644
--- a/pythonlib/README.md
+++ b/pythonlib/README.md
@@ -57,7 +57,7 @@ Manage installed browsers, active version, IP geolocation databases, and package
More updates on it will be coming soon.
-
+
diff --git a/pythonlib/camoufox/__main__.py b/pythonlib/camoufox/__main__.py
index 23744b9..a8b7576 100644
--- a/pythonlib/camoufox/__main__.py
+++ b/pythonlib/camoufox/__main__.py
@@ -21,8 +21,11 @@ from .geolocation import (
save_geoip_config,
)
from .multiversion import (
+ BROWSERS_DIR,
+ CONFIG_FILE,
REPO_CACHE_FILE,
InstalledVersion,
+ get_default_channel,
list_installed,
load_config,
load_repo_cache,
@@ -241,8 +244,8 @@ def fetch(version):
channel = config.get('channel', '')
repo_name = channel.split('/')[0] if '/' in channel else channel
ver_str = config['pinned']
- elif config.get('channel'):
- channel = config['channel']
+ else:
+ channel = config.get('channel') or get_default_channel()
if '/' in channel:
repo_name, ctype = channel.split('/', 1)
else:
@@ -261,9 +264,6 @@ def fetch(version):
else:
rprint(f"No versions found for channel '{channel}'.", fg="red")
return
- else:
- rprint("No channel set. Run 'camoufox select' first.", fg="red")
- return
for repo_data in cache.get('repos', []):
if repo_data['name'].lower() != repo_name.lower():
@@ -319,7 +319,7 @@ def _set_channel(repo_name: str, channel_type: str):
for inst in list_installed():
if inst.version.build == latest_build and inst.repo_name == repo_name.lower():
set_active(inst.relative_path)
- click.secho(f"Latest is installed: {inst.channel_path}", fg="green")
+ click.secho(f"Using latest: {inst.channel_path} (installed)", fg="green")
return
break
@@ -421,15 +421,13 @@ def set_cmd(specifier, geoip):
channels.append((name, 'prerelease', prereleases[0]))
config = load_config()
- channel = config.get('channel', '')
+ channel = config.get('channel') or get_default_channel()
pinned = config.get('pinned')
if pinned:
click.secho(f"Pinned: {channel.lower()}/{pinned}", fg="cyan")
- elif channel:
- click.secho(f"Channel: {channel.lower()}", fg="cyan")
else:
- click.secho("(no channel set)", fg="yellow")
+ click.secho(f"Channel: {channel.lower()}", fg="cyan")
click.echo()
channel_versions = {}
@@ -667,7 +665,8 @@ def remove(version_path, remove_all, yes):
has_geoip = GEOIP_DIR.exists()
if remove_all or version_path == 'all':
- if not installed and not has_geoip:
+ has_config = CONFIG_FILE.exists() or REPO_CACHE_FILE.exists()
+ if not installed and not has_geoip and not has_config:
rprint("Nothing to remove.", fg="yellow")
return
if installed and (yes or click.confirm(f"Remove all {len(installed)} browser version(s)?")):
@@ -676,6 +675,13 @@ def remove(version_path, remove_all, yes):
rprint(f"Removed {len(installed)} version(s).", fg="green")
if has_geoip and (yes or click.confirm("Remove GeoIP database?")):
remove_mmdb()
+ # Clean up config files
+ for f in (CONFIG_FILE, REPO_CACHE_FILE):
+ if f.exists():
+ f.unlink()
+ # Remove install dir if empty
+ if INSTALL_DIR.exists() and not any(INSTALL_DIR.iterdir()):
+ INSTALL_DIR.rmdir()
return
if not installed:
@@ -747,67 +753,167 @@ def gui(debug):
rprint("GUI requires PySide6. Install with: pip install 'camoufox\\[gui]'", fg="red")
+class VersionInfo:
+ def __init__(self):
+ from rich.table import Table
+ from rich.text import Text
+
+ from .pkgman import console
+
+ self.Text = Text
+ self.console = console
+ self.t = Table.grid(padding=(0, 2))
+
+ def _row(self, label, value, style="green"):
+ """
+ Print a row to the table
+ """
+ self.t.add_row(self.Text(f" {label}", style="dim"), self.Text(value, style=style))
+
+ def _header(self, title):
+ """
+ Print a section title to the table
+ """
+ self.t.add_row(self.Text(title, style="bold"), self.Text(""))
+
+ def _pkg(self, label, pkg_name):
+ try:
+ self._row(label, f"v{pkg_version(pkg_name)}")
+ except PackageNotFoundError:
+ self._row(label, "?", style="dim")
+
+ def packages(self):
+ """
+ Gets installed package versions
+ """
+ self._header("Python Packages")
+ self._pkg("Camoufox", "camoufox")
+ self._pkg("Browserforge", "browserforge")
+ self._pkg("Apify Fingerprints", "apify_fingerprint_datapoints")
+ self._pkg("Playwright", "playwright")
+
+ def browser(self):
+ """
+ Gets active browser, installed version, and sync status
+ """
+ from datetime import datetime, timezone
+
+ self._header("Browser")
+
+ config = load_config()
+ pinned = config.get('pinned')
+ channel = config.get('channel') or get_default_channel()
+
+ # Active: what was set (channel or pinned version)
+ if pinned:
+ self._row("Active", f"{channel.lower()}/{pinned}")
+ else:
+ self._row("Active", channel.lower())
+
+ # Find the active installed version
+ active_v = None
+ for v in list_installed():
+ if v.is_active:
+ active_v = v
+ break
+
+ # Browser version
+ if active_v:
+ self._row("Browser", f"v{active_v.version.full_string}")
+ else:
+ self._row("Browser", "—", style="dim")
+
+ # Is installed?
+ if active_v:
+ self._row("Installed", "Yes", style="green")
+ else:
+ self._row("Installed", "No", style="red")
+
+ # Check if installed version is the latest in its own channel
+ if active_v:
+ ctype = "prerelease" if active_v.is_prerelease else "stable"
+ repo_ch = f"{active_v.repo_name}/{ctype}"
+ is_latest = False
+ cache = load_repo_cache()
+ for repo_data in cache.get('repos', []):
+ if repo_data['name'].lower() != active_v.repo_name.lower():
+ continue
+ candidates = [v for v in repo_data.get('versions', []) if v.get('is_prerelease', False) == active_v.is_prerelease]
+ if candidates and active_v.version.build == candidates[0]['build']:
+ is_latest = True
+ break
+ self._row(
+ f"Latest in {repo_ch}?",
+ "Yes" if is_latest else "No",
+ style="green" if is_latest else "red",
+ )
+
+ # Last repo sync time from cache file mtime
+ if REPO_CACHE_FILE.exists():
+ mtime = REPO_CACHE_FILE.stat().st_mtime
+ dt = datetime.fromtimestamp(mtime, tz=timezone.utc).astimezone()
+ self._row("Last Sync", dt.strftime('%Y-%m-%d %H:%M'), style="dim")
+ else:
+ self._row("Last Sync", "Never", style="red")
+
+ def geoip(self):
+ """
+ Get info about the geoip db and check if its there
+ """
+ from datetime import datetime, timezone
+
+ self._header("GeoIP")
+ if not ALLOW_GEOIP:
+ # geoip2 package not installed
+ self._row("Status", "Not supported (install camoufox[geoip])", style="dim")
+ else:
+ mmdb_path = get_mmdb_path()
+ if mmdb_path.exists():
+ # Show active database name and last update time
+ geoip_cfg = load_geoip_config()
+ self._row("Database", geoip_cfg.get('name', 'Unknown'))
+ mtime = mmdb_path.stat().st_mtime
+ dt = datetime.fromtimestamp(mtime, tz=timezone.utc).astimezone()
+ self._row("Updated", dt.strftime('%Y-%m-%d %H:%M'), style="dim")
+ else:
+ self._row("Database", "Not installed", style="dim")
+
+ def _dir_size(self, path) -> str:
+ if not path.exists():
+ return "—"
+ total = sum(f.stat().st_size for f in path.rglob('*') if f.is_file())
+ for unit in ('B', 'KB', 'MB'):
+ if total < 1024:
+ return f"{total:.1f} {unit}" if unit != 'B' else f"{total} B"
+ total /= 1024
+ return f"{total:.1f} GB"
+
+ def storage(self):
+ """
+ Get paths and directory sizes
+ """
+ self._header("Storage")
+ self._row("Install path", str(INSTALL_DIR), style="cyan")
+ self._row("Browser(s) directory size", self._dir_size(BROWSERS_DIR), style="dim")
+ if ALLOW_GEOIP:
+ self._row("GeoIP database size", self._dir_size(GEOIP_DIR), style="dim")
+ self._row("Config file", str(CONFIG_FILE), style="cyan")
+ self._row("Repo cache", str(REPO_CACHE_FILE), style="cyan")
+
+ def print_all(self):
+ self.packages()
+ self.browser()
+ self.geoip()
+ self.storage()
+ self.console.print(self.t)
+
+
@cli.command(name='version')
def version():
"""
- Display version info
+ Display version, package, browser, and storage info
"""
- try:
- rprint(f"Pip package:\t v{pkg_version('camoufox')}", fg="green")
- except PackageNotFoundError:
- rprint("Pip package:\t Not installed!", fg="red")
-
- active_v = None
- for v in list_installed():
- if v.is_active:
- active_v = v
- break
-
- if not active_v:
- rprint("Active:\t\t Not installed!", fg="red")
- return
-
- config = load_config()
- pinned = config.get('pinned')
- channel = config.get('channel', '')
-
- # Channel
- if pinned:
- rprint(f"Channel:\t {channel.lower()} (Version pinned)", fg="cyan")
- elif channel:
- rprint(f"Channel:\t {channel.lower()} (Following updates)", fg="cyan")
-
- # Version with update status
- rprint(f"Version:\t v{active_v.version.full_string} ", fg="green", nl=False)
- if pinned:
- click.echo()
- elif channel:
- repo_name, ctype = channel.split('/', 1) if '/' in channel else ('', '')
- is_pre = ctype == "prerelease"
- latest_build = None
- cache = load_repo_cache()
- for repo_data in cache.get('repos', []):
- if repo_data['name'].lower() != repo_name.lower():
- continue
- candidates = [v for v in repo_data.get('versions', []) if v.get('is_prerelease', False) == is_pre]
- if candidates:
- latest_build = candidates[0]['build']
- break
- if latest_build and active_v.version.build == latest_build:
- rprint("(Up to date!)", fg="yellow")
- elif latest_build:
- rprint(f"(Latest: {latest_build})", fg="red")
- else:
- click.echo()
- else:
- click.echo()
-
- if REPO_CACHE_FILE.exists():
- from datetime import datetime, timezone
-
- mtime = REPO_CACHE_FILE.stat().st_mtime
- dt = datetime.fromtimestamp(mtime, tz=timezone.utc).astimezone()
- rprint(f"Last repo sync:\t {dt.strftime('%Y-%m-%d %H:%M')}", fg="bright_black")
+ VersionInfo().print_all()
@cli.command(name='active')
@@ -823,15 +929,13 @@ def active_cmd():
config = load_config()
pinned = config.get('pinned')
- channel = config.get('channel', '')
+ channel = config.get('channel') or get_default_channel()
if pinned:
click.echo(f"{channel.lower()}/{pinned} ", nl=False)
rprint("(not installed)", fg="yellow")
- elif channel:
+ else:
click.echo(f"{channel.lower()} ", nl=False)
rprint("(not installed)", fg="yellow")
- else:
- rprint("No active version.", fg="yellow")
@cli.command(name='path')
diff --git a/pythonlib/camoufox/gui/backend.py b/pythonlib/camoufox/gui/backend.py
index 5fa9f1d..2a9e14e 100644
--- a/pythonlib/camoufox/gui/backend.py
+++ b/pythonlib/camoufox/gui/backend.py
@@ -22,6 +22,7 @@ from PySide6.QtQuickControls2 import QQuickStyle
from ..multiversion import (
BROWSERS_DIR,
get_cached_versions,
+ get_default_channel,
get_repo_name,
list_installed,
load_config,
@@ -85,10 +86,11 @@ class DownloadWorker(Worker):
class SyncWorker(Worker):
- def __init__(self, spoof_os=None, spoof_arch=None):
+ def __init__(self, spoof_os=None, spoof_arch=None, spoof_lib_ver=None):
super().__init__()
self.spoof_os = spoof_os
self.spoof_arch = spoof_arch
+ self.spoof_lib_ver = spoof_lib_ver
def run(self):
try:
@@ -99,7 +101,7 @@ class SyncWorker(Worker):
self.status.emit("Syncing...")
cache = {'repos': []}
- for rc in RepoConfig.load_repos():
+ for rc in RepoConfig.load_repos(spoof_library_version=self.spoof_lib_ver):
self.status.emit(f"Syncing {rc.name}...")
versions = list_available_versions(
rc,
@@ -126,6 +128,7 @@ class SyncWorker(Worker):
cache['spoof_os'] = self.spoof_os
cache['spoof_arch'] = self.spoof_arch
+ cache['spoof_lib_ver'] = self.spoof_lib_ver
_dfmt = '%#m/%#d/%Y %#I:%M %p' if sys.platform == 'win32' else '%-m/%-d/%Y %-I:%M %p'
cache['sync_time'] = datetime.now().strftime(_dfmt)
save_repo_cache(cache)
@@ -287,6 +290,7 @@ class Backend(QObject):
self._spoof_os_idx = 0
self._spoof_arch_idx = 0
+ self._spoof_lib_ver = ""
self._load_spoof_from_cache()
self._load_geoip()
@@ -385,23 +389,20 @@ class Backend(QObject):
cfg = load_config()
if cfg.get('pinned'):
return f"v{cfg['pinned']}"
- channel = cfg.get('channel', '')
- if channel:
- _, keys, latest = self._build_channels()
- try:
- return latest[keys.index(channel)] or channel
- except ValueError:
- return channel
- return "(no channel set)"
+ channel = cfg.get('channel') or get_default_channel()
+ _, keys, latest = self._build_channels()
+ try:
+ return latest[keys.index(channel)] or channel
+ except ValueError:
+ return channel
@Property(str, notify=infoChanged)
def activeBrowserColor(self):
- cfg = load_config()
- return "#26a69a" if cfg.get('channel') or cfg.get('pinned') else "#888888"
+ return "#26a69a"
@Property(str, notify=infoChanged)
def followedChannel(self):
- return load_config().get('channel', '')
+ return load_config().get('channel') or get_default_channel()
@Property(list, notify=infoChanged)
def channels(self):
@@ -415,20 +416,24 @@ class Backend(QObject):
def channelLatest(self):
return self._build_channels()[2]
+ @Property(str, notify=infoChanged)
+ def activeBrowserLabel(self):
+ cfg = load_config()
+ return "Pinned Version" if cfg.get('pinned') else "Active Channel"
+
@Property(str, notify=infoChanged)
def activeLabel(self):
cfg = load_config()
- channel = cfg.get('channel', '')
pinned = cfg.get('pinned')
- if channel:
- channels, keys, _ = self._build_channels()
- try:
- return f"Active: following {channels[keys.index(channel)]}"
- except ValueError:
- return f"Active: following {channel}"
if pinned:
- return f"Active: pinned to v{pinned}"
- return ""
+ return f"Pinned version: v{pinned}"
+ channel = cfg.get('channel') or get_default_channel()
+ parts = channel.split('/', 1)
+ repo = parts[0].capitalize()
+ ctype = parts[1] if len(parts) > 1 else 'stable'
+ if ctype == 'stable':
+ return f"Following channel: {repo}"
+ return f"Following channel: {repo}/{ctype}"
@Property(str, notify=infoChanged)
def libraryVersion(self):
@@ -442,12 +447,16 @@ class Backend(QObject):
def browserforgeVersion(self):
return self._pkg_version('browserforge')
+ @Property(str, notify=infoChanged)
+ def fingerprintVersion(self):
+ return self._pkg_version('apify_fingerprint_datapoints')
+
@Property(str, notify=infoChanged)
def lastSyncTime(self):
cache = load_repo_cache()
- raw = cache.get('sync_time', 'Never') if cache else 'Never'
- if raw == 'Never':
- return raw
+ raw = cache.get('sync_time') if cache else None
+ if not raw:
+ return ""
try:
from datetime import datetime
@@ -482,6 +491,10 @@ class Backend(QObject):
def spoofArchIndex(self):
return self._spoof_arch_idx
+ @Property(str, notify=debugChanged)
+ def spoofLibVer(self):
+ return self._spoof_lib_ver
+
@Property(int, notify=currentRepoChanged)
def currentRepoIndex(self):
if self._current_repo:
@@ -661,7 +674,8 @@ class Backend(QObject):
def sync(self):
spoof_os = OS_OPTIONS[self._spoof_os_idx] if self._spoof_os_idx > 0 else None
spoof_arch = ARCH_OPTIONS[self._spoof_arch_idx] if self._spoof_arch_idx > 0 else None
- self._run_worker(SyncWorker(spoof_os, spoof_arch), self._on_done)
+ spoof_lib = self._spoof_lib_ver or None
+ self._run_worker(SyncWorker(spoof_os, spoof_arch, spoof_lib), self._on_done)
@Slot()
def cancelOperation(self):
@@ -727,6 +741,11 @@ class Backend(QObject):
self._spoof_arch_idx = index
self.debugChanged.emit()
+ @Slot(str)
+ def setSpoofLibVer(self, ver):
+ self._spoof_lib_ver = ver.strip()
+ self.debugChanged.emit()
+
@Slot()
def openGeoipFolder(self):
from ..geolocation import MMDB_DIR
@@ -836,10 +855,13 @@ class Backend(QObject):
return
spoof_os = cache.get('spoof_os')
spoof_arch = cache.get('spoof_arch')
+ spoof_lib = cache.get('spoof_lib_ver')
if spoof_os and spoof_os in OS_OPTIONS:
self._spoof_os_idx = OS_OPTIONS.index(spoof_os)
if spoof_arch and spoof_arch in ARCH_OPTIONS:
self._spoof_arch_idx = ARCH_OPTIONS.index(spoof_arch)
+ if spoof_lib:
+ self._spoof_lib_ver = spoof_lib
def _refresh(self):
items = []
@@ -856,7 +878,6 @@ class Backend(QObject):
versions = get_cached_versions(self._current_repo.name)
if not versions:
- self._set_status("No cache. Run Sync.", "#f39c12")
self._version_model.set_items(items)
return
diff --git a/pythonlib/camoufox/gui/qml/main.qml b/pythonlib/camoufox/gui/qml/main.qml
index b8e6f4b..0c661f0 100644
--- a/pythonlib/camoufox/gui/qml/main.qml
+++ b/pythonlib/camoufox/gui/qml/main.qml
@@ -519,60 +519,6 @@ ApplicationWindow {
anchors.right: parent.right
anchors.rightMargin: 1
- Rectangle {
- width: parent.width
- height: row
- color: "transparent"
-
- Header {
- anchors.left: parent.left
- anchors.leftMargin: s3
- anchors.verticalCenter: parent.verticalCenter
- text: "CHANNEL"
- }
- }
-
- Repeater {
- model: backend.channels
-
- Rectangle {
- width: parent ? parent.width : 0
- height: row + s2
- color: chMa.containsMouse ? c.raised : "transparent"
-
- Column {
- anchors.left: parent.left
- anchors.leftMargin: s4
- anchors.right: parent.right
- anchors.rightMargin: s3
- anchors.verticalCenter: parent.verticalCenter
- spacing: 1
-
- Row {
- spacing: s2
- Check { on: backend.followedChannel === backend.channelKeys[index]; anchors.verticalCenter: parent.verticalCenter }
- T { text: (backend.followedChannel === backend.channelKeys[index] ? "Following " : "Follow ") + modelData; anchors.verticalCenter: parent.verticalCenter }
- }
-
- Muted {
- leftPadding: s3 + s2
- text: backend.channelLatest[index] ? ("(latest: " + backend.channelLatest[index] + ")") : "(sync first)"
- font.pixelSize: Math.round(10 * scale)
- }
- }
-
- MouseArea {
- id: chMa
- anchors.fill: parent
- hoverEnabled: true
- cursorShape: Qt.PointingHandCursor
- onClicked: backend.setFollowedChannel(index)
- }
- }
- }
-
- Rule {}
-
Rectangle {
width: parent.width
height: row
@@ -612,6 +558,60 @@ ApplicationWindow {
Component.onCompleted: backend.selectRepo(0)
}
+
+ Rule {}
+
+ Rectangle {
+ width: parent.width
+ height: row
+ color: "transparent"
+
+ Header {
+ anchors.left: parent.left
+ anchors.leftMargin: s3
+ anchors.verticalCenter: parent.verticalCenter
+ text: "FOLLOW CHANNEL"
+ }
+ }
+
+ Repeater {
+ model: backend.channels
+
+ Rectangle {
+ width: parent ? parent.width : 0
+ height: row + s2
+ color: chMa.containsMouse ? c.raised : "transparent"
+
+ Column {
+ anchors.left: parent.left
+ anchors.leftMargin: s4
+ anchors.right: parent.right
+ anchors.rightMargin: s3
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: 1
+
+ Row {
+ spacing: s2
+ Check { on: backend.followedChannel === backend.channelKeys[index]; anchors.verticalCenter: parent.verticalCenter }
+ T { text: (backend.followedChannel === backend.channelKeys[index] ? "Following " : "Follow ") + modelData; anchors.verticalCenter: parent.verticalCenter }
+ }
+
+ Muted {
+ leftPadding: s3 + s2
+ text: backend.channelLatest[index] ? ("Latest: " + backend.channelLatest[index]) : "(sync first)"
+ font.pixelSize: Math.round(10 * scale)
+ }
+ }
+
+ MouseArea {
+ id: chMa
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: backend.setFollowedChannel(index)
+ }
+ }
+ }
}
}
@@ -678,7 +678,11 @@ ApplicationWindow {
clip: true
model: backend.versionModel
+ property real scrollBarWidth: vScrollBar.visible ? vScrollBar.width : 0
+
ScrollBar.vertical: ScrollBar {
+ id: vScrollBar
+ policy: vList.contentHeight > vList.height ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
contentItem: Rectangle {
implicitWidth: s2
radius: s1
@@ -690,7 +694,7 @@ ApplicationWindow {
id: vrow
property bool hov: hover.hovered
- width: vList.width
+ width: vList.width - vList.scrollBarWidth
height: row
color: model.isHeader ? c.fg :
model.isActive ? Qt.rgba(c.accent.r, c.accent.g, c.accent.b, 0.06) :
@@ -1101,7 +1105,7 @@ ApplicationWindow {
columnSpacing: s4 * 2
rowSpacing: s2
- Muted { text: "Active Browser" }
+ Muted { text: backend.activeBrowserLabel }
Bold { text: backend.activeBrowserText; color: backend.activeBrowserColor }
Muted { text: "Python Library" }
@@ -1113,11 +1117,11 @@ ApplicationWindow {
Muted { text: "Browserforge" }
T { text: backend.browserforgeVersion }
- Muted { text: "Last Sync" }
- T { text: backend.lastSyncTime }
+ Muted { text: "Fingerprints" }
+ T { text: backend.fingerprintVersion }
- Muted { text: "Repos" }
- T { text: backend.reposInfo }
+ Muted { text: "Last Sync" }
+ T { text: backend.lastSyncTime || "Never" }
Muted { text: "Website" }
T {
@@ -1156,13 +1160,38 @@ ApplicationWindow {
implicitWidth: Math.round(90 * scale)
}
}
+
+ Row {
+ spacing: s2
+ Muted { text: "Lib Version"; anchors.verticalCenter: parent.verticalCenter }
+ Rectangle {
+ width: Math.round(80 * scale)
+ height: Math.round(24 * scale)
+ color: "#1affffff"
+ radius: Math.round(3 * scale)
+ TextInput {
+ anchors.fill: parent
+ anchors.margins: Math.round(4 * scale)
+ color: "#fff"
+ font.pixelSize: Math.round(11 * scale)
+ text: backend.spoofLibVer
+ verticalAlignment: TextInput.AlignVCenter
+ clip: true
+ selectByMouse: true
+ property string placeholder: "(auto)"
+ Text {
+ text: parent.placeholder
+ color: "#80ffffff"
+ font: parent.font
+ visible: !parent.text && !parent.activeFocus
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ onEditingFinished: backend.setSpoofLibVer(text)
+ }
+ }
+ }
}
- Muted {
- visible: debugMode
- text: "Set spoof options then Sync on Browsers tab"
- font.pixelSize: Math.round(10 * scale)
- }
Rule { visible: debugMode }
@@ -1238,14 +1267,14 @@ ApplicationWindow {
Btn {
icon: "\uE895"
- text: "Sync Upstream"
+ text: "Sync Repos"
accent: c.accent
on: !backend.busy
onClicked: backend.sync()
}
Muted {
- text: "Synced: " + backend.lastSyncTime
+ text: backend.lastSyncTime ? "Synced: " + backend.lastSyncTime : "Sync has not been ran yet."
visible: !backend.busy
}
diff --git a/pythonlib/camoufox/multiversion.py b/pythonlib/camoufox/multiversion.py
index d056476..9df273d 100644
--- a/pythonlib/camoufox/multiversion.py
+++ b/pythonlib/camoufox/multiversion.py
@@ -35,6 +35,15 @@ def load_config() -> Dict:
return {}
+def get_default_channel() -> str:
+ """
+ Get the default repo's stable channel string (like official/stable)
+ """
+ from .pkgman import RepoConfig
+
+ return f"{RepoConfig.get_default_name().lower()}/stable"
+
+
def save_config(config: Dict) -> None:
"""
Save user config to disk
diff --git a/pythonlib/camoufox/pkgman.py b/pythonlib/camoufox/pkgman.py
index 82f7aa6..b58b27c 100644
--- a/pythonlib/camoufox/pkgman.py
+++ b/pythonlib/camoufox/pkgman.py
@@ -144,14 +144,14 @@ class RepoConfig:
build_max: Optional[str] = None
@staticmethod
- def load_repos() -> List['RepoConfig']:
+ def load_repos(spoof_library_version: Optional[str] = None) -> List['RepoConfig']:
"""
Load repository configurations from repos.yml
"""
repos_path = LOCAL_DATA / 'repos.yml'
with open(repos_path, 'r') as f:
data = load(f, Loader=CLoader)
- return [RepoConfig.from_dict(r) for r in data.get('browsers', [])]
+ return [RepoConfig.from_dict(r, spoof_library_version) for r in data.get('browsers', [])]
@staticmethod
def get_default_name() -> str:
@@ -164,7 +164,7 @@ class RepoConfig:
return data.get('default', {}).get('browser', 'Official')
@staticmethod
- def from_dict(d: Dict) -> 'RepoConfig':
+ def from_dict(d: Dict, spoof_library_version: Optional[str] = None) -> 'RepoConfig':
"""
Create RepoConfig from dictionary
"""
@@ -174,7 +174,7 @@ class RepoConfig:
build_min: Optional[str] = None
build_max: Optional[str] = None
if d.get('versions'):
- library_version = _get_library_version()
+ library_version = spoof_library_version or _get_library_version()
browser = _find_version_constraints(d['versions'], library_version)
if browser:
build_min = browser.get('min')