mirror of
https://github.com/daijro/camoufox.git
synced 2026-08-18 16:00:58 +00:00
Clean up manager UI & add more info in version cli
- Rewrite `camoufox version` to show storage/path info and more python packages - In camoufox active/set, show active channels. Fall back to original/stable if none is set - Remove config files in `camoufox remove --all` - Spoof the camoufox python library version in UI debug mode - Fix scrollbar always showing on windows - Other UI cleanup
This commit is contained in:
+1
-1
@@ -57,7 +57,7 @@ Manage installed browsers, active version, IP geolocation databases, and package
|
||||
|
||||
More updates on it will be coming soon.
|
||||
|
||||
<img width="1199" height="815" alt="manager-ui" src="https://github.com/user-attachments/assets/aeef1b58-64ea-45f8-8d01-ac646366169d" />
|
||||
<img width="1207" height="830" alt="ui-screenshot" src="https://github.com/user-attachments/assets/6668f8f0-5b08-4c36-bbea-fea4baeccc9c" />
|
||||
|
||||
<hr width=50>
|
||||
|
||||
|
||||
+176
-72
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user