# Copyright (C) 2024–present Loren Eteval & contributors # # This file is part of Furious. # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Own application routing selection independently from its UI controls.""" from __future__ import annotations from Furious.Frozenlib import ( AppConnectionController, AppBuiltinRouting, AppSettings, registerAppSettings, ) from Furious.Plugins import getPluginRegistry from Furious.Repository import Storage from PySide6 import QtCore __all__ = ['RoutingController'] registerAppSettings('Routing', default=AppBuiltinRouting.BypassMainlandChina.value) class RoutingController(QtCore.QObject): """Expose one routing state shared by every application surface.""" stateChanged = QtCore.Signal(object, str) optionsChanged = QtCore.Signal(object) routingChanged = QtCore.Signal(str) interactionEnabledChanged = QtCore.Signal(bool) def __init__(self, parent=None): """Initialize persisted routing state and plugin-provided options.""" super().__init__(parent) if AppSettings.get('Routing') == 'Bypass': # Update the value used by older Furious releases. AppSettings.set('Routing', AppBuiltinRouting.BypassMainlandChina.value) self._options = tuple() self._routing = str(AppSettings.get('Routing')) self._interactionEnabled = True self.refresh() @staticmethod def currentProfileForRouting(): """Return the connected profile, falling back to repository selection.""" try: profile = AppConnectionController().activeProfile if profile is not None: return profile except (AttributeError, RuntimeError): # The connection controller may not exist during early startup. pass try: index, servers = ( Storage.UserActivatedItemIndex(), Storage.UserServers(), ) if 0 <= index < len(servers): return servers[index] except Exception: # Any non-exit exceptions # The controller can exist before persistent storage is ready. pass return None @property def options(self): """Return routing options supported by the active configuration.""" return self._options @property def routing(self) -> str: """Return the normalized route shown by routing controls.""" return self._routing @property def interactionEnabled(self) -> bool: """Return whether users may change routing at this lifecycle state.""" return self._interactionEnabled def state(self): """Return the current immutable option and selection snapshot.""" return self.options, self.routing def refresh(self, *, force=False): """Refresh routing capabilities from the active proxy-core plugin.""" profile = self.currentProfileForRouting() registry = getPluginRegistry() options = registry.routingOptions(profile) if profile is not None else tuple() routing = ( registry.normalizeRouting(profile, AppSettings.get('Routing')) if profile is not None else str(AppSettings.get('Routing')) ) optionsChanged = options != self._options routingChanged = routing != self._routing self._options = options self._routing = routing if optionsChanged: self.optionsChanged.emit(options) if routingChanged: self.routingChanged.emit(routing) if force or optionsChanged or routingChanged: self.stateChanged.emit(options, routing) return self.state() def invalidateRouting(self, routing: str) -> bool: """Persist the fallback after an explicit removal or disabling of a choice.""" if AppSettings.get('Routing') != routing: return False profile = self.currentProfileForRouting() if profile is None: return False fallback = getPluginRegistry().normalizeRouting(profile, routing) if fallback == routing: return False AppSettings.set('Routing', fallback) self.refresh(force=True) return True @QtCore.Slot(str) def selectRouting(self, routing: str) -> bool: """Persist a supported route and reconnect the active proxy if needed.""" options, _current = self.refresh() if routing not in tuple(option.id for option in options): return False if AppSettings.get('Routing') == routing: return False AppSettings.set('Routing', routing) self.refresh(force=True) connectionController = AppConnectionController() if connectionController.isConnected(): connectionController.startReconnection() return True @QtCore.Slot(bool) def setInteractionEnabled(self, enabled: bool): """Publish whether routing controls may accept user interaction.""" enabled = bool(enabled) if enabled == self._interactionEnabled: return self._interactionEnabled = enabled self.interactionEnabledChanged.emit(enabled)