Refactor profile editor and update protocol location images

This commit is contained in:
JOhn 2026-10-12 11:40:46 -04:00
parent e48c8db320
commit 3ad4dd1a13
28 changed files with 663 additions and 802 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

0
gui/resources/images/hysteria2.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 258 KiB

After

Width:  |  Height:  |  Size: 258 KiB

BIN
gui/resources/images/hysteria2_mini.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

0
gui/resources/images/hystria2_button.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

0
gui/resources/images/icon_mini_is_1.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 9.3 KiB

After

Width:  |  Height:  |  Size: 9.3 KiB

0
gui/resources/images/icon_mini_is_1.webp Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 8.1 KiB

0
gui/resources/images/icon_mini_se_ab.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

0
gui/resources/images/icon_se_ab.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 277 KiB

After

Width:  |  Height:  |  Size: 277 KiB

0
gui/resources/images/vless.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 215 KiB

After

Width:  |  Height:  |  Size: 215 KiB

0
gui/resources/images/vless_button.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

0
gui/resources/images/wireguard_button.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

0
gui/resources/images/wireguard_mini.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 4.5 KiB

0
gui/resources/images/wireguard_mini_transparent.png Normal file → Executable file
View file

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

164
gui/v2/actions/editor_data.py Executable file
View file

@ -0,0 +1,164 @@
from typing import Any, Callable, Mapping, TypedDict
from core.controllers.LocationController import LocationController
from core.models.BaseProfile import BaseProfile
from gui.v2.actions.menu_profiles import profile_is_session, summarize_profile
class EditorState(TypedDict):
profile_id: int
baseline: dict[str, Any]
changes: dict[str, str]
locations: dict[str, dict[str, str]]
location_choices: list[str]
browser_choices: list[str]
resolutions: list[str]
is_synced: bool
class EditorResult(TypedDict, total=False):
status: str
message: str
state: EditorState
view: dict[str, Any]
request: dict[str, Any]
destination: str
refresh_menu: bool
adjusted: bool
saved_fields: list[str]
sync_data: tuple[list, list, bool, bool, list, list]
EditorCallback = Callable[[str, EditorResult], None]
SINGBOX_PROTOCOLS = ("hysteria2", "vless")
EDITABLE_FIELDS = ("name", "location", "browser", "dimentions", "connection")
def location_record(location: Any) -> dict[str, str]:
"""Flatten location and operator models before they reach Qt rendering code."""
if location is None or isinstance(location, dict):
return {}
operator = getattr(location, "operator", None)
return {
"key": f"{location.country_code}_{location.code}",
"country": getattr(location, "country_name", "") or "",
"label": f"{getattr(location, 'country_name', '')}, {getattr(location, 'name', '')}",
"operator": getattr(operator, "name", "") or "",
"nostr": getattr(operator, "nostr_public_key", "") or "",
}
def snapshot_profile(profile: BaseProfile) -> dict[str, Any]:
"""Reuse menu normalization while keeping the editor baseline independent of mutable core models."""
summary = summarize_profile(int(profile.id), profile)[1]
browser = summary["browser"]
version = summary["browser_version"]
return {**summary, "browser": f"{browser}:{version}" if version else browser,
"is_session": profile_is_session(profile),
"location_metadata": location_record(getattr(profile, "location", None))}
def prepare_editor_data() -> dict[str, dict[str, str]]:
"""Preload shared location metadata while leaving the editing baseline to a fresh selected-profile read."""
records = [location_record(location) for location in LocationController.get_all()]
return {record["key"]: record for record in records if record}
def copy_editor_state(state: EditorState) -> EditorState:
"""Capture task inputs so asynchronous operations and confirmations cannot modify the live draft."""
return {**state, "baseline": dict(state["baseline"]), "changes": dict(state["changes"]),
"locations": dict(state["locations"]), "location_choices": list(state["location_choices"]),
"browser_choices": list(state["browser_choices"]), "resolutions": list(state["resolutions"])}
def editor_parameters(data: Mapping[str, Any], state: EditorState) -> dict[str, list[str]]:
"""Choose supported editor fields with protocol tables instead of nested rendering branches."""
shared = {"location": state["location_choices"], "browser": state["browser_choices"],
"dimentions": list(dict.fromkeys([*state["resolutions"], str(data.get("dimentions", "None"))]))}
proxy = {"protocol": ["residential", "wireguard", "hidetor"], "connection": ["tor", "just proxy"], **shared}
protocol = data["protocol"]
rules = {
"wireguard": {"protocol": ["wireguard", "residential", "hidetor"], "connection": ["browser-only", "system-wide"], **shared},
"hidetor": proxy, "residential": proxy, "open": {"protocol": ["open"]},
"hysteria2": {"protocol": ["hysteria2"], "connection": ["system-wide"], "location": state["location_choices"]},
"vless": {"protocol": ["vless"], "connection": ["system-wide"], "location": state["location_choices"]},
}
return rules.get(protocol, {})
def _field_spec(key: str, data: Mapping[str, Any], state: EditorState, choices: list[str]) -> dict[str, Any]:
"""Prepare field content and control permissions before Qt creates the widgets."""
value = str(data.get(key, ""))
kind = {"browser": "browser", "location": "location", "dimentions": "resolution"}.get(key, "image")
location = state["locations"].get(value, {})
protocol_asset = {"hysteria2": "hystria2"}.get(value, value)
hidden = data["connection"] == "system-wide" and key in ("browser", "dimentions")
locked = key == "protocol" or (key == "connection" and (data["connection"] == "system-wide" or data["protocol"] in SINGBOX_PROTOCOLS))
browser = value.replace(":", " ", 1)
return {"key": key, "kind": kind, "value": value, "visible": not hidden,
"enabled": not locked, "needs_sync": key in ("browser", "location") and not state["is_synced"],
"asset": f"{protocol_asset if key == 'protocol' else value}_button.png",
"browser_text": browser, "unknown_browser": not browser or browser.lower().startswith("unknown"),
"blank_browser": key == "browser" and hidden,
"location_asset": f"button_{value}.png", "location_name": location.get("country") or value,
"provider": location.get("operator", ""), "choices": list(choices)}
def _preview(data: Mapping[str, Any], state: EditorState) -> dict[str, Any]:
"""Select preview assets and operator information without leaving protocol policy in Qt methods."""
protocol, connection, location = data["protocol"], data["connection"], data["location"]
location_data = state["locations"].get(location, {})
tor_prefix = "icon" if connection == "just proxy" else "hdtor"
primary = {"hidetor": f"{tor_prefix}_{location}.png"}.get(protocol, f"{protocol}_{location}.png")
operator = location_data.get("operator", "")
show_operator = bool(operator) and operator != "Simplified Privacy" and protocol not in ("hidetor", *SINGBOX_PROTOCOLS)
background = None
if protocol == "wireguard" and connection == "browser-only":
background = "browser only.png" if data["browser_supported"] else "unsupported_browser_only.png"
if protocol == "residential":
background = "browser only.png"
return {"asset": primary, "layout": {"hidetor": "tor", "vless": "vless"}.get(protocol, "standard"),
"encrypted": protocol in SINGBOX_PROTOCOLS,
"visible": protocol != "residential", "background": background,
"garage": "brazil garaje.png" if protocol == "residential" and location else None,
"operator": operator, "location_text": location_data.get("label", ""),
"nostr": location_data.get("nostr", ""), "show_operator": show_operator,
"location_asset": f"icon_{location}.png", "text_color": "black" if data["is_session"] else "white"}
def render_editor_state(state: EditorState, status: str = "render") -> EditorResult:
"""Rebuild presentation from the baseline and draft without fetching profile data again."""
data = {**state["baseline"], **state["changes"]}
if "browser" in state["changes"]:
data["browser_supported"] = data["browser"] in state["browser_choices"]
parameters = editor_parameters(data, state)
return {"status": status, "state": state, "view": {
"name": data["name"], "preview": _preview(data, state),
"fields": [_field_spec(key, data, state, choices) for key, choices in parameters.items()],
}}
def set_editor_value(state: EditorState, key: str, value: str) -> EditorResult:
"""Remove reverted values from the draft so unsaved-change warnings reflect actual differences."""
if key not in EDITABLE_FIELDS:
return {"status": "error", "message": f"Editing {key} is not supported.", "state": state}
updated = copy_editor_state(state)
updated["changes"].pop(key, None)
if value != str(updated["baseline"].get(key, "")):
updated["changes"][key] = value
return render_editor_state(updated, "draft" if key == "name" else "render")
def set_editor_resolution(state: EditorState, width_text: str, height_text: str, host_width: int, host_height: int) -> EditorResult:
"""Validate and fit custom dimensions before the dialog records a pending resolution."""
try:
width, height = int(width_text), int(height_text)
except ValueError:
return {"status": "error", "message": "Enter whole numbers for width and height."}
if min(width, height, host_width, host_height) <= 0:
return {"status": "error", "message": "Width, height and host dimensions must be positive."}
fitted_width = max(1, host_width - 50) if width > host_width else width
fitted_height = max(1, host_height - 50) if height > host_height else height
result = set_editor_value(state, "dimentions", f"{fitted_width}x{fitted_height}")
result["adjusted"] = (width, height) != (fitted_width, fitted_height)
return result

