mirror of
https://github.com/daijro/camoufox.git
synced 2026-08-25 00:00:25 +00:00
Fixes for version pinning in GUI
- Replace icons with pins rather than checkboxes - Add a tooltip that says "Pin version" - Fix "Active" status bubble UX issues
This commit is contained in:
@@ -48,10 +48,12 @@ class Worker(QThread):
|
||||
|
||||
|
||||
class DownloadWorker(Worker):
|
||||
def __init__(self, repo_config, version):
|
||||
def __init__(self, repo_config, version, activate_after_install=False):
|
||||
super().__init__()
|
||||
self.repo_config = repo_config
|
||||
self.version = version
|
||||
self.activate_after_install = activate_after_install
|
||||
self.relative_path = None
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
@@ -81,7 +83,7 @@ class DownloadWorker(Worker):
|
||||
if sys.platform != 'win32':
|
||||
os.system(f'chmod -R 755 {shlex.quote(str(path))}')
|
||||
|
||||
set_active(f"browsers/{repo_name}/{folder}")
|
||||
self.relative_path = f"browsers/{repo_name}/{folder}"
|
||||
self.done.emit(True, f"Installed v{self.version.version.full_string}")
|
||||
except Exception as e:
|
||||
msg = str(e)
|
||||
@@ -310,6 +312,7 @@ class Backend(QObject):
|
||||
debugChanged = Signal()
|
||||
currentRepoChanged = Signal()
|
||||
channelPrompt = Signal(int, str, str)
|
||||
installedPrompt = Signal(int, str, str, bool, str)
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -650,6 +653,45 @@ class Backend(QObject):
|
||||
self._refresh()
|
||||
self.infoChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def unpinVersion(self, index):
|
||||
item = self._version_model.get(index)
|
||||
if not item or not item.is_pinned:
|
||||
return
|
||||
|
||||
cfg = load_config()
|
||||
cfg.pop('pinned', None)
|
||||
cfg.pop('pinned_sha', None)
|
||||
save_config(cfg)
|
||||
|
||||
self._refresh()
|
||||
self.infoChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def followVersionChannel(self, index):
|
||||
item = self._version_model.get(index)
|
||||
if not item or not item.version_data or not item.installed_data:
|
||||
return
|
||||
|
||||
ctype = "prerelease" if item.is_prerelease else "stable"
|
||||
repo_name = self._current_repo.name
|
||||
cfg = load_config()
|
||||
cfg['channel'] = repo_name if ctype == "stable" else f"{repo_name}/{ctype}"
|
||||
cfg.pop('pinned', None)
|
||||
cfg.pop('pinned_sha', None)
|
||||
cfg.update(
|
||||
{
|
||||
'active_repo': repo_name,
|
||||
'active_build': item.version_data.version.build,
|
||||
'active_version': item.version_data.version.version,
|
||||
}
|
||||
)
|
||||
save_config(cfg)
|
||||
set_active(item.installed_data.relative_path)
|
||||
|
||||
self._refresh()
|
||||
self.infoChanged.emit()
|
||||
|
||||
@Slot(int)
|
||||
def setFollowedChannel(self, index):
|
||||
_, keys, _ = self._build_channels()
|
||||
@@ -702,31 +744,50 @@ class Backend(QObject):
|
||||
break
|
||||
|
||||
self._refresh()
|
||||
self.infoChanged.emit()
|
||||
|
||||
for idx, item in enumerate(self._version_model._items):
|
||||
if item.is_header:
|
||||
continue
|
||||
if item.is_prerelease == is_pre:
|
||||
self._selected = idx
|
||||
self.selectionChanged.emit()
|
||||
if item.installed_data:
|
||||
set_active(item.installed_data.relative_path)
|
||||
self._refresh()
|
||||
self._selected = idx
|
||||
self.selectionChanged.emit()
|
||||
break
|
||||
|
||||
self.infoChanged.emit()
|
||||
|
||||
@Slot()
|
||||
def installSelected(self):
|
||||
item = self._version_model.get(self._selected)
|
||||
if item and not item.is_header and not item.is_installed:
|
||||
self._run_worker(DownloadWorker(self._current_repo, item.version_data), self._on_done)
|
||||
worker = DownloadWorker(
|
||||
self._current_repo,
|
||||
item.version_data,
|
||||
activate_after_install=item.is_active,
|
||||
)
|
||||
self._run_worker(worker, self._on_done)
|
||||
|
||||
@Slot()
|
||||
def uninstallSelected(self):
|
||||
item = self._version_model.get(self._selected)
|
||||
if not item or not item.is_installed or not item.installed_data:
|
||||
return
|
||||
installed = item.installed_data
|
||||
was_active = item.is_active
|
||||
try:
|
||||
remove_version(item.installed_data.path)
|
||||
remove_version(installed.path)
|
||||
if was_active:
|
||||
cfg = load_config()
|
||||
cfg.update(
|
||||
{
|
||||
'active_repo': self._current_repo.name,
|
||||
'active_build': installed.version.build,
|
||||
'active_version': installed.version.version,
|
||||
}
|
||||
)
|
||||
save_config(cfg)
|
||||
self._set_status(f"Uninstalled {item.display}", "#2ecc71")
|
||||
self._refresh()
|
||||
self.infoChanged.emit()
|
||||
@@ -965,6 +1026,20 @@ class Backend(QObject):
|
||||
return v.sha256 == latest_sha.get(v.version.full_string)
|
||||
|
||||
repo_key = get_repo_name(self._current_repo.repo)
|
||||
channel = cfg.get('channel') or get_default_channel()
|
||||
_, active_channel = (channel.split('/', 1) + ['stable'])[:2]
|
||||
|
||||
def _is_active(v, inst):
|
||||
if inst and inst.is_active:
|
||||
return True
|
||||
return (
|
||||
str(cfg.get('active_repo') or '').lower() == repo_key
|
||||
and cfg.get('active_build') == v.version.build
|
||||
and cfg.get('active_version') == v.version.version
|
||||
and v.is_prerelease == (active_channel == 'prerelease')
|
||||
and v.sha256 == latest_sha.get(v.version.full_string)
|
||||
)
|
||||
|
||||
installed_list = [iv for iv in list_installed() if iv.repo_name == repo_key]
|
||||
row_inst, extras = classify_installs(versions, installed_list)
|
||||
inst_by_id = {id(v): inst for v, inst in zip(versions, row_inst)}
|
||||
@@ -995,7 +1070,7 @@ class Backend(QObject):
|
||||
f"v{v.version.version}",
|
||||
v.version.build,
|
||||
is_prerelease=is_prerelease,
|
||||
is_active=bool(inst and inst.is_active),
|
||||
is_active=_is_active(v, inst),
|
||||
is_pinned=_is_pinned(v),
|
||||
is_installed=bool(inst),
|
||||
version_data=v,
|
||||
@@ -1063,13 +1138,66 @@ class Backend(QObject):
|
||||
worker.start()
|
||||
|
||||
def _on_done(self, ok, msg):
|
||||
worker = self._worker
|
||||
self._busy = False
|
||||
self._progress = -1
|
||||
self.busyChanged.emit()
|
||||
self._set_status(msg if ok else f"Error: {msg}", "#2ecc71" if ok else "#e74c3c")
|
||||
if ok:
|
||||
if (
|
||||
isinstance(worker, DownloadWorker)
|
||||
and worker.activate_after_install
|
||||
and worker.relative_path
|
||||
):
|
||||
set_active(worker.relative_path)
|
||||
|
||||
self._channel_data = None
|
||||
self._refresh()
|
||||
|
||||
if isinstance(worker, DownloadWorker):
|
||||
target_idx = -1
|
||||
for idx, item in enumerate(self._version_model._items):
|
||||
version = item.version_data
|
||||
if not version:
|
||||
continue
|
||||
if (
|
||||
version.version.full_string == worker.version.version.full_string
|
||||
and version.sha256 == worker.version.sha256
|
||||
):
|
||||
target_idx = idx
|
||||
break
|
||||
|
||||
if target_idx >= 0:
|
||||
self._selected = target_idx
|
||||
self.selectionChanged.emit()
|
||||
|
||||
if not worker.activate_after_install:
|
||||
versions = get_cached_versions(worker.repo_config.name)
|
||||
latest = next(
|
||||
(
|
||||
version
|
||||
for version in versions
|
||||
if version.is_prerelease == worker.version.is_prerelease
|
||||
),
|
||||
None,
|
||||
)
|
||||
is_latest = bool(
|
||||
latest
|
||||
and latest.version.full_string == worker.version.version.full_string
|
||||
and latest.sha256 == worker.version.sha256
|
||||
)
|
||||
channel = worker.repo_config.name
|
||||
if worker.version.is_prerelease:
|
||||
channel += " (Prerelease)"
|
||||
item = self._version_model.get(target_idx)
|
||||
self.installedPrompt.emit(
|
||||
target_idx,
|
||||
item.display,
|
||||
item.build,
|
||||
is_latest,
|
||||
channel,
|
||||
)
|
||||
|
||||
self.infoChanged.emit()
|
||||
self._load_spoof_from_cache()
|
||||
self.debugChanged.emit()
|
||||
|
||||
@@ -152,24 +152,42 @@ ApplicationWindow {
|
||||
}
|
||||
}
|
||||
|
||||
component Check: Rectangle {
|
||||
property bool on: false
|
||||
component PinButton: Item {
|
||||
id: pinButton
|
||||
property bool pinned: false
|
||||
property string pinToolTip: "Pin version"
|
||||
property string unpinToolTip: "Unpin version"
|
||||
signal clicked
|
||||
|
||||
width: s3
|
||||
height: s3
|
||||
radius: s1
|
||||
color: on ? c.accent : "transparent"
|
||||
border.color: on ? c.accent : c.muted
|
||||
border.width: 1
|
||||
width: s4
|
||||
height: s4
|
||||
|
||||
Text {
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
text: "\uE73E"
|
||||
color: c.bg
|
||||
font.family: fontIcon
|
||||
font.pixelSize: Math.round(8 * scale)
|
||||
visible: parent.on
|
||||
icon: "\uE840"
|
||||
color: pinButton.pinned ? c.accent : c.muted
|
||||
opacity: pinButton.enabled ? 1 : 0.4
|
||||
}
|
||||
|
||||
Icon {
|
||||
anchors.centerIn: parent
|
||||
icon: "\uE842"
|
||||
color: c.accent
|
||||
opacity: pinButton.enabled ? 1 : 0.4
|
||||
visible: pinButton.pinned
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: pinMa
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: pinButton.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (pinButton.enabled) pinButton.clicked()
|
||||
}
|
||||
|
||||
ToolTip.visible: pinMa.containsMouse && pinButton.enabled
|
||||
ToolTip.text: pinButton.pinned ? pinButton.unpinToolTip : pinButton.pinToolTip
|
||||
ToolTip.delay: 500
|
||||
}
|
||||
|
||||
component Input: Rectangle {
|
||||
@@ -335,14 +353,16 @@ ApplicationWindow {
|
||||
property string dlgAct: ""
|
||||
property string dlgVer: ""
|
||||
property string dlgBuild: ""
|
||||
property string dlgChannel: ""
|
||||
property int dlgIdx: -1
|
||||
|
||||
function showDlg(act, idx, ver, build) {
|
||||
function showDlg(act, idx, ver, build, channel) {
|
||||
backend.selectVersion(idx)
|
||||
dlgAct = act
|
||||
dlgIdx = idx
|
||||
dlgVer = ver
|
||||
dlgBuild = build
|
||||
dlgChannel = channel || ""
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
@@ -377,7 +397,9 @@ ApplicationWindow {
|
||||
|
||||
Bold {
|
||||
text: (dlgAct === "install" || dlgAct === "promptInstall") ? "Install"
|
||||
: dlgAct === "uninstall" ? "Uninstall" : "Set Active"
|
||||
: dlgAct === "uninstall" ? "Uninstall"
|
||||
: dlgAct === "promptPin" ? "Pin Version"
|
||||
: dlgAct === "promptFollow" ? "Follow Channel" : "Set Active"
|
||||
font.pixelSize: textMd
|
||||
}
|
||||
|
||||
@@ -386,6 +408,10 @@ ApplicationWindow {
|
||||
wrapMode: Text.Wrap
|
||||
text: dlgAct === "promptInstall"
|
||||
? "Camoufox " + dlgVer + "-" + dlgBuild + " is now active but not installed. Install it now?"
|
||||
: dlgAct === "promptPin"
|
||||
? "Camoufox " + dlgVer + "-" + dlgBuild + " is installed but not active. Would you like to pin this version?"
|
||||
: dlgAct === "promptFollow"
|
||||
? "Camoufox " + dlgVer + "-" + dlgBuild + " is installed but not active. Would you like to follow " + dlgChannel + " for updates?"
|
||||
: (dlgAct === "install" ? "Install" : dlgAct === "uninstall" ? "Remove" : "Set") +
|
||||
" Camoufox " + dlgVer + "-" + dlgBuild +
|
||||
(dlgAct === "install" ? "?" : dlgAct === "uninstall" ? " from disk?" : " as active?")
|
||||
@@ -406,13 +432,25 @@ ApplicationWindow {
|
||||
spacing: s2
|
||||
|
||||
Btn {
|
||||
text: dlgAct === "promptInstall" ? "Skip" : "Cancel"
|
||||
text: (dlgAct === "promptInstall" || dlgAct === "promptPin" || dlgAct === "promptFollow") ? "Skip" : "Cancel"
|
||||
onClicked: dlgAct = ""
|
||||
}
|
||||
|
||||
Btn {
|
||||
visible: dlgAct === "promptFollow"
|
||||
text: "Pin Version"
|
||||
accent: c.accent
|
||||
onClicked: {
|
||||
backend.setActive(dlgIdx)
|
||||
dlgAct = ""
|
||||
}
|
||||
}
|
||||
|
||||
Btn {
|
||||
text: (dlgAct === "install" || dlgAct === "promptInstall") ? "Install"
|
||||
: dlgAct === "uninstall" ? "Uninstall" : "Set Active"
|
||||
: dlgAct === "uninstall" ? "Uninstall"
|
||||
: dlgAct === "promptPin" ? "Pin Version"
|
||||
: dlgAct === "promptFollow" ? "Follow Channel" : "Set Active"
|
||||
accent: dlgAct === "uninstall" ? c.err
|
||||
: (dlgAct === "install" || dlgAct === "promptInstall") ? c.ok : c.accent
|
||||
onClicked: {
|
||||
@@ -420,6 +458,10 @@ ApplicationWindow {
|
||||
backend.installSelected()
|
||||
} else if (dlgAct === "uninstall") {
|
||||
backend.uninstallSelected()
|
||||
} else if (dlgAct === "promptPin") {
|
||||
backend.setActive(dlgIdx)
|
||||
} else if (dlgAct === "promptFollow") {
|
||||
backend.followVersionChannel(dlgIdx)
|
||||
} else if (dlgAct === "channel") {
|
||||
backend.confirmFollowChannel()
|
||||
if (backend.canInstall) {
|
||||
@@ -449,6 +491,9 @@ ApplicationWindow {
|
||||
function onChannelPrompt(idx, display, build) {
|
||||
showDlg("channel", idx, display, build)
|
||||
}
|
||||
function onInstalledPrompt(idx, display, build, latest, channel) {
|
||||
showDlg(latest ? "promptFollow" : "promptPin", idx, display, build, channel)
|
||||
}
|
||||
}
|
||||
|
||||
// Layout
|
||||
@@ -601,12 +646,18 @@ ApplicationWindow {
|
||||
|
||||
Row {
|
||||
spacing: s2
|
||||
Check { on: backend.followedChannel === backend.channelKeys[index]; anchors.verticalCenter: parent.verticalCenter }
|
||||
PinButton {
|
||||
pinned: backend.followedChannel === backend.channelKeys[index]
|
||||
pinToolTip: "Follow channel"
|
||||
unpinToolTip: "Following channel"
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
onClicked: backend.setFollowedChannel(index)
|
||||
}
|
||||
T { text: (backend.followedChannel === backend.channelKeys[index] ? "Following " : "Follow ") + modelData; anchors.verticalCenter: parent.verticalCenter }
|
||||
}
|
||||
|
||||
Muted {
|
||||
leftPadding: s3 + s2
|
||||
leftPadding: s4 + s2
|
||||
text: backend.channelLatest[index] ? ("Latest: " + backend.channelLatest[index]) : "(sync first)"
|
||||
font.pixelSize: Math.round(10 * scale)
|
||||
}
|
||||
@@ -752,12 +803,15 @@ ApplicationWindow {
|
||||
spacing: s3
|
||||
visible: !model.isHeader
|
||||
|
||||
Check {
|
||||
on: model.isPinned
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: model.note === "" ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
onClicked: if (model.note === "") showDlg("setActive", index, model.display, model.build)
|
||||
PinButton {
|
||||
pinned: model.isPinned
|
||||
enabled: model.note === ""
|
||||
onClicked: {
|
||||
if (model.isPinned) {
|
||||
backend.unpinVersion(index)
|
||||
} else {
|
||||
showDlg("setActive", index, model.display, model.build)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user