mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
Merge pull request #2649 from Mathew-D/v5
Added VPN's to the Network tab
This commit is contained in:
@@ -237,6 +237,9 @@
|
||||
"wifi": "Wi-Fi",
|
||||
"password-prompt": "Enter password",
|
||||
"password-prompt-for": "Enter password for {ssid}",
|
||||
"vpn": "VPN",
|
||||
"vpns": "VPNs",
|
||||
"wireless": "Wireless",
|
||||
"unavailable-title": "NetworkManager unavailable",
|
||||
"unavailable-detail": "Install and enable NetworkManager to manage networks from here.",
|
||||
"no-networks": "No networks in range"
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <map>
|
||||
#include <sdbus-c++/IProxy.h>
|
||||
#include <sdbus-c++/Types.h>
|
||||
#include <set>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
@@ -98,6 +99,9 @@ namespace {
|
||||
} // namespace
|
||||
|
||||
const char* NetworkService::glyphForState(const NetworkState& state) noexcept {
|
||||
if (state.vpnActive) {
|
||||
return "shield-check";
|
||||
}
|
||||
if (state.kind == NetworkConnectivity::Wired) {
|
||||
return state.connected ? "ethernet" : "ethernet-off";
|
||||
}
|
||||
@@ -197,18 +201,21 @@ void NetworkService::setChangeCallback(ChangeCallback callback) { m_changeCallba
|
||||
|
||||
void NetworkService::refresh() {
|
||||
const auto previousAps = m_accessPoints;
|
||||
const auto previousVpns = m_vpnConnections;
|
||||
const auto previousSaved = m_savedSsids;
|
||||
refreshAccessPoints();
|
||||
refreshVpnConnections();
|
||||
refreshSavedConnections();
|
||||
NetworkState next = readState();
|
||||
const bool apsChanged = previousAps != m_accessPoints;
|
||||
const bool vpnsChanged = previousVpns != m_vpnConnections;
|
||||
const bool savedChanged = previousSaved != m_savedSsids;
|
||||
const bool stateChanged = next != m_state;
|
||||
const bool wirelessEnabledChanged = next.wirelessEnabled != m_state.wirelessEnabled;
|
||||
const NetworkChangeOrigin origin =
|
||||
wirelessEnabledChanged ? consumeWirelessEnabledChangeOrigin(next.wirelessEnabled) : NetworkChangeOrigin::External;
|
||||
m_state = std::move(next);
|
||||
if ((stateChanged || apsChanged || savedChanged) && m_changeCallback) {
|
||||
if ((stateChanged || apsChanged || vpnsChanged || savedChanged) && m_changeCallback) {
|
||||
m_changeCallback(m_state, origin);
|
||||
}
|
||||
}
|
||||
@@ -301,6 +308,76 @@ bool NetworkService::activateAccessPoint(const AccessPointInfo& ap) {
|
||||
}
|
||||
}
|
||||
|
||||
bool NetworkService::activateVpnConnection(const VpnConnectionInfo& vpn) {
|
||||
if (vpn.path.empty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
// Async: ActivateConnection can involve polkit/agent interactions, and a
|
||||
// synchronous call can stall the main loop while authorization is pending.
|
||||
const std::string vpnName = vpn.name;
|
||||
const std::string vpnPath = vpn.path;
|
||||
m_nm->callMethodAsync("ActivateConnection")
|
||||
.onInterface(k_nmInterface)
|
||||
.withArguments(sdbus::ObjectPath{vpnPath}, sdbus::ObjectPath{"/"}, sdbus::ObjectPath{"/"})
|
||||
.uponReplyInvoke([vpnName, vpnPath](std::optional<sdbus::Error> err, sdbus::ObjectPath activePath) {
|
||||
if (err.has_value()) {
|
||||
kLog.warn("ActivateConnection(vpn) failed name={} path={}: {}", vpnName, vpnPath, err->what());
|
||||
} else {
|
||||
kLog.info("activating vpn name={} active={}", vpnName, std::string(activePath));
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.warn("ActivateConnection(vpn) failed name={} path={} err={}", vpn.name, vpn.path, e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool NetworkService::deactivateVpnConnection(const VpnConnectionInfo& vpn) {
|
||||
if (vpn.path.empty()) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
std::vector<sdbus::ObjectPath> activeConnections;
|
||||
const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
||||
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
||||
for (const auto& activePath : activeConnections) {
|
||||
try {
|
||||
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
||||
const auto profilePath =
|
||||
getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface, "Connection", sdbus::ObjectPath{});
|
||||
const auto activeState = getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
|
||||
if (profilePath != vpn.path || activeState != k_nmActiveConnectionStateActivated) {
|
||||
continue;
|
||||
}
|
||||
// Async: DeactivateConnection on a system-owned profile is gated by polkit,
|
||||
// and a sync call would freeze the main loop while the polkit agent prompts
|
||||
// (or while polkit waits for an agent to register). Fire-and-forget here.
|
||||
const std::string activePathStr = std::string(activePath);
|
||||
const std::string vpnName = vpn.name;
|
||||
m_nm->callMethodAsync("DeactivateConnection")
|
||||
.onInterface(k_nmInterface)
|
||||
.withArguments(sdbus::ObjectPath{activePathStr})
|
||||
.uponReplyInvoke([activePathStr, vpnName](std::optional<sdbus::Error> err) {
|
||||
if (err.has_value()) {
|
||||
kLog.warn("DeactivateConnection(vpn) failed name={} active={}: {}", vpnName, activePathStr,
|
||||
err->what());
|
||||
} else {
|
||||
kLog.info("deactivated vpn name={} active={}", vpnName, activePathStr);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.warn("DeactivateConnection(vpn) lookup failed path={}: {}", vpn.path, e.what());
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void NetworkService::setWirelessEnabled(bool enabled) {
|
||||
if (enabled != m_state.wirelessEnabled) {
|
||||
m_pendingLocalWirelessEnabled = enabled;
|
||||
@@ -517,6 +594,102 @@ void NetworkService::refreshSavedConnections() {
|
||||
m_savedSsids = std::move(next);
|
||||
}
|
||||
|
||||
void NetworkService::refreshVpnConnections() {
|
||||
std::vector<VpnConnectionInfo> next;
|
||||
std::set<std::string> vpnProfilePaths;
|
||||
try {
|
||||
auto settings = sdbus::createProxy(m_bus.connection(), k_nmBusName, k_nmSettingsObjectPath);
|
||||
std::vector<sdbus::ObjectPath> connectionPaths;
|
||||
settings->callMethod("ListConnections").onInterface(k_nmSettingsInterface).storeResultsTo(connectionPaths);
|
||||
for (const auto& connectionPath : connectionPaths) {
|
||||
try {
|
||||
auto connection = sdbus::createProxy(m_bus.connection(), k_nmBusName, connectionPath);
|
||||
std::map<std::string, std::map<std::string, sdbus::Variant>> cfg;
|
||||
connection->callMethod("GetSettings").onInterface(k_nmSettingsConnectionInterface).storeResultsTo(cfg);
|
||||
auto connIt = cfg.find("connection");
|
||||
if (connIt == cfg.end()) {
|
||||
continue;
|
||||
}
|
||||
auto typeIt = connIt->second.find("type");
|
||||
if (typeIt == connIt->second.end()) {
|
||||
continue;
|
||||
}
|
||||
std::string type;
|
||||
try {
|
||||
type = typeIt->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bool hasVpnSection = cfg.contains("vpn");
|
||||
const bool vpnLikeType = type == "vpn" || type == "wireguard";
|
||||
if (!vpnLikeType && !hasVpnSection) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VpnConnectionInfo info;
|
||||
info.path = std::string(connectionPath);
|
||||
auto idIt = connIt->second.find("id");
|
||||
if (idIt != connIt->second.end()) {
|
||||
try {
|
||||
info.name = idIt->second.get<std::string>();
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
if (info.name.empty()) {
|
||||
info.name = info.path;
|
||||
}
|
||||
info.active = false;
|
||||
vpnProfilePaths.insert(info.path);
|
||||
next.push_back(std::move(info));
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("refreshVpnConnections: {}", e.what());
|
||||
}
|
||||
|
||||
if (!next.empty()) {
|
||||
try {
|
||||
std::vector<sdbus::ObjectPath> activeConnections;
|
||||
const sdbus::Variant activeVar = m_nm->getProperty("ActiveConnections").onInterface(k_nmInterface);
|
||||
activeConnections = activeVar.get<std::vector<sdbus::ObjectPath>>();
|
||||
for (const auto& activePath : activeConnections) {
|
||||
try {
|
||||
auto active = sdbus::createProxy(m_bus.connection(), k_nmBusName, activePath);
|
||||
const auto state = getPropertyOr<std::uint32_t>(*active, k_nmActiveConnectionInterface, "State", 0U);
|
||||
if (state != k_nmActiveConnectionStateActivated) {
|
||||
continue;
|
||||
}
|
||||
const auto profilePath = getPropertyOr<sdbus::ObjectPath>(*active, k_nmActiveConnectionInterface,
|
||||
"Connection", sdbus::ObjectPath{});
|
||||
const std::string profilePathStr = std::string(profilePath);
|
||||
if (!vpnProfilePaths.contains(profilePathStr)) {
|
||||
continue;
|
||||
}
|
||||
for (auto& vpn : next) {
|
||||
if (vpn.path == profilePathStr) {
|
||||
vpn.active = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error&) {
|
||||
}
|
||||
}
|
||||
} catch (const sdbus::Error& e) {
|
||||
kLog.debug("refreshVpnConnections active list: {}", e.what());
|
||||
}
|
||||
}
|
||||
|
||||
std::ranges::sort(next, [](const VpnConnectionInfo& a, const VpnConnectionInfo& b) {
|
||||
if (a.active != b.active) {
|
||||
return a.active;
|
||||
}
|
||||
return a.name < b.name;
|
||||
});
|
||||
m_vpnConnections = std::move(next);
|
||||
}
|
||||
|
||||
void NetworkService::ensureWifiDeviceSubscribed(const std::string& devicePath) {
|
||||
if (m_wifiDevices.contains(devicePath)) {
|
||||
return;
|
||||
@@ -776,6 +949,27 @@ NetworkState NetworkService::readState() {
|
||||
|
||||
next.wirelessEnabled = getPropertyOr<bool>(*m_nm, k_nmInterface, "WirelessEnabled", false);
|
||||
next.scanning = m_scanning;
|
||||
next.vpnActive = false;
|
||||
|
||||
// Check primary connection: detect VPN type and connection state
|
||||
if (m_activeConnection != nullptr) {
|
||||
const auto type =
|
||||
getPropertyOr<std::string>(*m_activeConnection, k_nmActiveConnectionInterface, "Type", std::string{});
|
||||
next.vpnActive = (type == "vpn" || type == "wireguard");
|
||||
const auto state = getPropertyOr<std::uint32_t>(*m_activeConnection, k_nmActiveConnectionInterface, "State", 0U);
|
||||
next.connected = state == k_nmActiveConnectionStateActivated;
|
||||
}
|
||||
|
||||
// Also check if any VPN profile is active (in case it's not the primary connection)
|
||||
if (!next.vpnActive) {
|
||||
for (const auto& vpn : m_vpnConnections) {
|
||||
if (vpn.active) {
|
||||
next.vpnActive = true;
|
||||
next.connected = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (m_activeDevice == nullptr) {
|
||||
return next;
|
||||
@@ -788,11 +982,6 @@ NetworkState NetworkService::readState() {
|
||||
getPropertyOr<sdbus::ObjectPath>(*m_activeDevice, k_nmDeviceInterface, "Ip4Config", sdbus::ObjectPath{});
|
||||
next.ipv4 = firstIpv4FromConfig(m_bus.connection(), ip4ConfigPath);
|
||||
|
||||
if (m_activeConnection != nullptr) {
|
||||
const auto state = getPropertyOr<std::uint32_t>(*m_activeConnection, k_nmActiveConnectionInterface, "State", 0U);
|
||||
next.connected = state == k_nmActiveConnectionStateActivated;
|
||||
}
|
||||
|
||||
if (deviceType == k_nmDeviceTypeWifi) {
|
||||
next.kind = NetworkConnectivity::Wireless;
|
||||
if (m_activeAp != nullptr) {
|
||||
|
||||
@@ -25,6 +25,14 @@ struct AccessPointInfo {
|
||||
bool operator==(const AccessPointInfo&) const = default;
|
||||
};
|
||||
|
||||
struct VpnConnectionInfo {
|
||||
std::string path; // NM Settings.Connection object path
|
||||
std::string name;
|
||||
bool active = false;
|
||||
|
||||
bool operator==(const VpnConnectionInfo&) const = default;
|
||||
};
|
||||
|
||||
enum class NetworkConnectivity {
|
||||
Unknown = 0,
|
||||
None = 1,
|
||||
@@ -37,6 +45,7 @@ struct NetworkState {
|
||||
bool connected = false;
|
||||
bool wirelessEnabled = false;
|
||||
bool scanning = false;
|
||||
bool vpnActive = false; // true if a VPN is the active connection
|
||||
std::string ssid; // Wi-Fi only
|
||||
std::string ipv4; // dotted-quad of first address; empty if none
|
||||
std::string interfaceName; // e.g. "wlan0", "eth0"
|
||||
@@ -65,6 +74,7 @@ public:
|
||||
|
||||
[[nodiscard]] const NetworkState& state() const noexcept { return m_state; }
|
||||
[[nodiscard]] const std::vector<AccessPointInfo>& accessPoints() const noexcept { return m_accessPoints; }
|
||||
[[nodiscard]] const std::vector<VpnConnectionInfo>& vpnConnections() const noexcept { return m_vpnConnections; }
|
||||
[[nodiscard]] static const char* glyphForState(const NetworkState& state) noexcept;
|
||||
[[nodiscard]] static const char* wifiGlyphForState(const NetworkState& state) noexcept;
|
||||
[[nodiscard]] static const char* wifiGlyphForSignal(std::uint8_t signal) noexcept;
|
||||
@@ -79,6 +89,10 @@ public:
|
||||
// handles that via a SecretAgent).
|
||||
bool activateAccessPoint(const AccessPointInfo& ap);
|
||||
|
||||
// Activate / deactivate a saved VPN connection profile.
|
||||
bool activateVpnConnection(const VpnConnectionInfo& vpn);
|
||||
bool deactivateVpnConnection(const VpnConnectionInfo& vpn);
|
||||
|
||||
// Enable / disable the Wi-Fi radio.
|
||||
void setWirelessEnabled(bool enabled);
|
||||
|
||||
@@ -94,6 +108,7 @@ public:
|
||||
private:
|
||||
void refreshAccessPoints();
|
||||
void refreshSavedConnections();
|
||||
void refreshVpnConnections();
|
||||
void rebindActiveConnection();
|
||||
void rebindActiveDevice(const std::string& devicePath);
|
||||
void rebindActiveAccessPoint(const std::string& apPath);
|
||||
@@ -113,6 +128,7 @@ private:
|
||||
std::string m_activeApPath;
|
||||
NetworkState m_state;
|
||||
std::vector<AccessPointInfo> m_accessPoints;
|
||||
std::vector<VpnConnectionInfo> m_vpnConnections;
|
||||
std::vector<std::string> m_savedSsids;
|
||||
bool m_scanning = false;
|
||||
std::int64_t m_scanBaselineLastScan = 0;
|
||||
|
||||
@@ -191,6 +191,7 @@ const std::unordered_map<std::string, char32_t> kIcons = {
|
||||
{"ethernet-off", 0xECCD},
|
||||
{"ethernet-exclamation", 0xECCE},
|
||||
{"ethernet-question", 0xECCF},
|
||||
{"shield-check", 0xEB22},
|
||||
|
||||
// Bluetooth devices
|
||||
{"bluetooth", 0xEA37},
|
||||
|
||||
@@ -187,6 +187,120 @@ namespace {
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
class VpnConnectionRow : public Flex {
|
||||
public:
|
||||
VpnConnectionRow(float scale, VpnConnectionInfo vpn, std::function<void(const VpnConnectionInfo&)> onActivate,
|
||||
std::function<void(const VpnConnectionInfo&)> onDeactivate)
|
||||
: m_vpn(std::move(vpn)), m_onActivate(std::move(onActivate)), m_onDeactivate(std::move(onDeactivate)) {
|
||||
setDirection(FlexDirection::Horizontal);
|
||||
setAlign(FlexAlign::Center);
|
||||
setGap(Style::spaceSm * scale);
|
||||
setPadding(Style::spaceSm * scale, Style::spaceMd * scale);
|
||||
setMinHeight(kRowMinHeight * scale);
|
||||
setRadius(Style::radiusMd * scale);
|
||||
setFill(colorSpecFromRole(ColorRole::Surface));
|
||||
clearBorder();
|
||||
|
||||
auto kind = std::make_unique<Label>();
|
||||
kind->setText(i18n::tr("control-center.network.vpn"));
|
||||
kind->setCaptionStyle();
|
||||
kind->setFontSize(Style::fontSizeCaption * scale);
|
||||
kind->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
addChild(std::move(kind));
|
||||
|
||||
auto name = std::make_unique<Label>();
|
||||
name->setText(m_vpn.name);
|
||||
name->setBold(m_vpn.active);
|
||||
name->setFontSize(Style::fontSizeBody * scale);
|
||||
name->setColor(colorSpecFromRole(ColorRole::OnSurface));
|
||||
name->setFlexGrow(1.0f);
|
||||
m_title = name.get();
|
||||
addChild(std::move(name));
|
||||
|
||||
auto action = std::make_unique<Button>();
|
||||
action->setVariant(m_vpn.active ? ButtonVariant::Destructive : ButtonVariant::Default);
|
||||
action->setText(i18n::tr(m_vpn.active ? "control-center.network.disconnect" : "control-center.network.connect"));
|
||||
action->setOnClick([this]() { triggerAction(); });
|
||||
m_actionButton = static_cast<Button*>(addChild(std::move(action)));
|
||||
|
||||
auto area = std::make_unique<InputArea>();
|
||||
area->setPropagateEvents(true);
|
||||
area->setOnEnter([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnLeave([this]() { applyState(); });
|
||||
area->setOnPress([this](const InputArea::PointerData& /*data*/) { applyState(); });
|
||||
area->setOnClick([this](const InputArea::PointerData& /*data*/) { triggerAction(); });
|
||||
m_inputArea = static_cast<InputArea*>(addChild(std::move(area)));
|
||||
|
||||
applyState();
|
||||
m_paletteConn = paletteChanged().connect([this] { applyState(); });
|
||||
}
|
||||
|
||||
void doLayout(Renderer& renderer) override {
|
||||
if (m_inputArea == nullptr) {
|
||||
return;
|
||||
}
|
||||
m_inputArea->setVisible(false);
|
||||
Flex::doLayout(renderer);
|
||||
m_inputArea->setVisible(true);
|
||||
m_inputArea->setPosition(0.0f, 0.0f);
|
||||
m_inputArea->setSize(width(), height());
|
||||
if (m_actionButton != nullptr) {
|
||||
const float areaWidth = std::max(0.0f, m_actionButton->x() - gap());
|
||||
m_inputArea->setSize(areaWidth, height());
|
||||
}
|
||||
applyState();
|
||||
}
|
||||
|
||||
LayoutSize doMeasure(Renderer& renderer, const LayoutConstraints& constraints) override {
|
||||
return measureByLayout(renderer, constraints);
|
||||
}
|
||||
|
||||
void doArrange(Renderer& renderer, const LayoutRect& rect) override { arrangeByLayout(renderer, rect); }
|
||||
|
||||
private:
|
||||
void triggerAction() {
|
||||
if (m_vpn.active) {
|
||||
if (m_onDeactivate) {
|
||||
m_onDeactivate(m_vpn);
|
||||
}
|
||||
} else {
|
||||
if (m_onActivate) {
|
||||
m_onActivate(m_vpn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void applyState() {
|
||||
const bool hov = m_inputArea != nullptr && m_inputArea->hovered();
|
||||
const bool pressed = m_inputArea != nullptr && m_inputArea->pressed();
|
||||
if (pressed) {
|
||||
setFill(colorSpecFromRole(ColorRole::Primary));
|
||||
setBorder(colorSpecFromRole(ColorRole::Primary), Style::borderWidth);
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(colorSpecFromRole(ColorRole::OnPrimary));
|
||||
}
|
||||
return;
|
||||
}
|
||||
setFill(colorSpecFromRole(m_vpn.active ? ColorRole::SurfaceVariant : ColorRole::Surface));
|
||||
if (hov) {
|
||||
setBorder(colorSpecFromRole(ColorRole::Primary), Style::borderWidth);
|
||||
} else {
|
||||
clearBorder();
|
||||
}
|
||||
if (m_title != nullptr) {
|
||||
m_title->setColor(colorSpecFromRole(ColorRole::OnSurface));
|
||||
}
|
||||
}
|
||||
|
||||
VpnConnectionInfo m_vpn;
|
||||
std::function<void(const VpnConnectionInfo&)> m_onActivate;
|
||||
std::function<void(const VpnConnectionInfo&)> m_onDeactivate;
|
||||
Label* m_title = nullptr;
|
||||
Button* m_actionButton = nullptr;
|
||||
InputArea* m_inputArea = nullptr;
|
||||
Signal<>::ScopedConnection m_paletteConn;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
NetworkTab::NetworkTab(NetworkService* network, NetworkSecretAgent* secrets) : m_network(network), m_secrets(secrets) {
|
||||
@@ -403,6 +517,23 @@ std::unique_ptr<Flex> NetworkTab::createHeaderActions() {
|
||||
});
|
||||
m_rescanButton = rescan.get();
|
||||
row->addChild(std::move(rescan));
|
||||
|
||||
auto vpnLabel = std::make_unique<Label>();
|
||||
vpnLabel->setText(i18n::tr("control-center.network.vpns"));
|
||||
vpnLabel->setFontSize(Style::fontSizeCaption * scale);
|
||||
vpnLabel->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
row->addChild(std::move(vpnLabel));
|
||||
|
||||
auto vpnToggle = std::make_unique<Toggle>();
|
||||
vpnToggle->setToggleSize(ToggleSize::Small);
|
||||
vpnToggle->setScale(scale);
|
||||
vpnToggle->setChecked(m_vpnVisible);
|
||||
vpnToggle->setOnChange([this](bool checked) {
|
||||
m_vpnVisible = checked;
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_vpnToggle = vpnToggle.get();
|
||||
row->addChild(std::move(vpnToggle));
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -439,6 +570,7 @@ void NetworkTab::onClose() {
|
||||
m_list = nullptr;
|
||||
m_rescanButton = nullptr;
|
||||
m_wifiToggle = nullptr;
|
||||
m_vpnToggle = nullptr;
|
||||
m_disconnectRow = nullptr;
|
||||
m_disconnectButton = nullptr;
|
||||
m_scanSpinner = nullptr;
|
||||
@@ -532,6 +664,19 @@ std::string NetworkTab::apListKey(const std::vector<AccessPointInfo>& aps) const
|
||||
return key;
|
||||
}
|
||||
|
||||
std::string NetworkTab::vpnListKey(const std::vector<VpnConnectionInfo>& vpns) const {
|
||||
std::string key;
|
||||
for (const auto& vpn : vpns) {
|
||||
key += vpn.path;
|
||||
key.push_back(':');
|
||||
key += vpn.name;
|
||||
key.push_back(':');
|
||||
key += vpn.active ? '1' : '0';
|
||||
key.push_back('\n');
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
uiAssertNotRendering("NetworkTab::rebuildApList");
|
||||
if (m_list == nullptr || m_listScroll == nullptr) {
|
||||
@@ -543,7 +688,11 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
}
|
||||
|
||||
const auto& aps = m_network != nullptr ? m_network->accessPoints() : std::vector<AccessPointInfo>{};
|
||||
const std::string nextKey = aps.empty() ? std::string("empty") : apListKey(aps);
|
||||
const auto& vpns = m_network != nullptr ? m_network->vpnConnections() : std::vector<VpnConnectionInfo>{};
|
||||
const std::string nextKey =
|
||||
aps.empty() && vpns.empty()
|
||||
? std::string("empty")
|
||||
: (apListKey(aps) + "\n---\n" + vpnListKey(vpns) + "\nvis:" + (m_vpnVisible ? '1' : '0'));
|
||||
if (listWidth == m_lastListWidth && nextKey == m_lastListKey) {
|
||||
return;
|
||||
}
|
||||
@@ -554,7 +703,7 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
m_list->removeChild(m_list->children().front().get());
|
||||
}
|
||||
|
||||
if (aps.empty()) {
|
||||
if (aps.empty() && vpns.empty()) {
|
||||
auto empty = std::make_unique<Label>();
|
||||
empty->setText(m_network != nullptr ? i18n::tr("control-center.network.no-networks")
|
||||
: i18n::tr("control-center.network.unavailable-title"));
|
||||
@@ -563,6 +712,42 @@ void NetworkTab::rebuildApList(Renderer& renderer) {
|
||||
empty->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
m_list->addChild(std::move(empty));
|
||||
} else {
|
||||
if (m_vpnVisible && !vpns.empty()) {
|
||||
auto section = std::make_unique<Label>();
|
||||
section->setText(i18n::tr("control-center.network.vpns"));
|
||||
section->setCaptionStyle();
|
||||
section->setFontSize(Style::fontSizeCaption * contentScale());
|
||||
section->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
m_list->addChild(std::move(section));
|
||||
|
||||
for (const auto& vpn : vpns) {
|
||||
auto row = std::make_unique<VpnConnectionRow>(
|
||||
contentScale(), vpn,
|
||||
[this](const VpnConnectionInfo& clicked) {
|
||||
if (m_network != nullptr) {
|
||||
m_network->activateVpnConnection(clicked);
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
},
|
||||
[this](const VpnConnectionInfo& clicked) {
|
||||
if (m_network != nullptr) {
|
||||
m_network->deactivateVpnConnection(clicked);
|
||||
}
|
||||
PanelManager::instance().refresh();
|
||||
});
|
||||
m_list->addChild(std::move(row));
|
||||
}
|
||||
}
|
||||
|
||||
if (!aps.empty()) {
|
||||
auto section = std::make_unique<Label>();
|
||||
section->setText(i18n::tr("control-center.network.wireless"));
|
||||
section->setCaptionStyle();
|
||||
section->setFontSize(Style::fontSizeCaption * contentScale());
|
||||
section->setColor(colorSpecFromRole(ColorRole::OnSurfaceVariant));
|
||||
m_list->addChild(std::move(section));
|
||||
}
|
||||
|
||||
for (const auto& ap : aps) {
|
||||
const bool saved = m_network != nullptr && m_network->hasSavedConnection(ap.ssid);
|
||||
auto row = std::make_unique<AccessPointRow>(
|
||||
|
||||
@@ -34,6 +34,7 @@ private:
|
||||
void showPasswordPrompt(const NetworkSecretAgent::SecretRequest& request);
|
||||
void clearPasswordPrompt();
|
||||
[[nodiscard]] std::string apListKey(const std::vector<AccessPointInfo>& aps) const;
|
||||
[[nodiscard]] std::string vpnListKey(const std::vector<VpnConnectionInfo>& vpns) const;
|
||||
|
||||
NetworkService* m_network = nullptr;
|
||||
NetworkSecretAgent* m_secrets = nullptr;
|
||||
@@ -53,9 +54,11 @@ private:
|
||||
|
||||
Button* m_rescanButton = nullptr;
|
||||
Toggle* m_wifiToggle = nullptr;
|
||||
Toggle* m_vpnToggle = nullptr;
|
||||
Flex* m_disconnectRow = nullptr;
|
||||
Button* m_disconnectButton = nullptr;
|
||||
Spinner* m_scanSpinner = nullptr;
|
||||
bool m_vpnVisible = true;
|
||||
|
||||
std::string m_lastListKey;
|
||||
float m_lastListWidth = -1.0f;
|
||||
|
||||
Reference in New Issue
Block a user