199
gui/v2/actions/editor_tasks.py Executable file
View file

@ -0,0 +1,199 @@
from functools import partial
from typing import Any, Callable, Mapping, Sequence
from PyQt6.QtCore import Qt
from core.controllers.ProfileController import ProfileController
from core.controllers.profile_state.update_profile import update_profile
from gui.v2.actions.database_health import GuiStorageDatabaseError
from gui.v2.actions.editor_data import (
EDITABLE_FIELDS, EditorCallback, EditorResult, EditorState,
copy_editor_state, location_record, prepare_editor_data, render_editor_state, set_editor_value, snapshot_profile,
)
from gui.v2.actions.profile_operations import current_connection, synchronize
from gui.v2.workers.page_data_worker import PageDataWorker
_active_workers: set[PageDataWorker] = set()
def _release_worker(worker: PageDataWorker) -> None:
"""Keep worker references until native completion so page navigation cannot destroy running tasks."""
_active_workers.discard(worker)
worker.deleteLater()
def _report_task_failure(on_result: EditorCallback, request_name: str, message: str) -> None:
"""Restore GUI controls even if the worker cannot format an unexpected task failure."""
on_result(request_name, {"status": "error", "message": message})
def _execute(operation: Callable[[], EditorResult], state: EditorState | None) -> EditorResult:
"""Turn background exceptions into explicit outcomes that leave pending changes available."""
try:
return operation()
except GuiStorageDatabaseError:
message = "Local storage database could not be read. Restart and recover storage.db."
except Exception as error:
message = str(error) or "The editor operation failed."
result: EditorResult = {"status": "error", "message": message}
if state is not None:
result.update(render_editor_state(state, "error"))
return result
def _start_task(request_name: str, operation: Callable[[], EditorResult], on_result: EditorCallback, state: EditorState | None = None) -> PageDataWorker:
"""Own worker construction and GUI signal delivery so editor handlers only pass task inputs."""
worker = PageDataWorker(request_name, _execute, operation, state)
worker.data_ready.connect(on_result, Qt.ConnectionType.QueuedConnection)
worker.failed.connect(partial(_report_task_failure, on_result), Qt.ConnectionType.QueuedConnection)
worker.finished.connect(partial(_release_worker, worker))
_active_workers.add(worker)
worker.start()
return worker
def _load_editor(profile_id: int | None, location_choices: list[str], browser_choices: list[str], resolutions: list[str], is_synced: bool,
prepared: Mapping[str, dict[str, str]] | None, previous: EditorState | None) -> EditorResult:
"""Load one editing baseline and location metadata while preserving an existing unsaved draft."""
if profile_id is None:
return {"status": "error", "message": "Select a profile before opening the editor."}
profile_id = int(profile_id)
baseline = previous["baseline"] if previous is not None and previous["changes"] else None
profile = ProfileController.get(profile_id) if baseline is None else None
if baseline is None and profile is None:
return {"status": "error", "message": f"Profile {profile_id} no longer exists."}
if baseline is None:
baseline = snapshot_profile(profile)
location_map = dict(prepared) if prepared is not None else prepare_editor_data()
if not set(location_choices).issubset(location_map):
location_map = prepare_editor_data()
location_metadata = baseline["location_metadata"]
if location_metadata:
location_map[location_metadata["key"]] = location_metadata
state: EditorState = {
"profile_id": profile_id, "baseline": dict(baseline),
"changes": dict(previous["changes"]) if previous is not None else {},
"locations": location_map, "location_choices": list(dict.fromkeys(location_choices)),
"browser_choices": list(dict.fromkeys(browser_choices)), "resolutions": list(dict.fromkeys(resolutions)),
"is_synced": is_synced,
}
return render_editor_state(state)
def load_editor(profile_id: int | None, location_choices: Sequence[str], browser_choices: Sequence[str], resolutions: Sequence[str], is_synced: bool,
prepared: Mapping[str, dict[str, str]] | None, previous: EditorState | None, request_name: str, on_result: EditorCallback) -> PageDataWorker:
"""Prepare editor content asynchronously without letting the page coordinate controller lookups."""
captured = copy_editor_state(previous) if previous is not None else None
operation = partial(_load_editor, profile_id, list(location_choices), list(browser_choices), list(resolutions), is_synced, prepared, captured)
return _start_task(request_name, operation, on_result, captured)
def _sync_editor(state: EditorState) -> EditorResult:
"""Replace available choices only after synchronization succeeds while retaining the current draft."""
synced = synchronize(current_connection())
if synced["status"] == "error":
return {**render_editor_state(state, "error"), "message": synced["message"], "sync_data": synced["sync_data"]}
locations, browsers, _, _, location_models, _ = synced["sync_data"]
records = [location_record(location) for location in location_models]
updated: EditorState = {**state, "location_choices": locations, "browser_choices": browsers,
"locations": {record["key"]: record for record in records if record}, "is_synced": True}
return {**render_editor_state(updated), "sync_data": synced["sync_data"], "message": "Sync complete."}
def cycle_editor_value(state: EditorState, key: str, direction: int, request_name: str, on_result: EditorCallback) -> PageDataWorker | None:
"""Share previous and next selection logic and own the synchronization needed to populate choices."""
captured = copy_editor_state(state)
field = next((field for field in render_editor_state(captured)["view"]["fields"] if field["key"] == key), None)
if field is None or not field["enabled"] or not field["visible"] or direction not in (-1, 1):
on_result(request_name, {"status": "error", "message": "This editor field cannot be changed."})
return None
if field["needs_sync"]:
return _start_task(request_name, partial(_sync_editor, captured), on_result, captured)
choices = field["choices"]
if not choices:
on_result(request_name, {"status": "error", "message": f"No {key} data available. Sync the database and try again."})
return None
current = field["value"]
index = choices.index(current) if current in choices else (-1 if direction == 1 else 0)
on_result(request_name, set_editor_value(captured, key, choices[(index + direction) % len(choices)]))
return None
def _confirmation(state: EditorState, task: str, message: str) -> EditorResult:
"""Capture the profile and draft together so confirmations cannot apply subsequently edited values."""
return {"status": "confirmation", "message": message,
"request": {"task": task, "state": copy_editor_state(state)}}
def _apply_warning(state: EditorState) -> str:
"""Include every destructive change in one confirmation rather than hiding the browser warning behind location changes."""
warnings = {
"location": "A new location requires a new subscription code and erases the old one.",
"browser": "Changing the browser would delete all data associated with it.",
}
messages = [message for field, message in warnings.items() if field in state["changes"]]
return "\n\n".join([*messages, "Do you want to proceed?"]) if messages else ""
def _save_editor(state: EditorState) -> EditorResult:
"""Track each core save so partial failures retain only the changes still needing attention."""
pending = state["changes"]
unsupported = set(pending).difference(EDITABLE_FIELDS)
if unsupported:
return {**render_editor_state(state, "error"), "message": "These editor changes are not supported; protocol switching is disabled."}
updated = copy_editor_state(state)
saved: list[str] = []
for key, value in pending.items():
try:
outcome = update_profile(state["profile_id"], key, value)
message = None if outcome.valid else str(outcome.message or outcome.data or f"Could not save {key}.")
except Exception as error:
message = str(error) or f"Could not save {key}."
if message is not None:
progress = f"Saved {', '.join(saved)}. " if saved else ""
return {**render_editor_state(updated, "error"), "message": f"{progress}Could not save {key}: {message}",
"saved_fields": saved, "refresh_menu": bool(saved)}
updated["baseline"][key] = value
if key == "browser":
updated["baseline"]["browser_supported"] = value in updated["browser_choices"]
updated["changes"].pop(key, None)
saved.append(key)
return {"status": "navigate", "destination": "menu", "state": updated, "refresh_menu": True,
"saved_fields": saved, "message": f"Updated profile {state['profile_id']}"}
def apply_editor_changes(state: EditorState, request_name: str, on_result: EditorCallback, confirmed: bool = False) -> PageDataWorker | None:
"""Own confirmation and saving as one user task so the GUI never chains individual core updates."""
captured = copy_editor_state(state)
if not captured["changes"]:
on_result(request_name, {"status": "navigate", "destination": "menu", "state": captured})
return None
warning = _apply_warning(captured)
if warning and not confirmed:
on_result(request_name, _confirmation(captured, "apply", warning))
return None
return _start_task(request_name, partial(_save_editor, captured), on_result, captured)
def leave_editor(state: EditorState, request_name: str, on_result: EditorCallback, confirmed: bool = False) -> None:
"""Discard changes only after the user accepts leaving an editor with an unsaved draft."""
captured = copy_editor_state(state)
if captured["changes"] and not confirmed:
on_result(request_name, _confirmation(captured, "leave", "You have unsaved changes. Do you want to continue?"))
return
captured["changes"].clear()
on_result(request_name, {"status": "navigate", "destination": "menu", "state": captured})
def continue_editor_task(request: Mapping[str, Any], accepted: bool, request_name: str, on_result: EditorCallback) -> PageDataWorker | None:
"""Return popup decisions to the original task without rebuilding its profile or draft from the page."""
state = request["state"]
if not accepted:
on_result(request_name, render_editor_state(state))
return None
handlers = {
"apply": partial(apply_editor_changes, state, request_name, on_result, confirmed=True),
"leave": partial(leave_editor, state, request_name, on_result, confirmed=True),
}
return handlers[request["task"]]()

View file

@ -124,7 +124,7 @@ def profile_is_session(profile: BaseProfile | None) -> bool:
return isinstance(profile, SessionProfile) return isinstance(profile, SessionProfile)
def _matched_profile(profile_id: int, profile: BaseProfile) -> tuple[str, dict[str, Any]]: def summarize_profile(profile_id: int, profile: BaseProfile) -> tuple[str, dict[str, Any]]:
"""Build one normalized menu record so ordering does not contain profile-shape branches.""" """Build one normalized menu record so ordering does not contain profile-shape branches."""
protocol = connection_code(profile) protocol = connection_code(profile)
display_protocol = protocol if protocol in ENCRYPTED_PROTOCOLS else "hidetor" display_protocol = protocol if protocol in ENCRYPTED_PROTOCOLS else "hidetor"
@ -154,7 +154,7 @@ def match_core_profiles(profiles: Mapping[int, BaseProfile], gui_config_file: st
"""Translate core profiles into stable menu data before any Qt widgets are created.""" """Translate core profiles into stable menu data before any Qt widgets are created."""
profile_ids = normalize_profile_order(gui_config_file, profiles.keys()) profile_ids = normalize_profile_order(gui_config_file, profiles.keys())
matched_profiles = ( matched_profiles = (
_matched_profile(profile_id, profiles[profile_id]) summarize_profile(profile_id, profiles[profile_id])
for profile_id in profile_ids for profile_id in profile_ids
if profile_id in profiles if profile_id in profiles
) )
@ -231,9 +231,7 @@ def _profile_detail_data(profile: BaseProfile | None, summary: Mapping[str, Any]
], ],
"location_text": f"{profile_location.country_name}, {profile_location.name}" if profile_location else "", "location_text": f"{profile_location.country_name}, {profile_location.name}" if profile_location else "",
"background_asset": _detail_background(summary, display_mode, profile_is_session(profile)), "background_asset": _detail_background(summary, display_mode, profile_is_session(profile)),
"primary_asset": f"{protocol}.png" if protocol in ("hysteria2", "vless") else f"{protocol}_{summary['location']}.png", "primary_asset": f"{protocol}_{summary['location']}.png",
"location_overlay": protocol in ("hysteria2", "vless"),
"overlay_y": {"hysteria2": 0.35}.get(protocol, 0.30),
"raised_primary": protocol == "hidetor" or (protocol == "wireguard" and summary["connection"] == "browser-only"), "raised_primary": protocol == "hidetor" or (protocol == "wireguard" and summary["connection"] == "browser-only"),
"operator_name": operator_name, "operator_name": operator_name,
"nostr_public_key": getattr(operator, "nostr_public_key", "") if operator else "", "nostr_public_key": getattr(operator, "nostr_public_key", "") if operator else "",
@ -280,7 +278,7 @@ def get_profile_details(
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Load and prepare profile details as one task so the page never coordinates core model helpers.""" """Load and prepare profile details as one task so the page never coordinates core model helpers."""
profile = load_profile(profile_id, on_error) profile = load_profile(profile_id, on_error)
current_summary = _matched_profile(profile_id, profile)[1] if profile is not None else summary current_summary = summarize_profile(profile_id, profile)[1] if profile is not None else summary
return _profile_detail_data(profile, current_summary, is_connected) return _profile_detail_data(profile, current_summary, is_connected)

File diff suppressed because it is too large Load diff

View file

@ -419,10 +419,10 @@ class MenuPage(Page):
self._detail_image("verified_profile.png", (20, 50, 100, 80)) self._detail_image("verified_profile.png", (20, 50, 100, 80))
self.add_selected_profile(details["selection"]) self.add_selected_profile(details["selection"])
def _detail_image(self, asset, geometry, lower=False, pixmap=None): def _detail_image(self, asset, geometry, lower=False):
label = QLabel(self) label = QLabel(self)
label.setGeometry(*geometry) label.setGeometry(*geometry)
label.setPixmap(pixmap if pixmap is not None else QPixmap(os.path.join(self.btn_path, asset))) label.setPixmap(QPixmap(os.path.join(self.btn_path, asset)))
label.setScaledContents(True) label.setScaledContents(True)
label.show() label.show()
if lower: if lower:
@ -434,10 +434,7 @@ class MenuPage(Page):
self.additional_labels.append(self.create_verification_widget(details["verification_values"])) self.additional_labels.append(self.create_verification_widget(details["verification_values"]))
def _render_protocol_image(self, details): def _render_protocol_image(self, details):
pixmap = QPixmap(os.path.join(self.btn_path, details["primary_asset"])) self._detail_image(details["primary_asset"], (0, 80 if details["raised_primary"] else 90, 400, 300))
if details["location_overlay"]:
pixmap = self.build_encrypted_proxy_detail_pixmap(pixmap, details["location"], details["overlay_y"])
self._detail_image(details["primary_asset"], (0, 80 if details["raised_primary"] else 90, 400, 300), pixmap=pixmap)
def _render_operator_details(self, details): def _render_operator_details(self, details):
color = "black" if details["is_session"] else "white" color = "black" if details["is_session"] else "white"
@ -460,34 +457,6 @@ class MenuPage(Page):
widget.show() widget.show()
self.additional_labels.append(widget) self.additional_labels.append(widget)
def build_encrypted_proxy_detail_pixmap(self, pixmap, location, overlay_y):
if pixmap.isNull():
return pixmap
location_pixmap = QPixmap(os.path.join(
self.btn_path, f"icon_mini_{location}.png"))
if location_pixmap.isNull():
location_pixmap = QPixmap(os.path.join(
self.btn_path, "default_location_mini.png"))
if location_pixmap.isNull():
return pixmap
icon_size = max(42, min(72, int(min(pixmap.width(), pixmap.height()) * 0.22)))
location_pixmap = location_pixmap.scaled(
icon_size,
icon_size,
Qt.AspectRatioMode.KeepAspectRatio,
Qt.TransformationMode.SmoothTransformation)
x = int(pixmap.width() * 0.58)
y = int(pixmap.height() * overlay_y)
x = min(max(0, x), max(0, pixmap.width() - location_pixmap.width()))
y = min(max(0, y), max(0, pixmap.height() - location_pixmap.height()))
painter = QPainter(pixmap)
painter.drawPixmap(x, y, location_pixmap)
painter.end()
return pixmap
def show_profile_verification(self, profile_id): def show_profile_verification(self, profile_id):
location_icon_name = find_verification_icon( location_icon_name = find_verification_icon(
profile_id, profile_id,