nmcli: migrated all of wireless controlcenter
This commit is contained in:
parent
36a91213b1
commit
7799ec67fd
4 changed files with 84 additions and 110 deletions
|
|
@ -28,28 +28,23 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSavedProfile(): void {
|
function checkSavedProfile(): void {
|
||||||
// Refresh saved connections list to ensure it's up to date
|
|
||||||
// This ensures the "Forget Network" button visibility is accurate
|
|
||||||
if (network && network.ssid) {
|
if (network && network.ssid) {
|
||||||
// Always refresh to ensure we have the latest saved connections
|
Nmcli.loadSavedConnections(() => {});
|
||||||
// This is important when networks are selected or changed
|
|
||||||
Network.listConnectionsProc.running = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Network
|
target: Nmcli
|
||||||
function onActiveChanged() {
|
function onActiveChanged() {
|
||||||
updateDeviceDetails();
|
updateDeviceDetails();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateDeviceDetails(): void {
|
function updateDeviceDetails(): void {
|
||||||
// Only update details if the selected network is currently active
|
if (network && Nmcli.active && Nmcli.active.ssid === network.ssid) {
|
||||||
if (network && Network.active && Network.active.ssid === network.ssid) {
|
Nmcli.getWirelessDeviceDetails("", () => {});
|
||||||
Network.updateWirelessDeviceDetails();
|
|
||||||
} else {
|
} else {
|
||||||
Network.wirelessDeviceDetails = null;
|
Nmcli.wirelessDeviceDetails = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -84,7 +79,7 @@ Item {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
handleConnect();
|
handleConnect();
|
||||||
} else {
|
} else {
|
||||||
Network.disconnectFromNetwork();
|
Nmcli.disconnectFromNetwork();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -96,8 +91,7 @@ Item {
|
||||||
if (!root.network || !root.network.ssid) {
|
if (!root.network || !root.network.ssid) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Check if profile exists - this will update reactively when savedConnectionSsids changes
|
return Nmcli.hasSavedProfile(root.network.ssid);
|
||||||
return Network.hasSavedProfile(root.network.ssid);
|
|
||||||
}
|
}
|
||||||
color: Colours.palette.m3errorContainer
|
color: Colours.palette.m3errorContainer
|
||||||
onColor: Colours.palette.m3onErrorContainer
|
onColor: Colours.palette.m3onErrorContainer
|
||||||
|
|
@ -105,12 +99,10 @@ Item {
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (root.network && root.network.ssid) {
|
if (root.network && root.network.ssid) {
|
||||||
// Disconnect first if connected
|
|
||||||
if (root.network.active) {
|
if (root.network.active) {
|
||||||
Network.disconnectFromNetwork();
|
Nmcli.disconnectFromNetwork();
|
||||||
}
|
}
|
||||||
// Delete the connection profile
|
Nmcli.forgetNetwork(root.network.ssid, () => {});
|
||||||
Network.forgetNetwork(root.network.ssid);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -161,7 +153,7 @@ Item {
|
||||||
|
|
||||||
SectionContainer {
|
SectionContainer {
|
||||||
ConnectionInfoSection {
|
ConnectionInfoSection {
|
||||||
deviceDetails: Network.wirelessDeviceDetails
|
deviceDetails: Nmcli.wirelessDeviceDetails
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,9 +161,8 @@ Item {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnect(): void {
|
function handleConnect(): void {
|
||||||
// If already connected to a different network, disconnect first
|
if (Nmcli.active && Nmcli.active.ssid !== root.network.ssid) {
|
||||||
if (Network.active && Network.active.ssid !== root.network.ssid) {
|
Nmcli.disconnectFromNetwork();
|
||||||
Network.disconnectFromNetwork();
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
connectToNetwork();
|
connectToNetwork();
|
||||||
});
|
});
|
||||||
|
|
@ -182,29 +173,31 @@ Item {
|
||||||
|
|
||||||
function connectToNetwork(): void {
|
function connectToNetwork(): void {
|
||||||
if (root.network.isSecure) {
|
if (root.network.isSecure) {
|
||||||
// Check if we have a saved connection profile for this network (by SSID)
|
const hasSavedProfile = Nmcli.hasSavedProfile(root.network.ssid);
|
||||||
const hasSavedProfile = Network.hasSavedProfile(root.network.ssid);
|
|
||||||
|
|
||||||
if (hasSavedProfile) {
|
if (hasSavedProfile) {
|
||||||
// Try connecting with saved password - don't show dialog if it fails
|
Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null);
|
||||||
// The saved password should work, but if connection fails for other reasons,
|
|
||||||
// we'll let the user try manually later
|
|
||||||
Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null);
|
|
||||||
} else {
|
} else {
|
||||||
// No saved profile, try connecting without password first
|
Nmcli.connectToNetworkWithPasswordCheck(
|
||||||
Network.connectToNetworkWithPasswordCheck(
|
|
||||||
root.network.ssid,
|
root.network.ssid,
|
||||||
root.network.isSecure,
|
root.network.isSecure,
|
||||||
() => {
|
(result) => {
|
||||||
// Callback: connection failed, show password dialog
|
if (result.needsPassword) {
|
||||||
|
if (Nmcli.pendingConnection) {
|
||||||
|
Nmcli.connectionCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.checkCount = 0;
|
||||||
|
Nmcli.pendingConnection = null;
|
||||||
|
}
|
||||||
root.session.network.showPasswordDialog = true;
|
root.session.network.showPasswordDialog = true;
|
||||||
root.session.network.pendingNetwork = root.network;
|
root.session.network.pendingNetwork = root.network;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
root.network.bssid
|
root.network.bssid
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Network.connectToNetwork(root.network.ssid, "", root.network.bssid, null);
|
Nmcli.connectToNetwork(root.network.ssid, "", root.network.bssid, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -31,22 +31,22 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleButton {
|
ToggleButton {
|
||||||
toggled: Network.wifiEnabled
|
toggled: Nmcli.wifiEnabled
|
||||||
icon: "wifi"
|
icon: "wifi"
|
||||||
accent: "Tertiary"
|
accent: "Tertiary"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Network.toggleWifi();
|
Nmcli.toggleWifi(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ToggleButton {
|
ToggleButton {
|
||||||
toggled: Network.scanning
|
toggled: Nmcli.scanning
|
||||||
icon: "wifi_find"
|
icon: "wifi_find"
|
||||||
accent: "Secondary"
|
accent: "Secondary"
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
Network.rescanWifi();
|
Nmcli.rescanWifi();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,13 +70,13 @@ ColumnLayout {
|
||||||
spacing: Appearance.spacing.small
|
spacing: Appearance.spacing.small
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: qsTr("Networks (%1)").arg(Network.networks.length)
|
text: qsTr("Networks (%1)").arg(Nmcli.networks.length)
|
||||||
font.pointSize: Appearance.font.size.large
|
font.pointSize: Appearance.font.size.large
|
||||||
font.weight: 500
|
font.weight: 500
|
||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
visible: Network.scanning
|
visible: Nmcli.scanning
|
||||||
text: qsTr("Scanning...")
|
text: qsTr("Scanning...")
|
||||||
color: Colours.palette.m3primary
|
color: Colours.palette.m3primary
|
||||||
font.pointSize: Appearance.font.size.small
|
font.pointSize: Appearance.font.size.small
|
||||||
|
|
@ -94,7 +94,7 @@ ColumnLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
model: Network.networks
|
model: Nmcli.networks
|
||||||
|
|
||||||
spacing: Appearance.spacing.small / 2
|
spacing: Appearance.spacing.small / 2
|
||||||
clip: true
|
clip: true
|
||||||
|
|
@ -183,7 +183,7 @@ ColumnLayout {
|
||||||
StateLayer {
|
StateLayer {
|
||||||
function onClicked(): void {
|
function onClicked(): void {
|
||||||
if (modelData.active) {
|
if (modelData.active) {
|
||||||
Network.disconnectFromNetwork();
|
Nmcli.disconnectFromNetwork();
|
||||||
} else {
|
} else {
|
||||||
handleConnect(modelData);
|
handleConnect(modelData);
|
||||||
}
|
}
|
||||||
|
|
@ -205,19 +205,14 @@ ColumnLayout {
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkSavedProfileForNetwork(ssid: string): void {
|
function checkSavedProfileForNetwork(ssid: string): void {
|
||||||
// Refresh saved connections list to ensure it's up to date
|
|
||||||
// This ensures accurate profile detection when selecting networks
|
|
||||||
if (ssid && ssid.length > 0) {
|
if (ssid && ssid.length > 0) {
|
||||||
// Always refresh to ensure we have the latest saved connections
|
Nmcli.loadSavedConnections(() => {});
|
||||||
// This is important when a network is selected from the list
|
|
||||||
Network.listConnectionsProc.running = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleConnect(network): void {
|
function handleConnect(network): void {
|
||||||
// If already connected to a different network, disconnect first
|
if (Nmcli.active && Nmcli.active.ssid !== network.ssid) {
|
||||||
if (Network.active && Network.active.ssid !== network.ssid) {
|
Nmcli.disconnectFromNetwork();
|
||||||
Network.disconnectFromNetwork();
|
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
connectToNetwork(network);
|
connectToNetwork(network);
|
||||||
});
|
});
|
||||||
|
|
@ -228,29 +223,31 @@ ColumnLayout {
|
||||||
|
|
||||||
function connectToNetwork(network): void {
|
function connectToNetwork(network): void {
|
||||||
if (network.isSecure) {
|
if (network.isSecure) {
|
||||||
// Check if we have a saved connection profile for this network (by SSID)
|
const hasSavedProfile = Nmcli.hasSavedProfile(network.ssid);
|
||||||
const hasSavedProfile = Network.hasSavedProfile(network.ssid);
|
|
||||||
|
|
||||||
if (hasSavedProfile) {
|
if (hasSavedProfile) {
|
||||||
// Try connecting with saved password - don't show dialog if it fails
|
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
|
||||||
// The saved password should work, but if connection fails for other reasons,
|
|
||||||
// we'll let the user try manually later
|
|
||||||
Network.connectToNetwork(network.ssid, "", network.bssid, null);
|
|
||||||
} else {
|
} else {
|
||||||
// No saved profile, try connecting without password first
|
Nmcli.connectToNetworkWithPasswordCheck(
|
||||||
Network.connectToNetworkWithPasswordCheck(
|
|
||||||
network.ssid,
|
network.ssid,
|
||||||
network.isSecure,
|
network.isSecure,
|
||||||
() => {
|
(result) => {
|
||||||
// Callback: connection failed, show password dialog
|
if (result.needsPassword) {
|
||||||
|
if (Nmcli.pendingConnection) {
|
||||||
|
Nmcli.connectionCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.stop();
|
||||||
|
Nmcli.immediateCheckTimer.checkCount = 0;
|
||||||
|
Nmcli.pendingConnection = null;
|
||||||
|
}
|
||||||
root.session.network.showPasswordDialog = true;
|
root.session.network.showPasswordDialog = true;
|
||||||
root.session.network.pendingNetwork = network;
|
root.session.network.pendingNetwork = network;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
network.bssid
|
network.bssid
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Network.connectToNetwork(network.ssid, "", network.bssid, null);
|
Nmcli.connectToNetwork(network.ssid, "", network.bssid, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -108,26 +108,16 @@ Item {
|
||||||
|
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
Layout.topMargin: Appearance.spacing.small
|
Layout.topMargin: Appearance.spacing.small
|
||||||
visible: Network.connectionStatus.length > 0 || connectButton.connecting
|
visible: connectButton.connecting
|
||||||
text: {
|
text: {
|
||||||
if (Network.connectionStatus.length > 0) {
|
if (connectButton.connecting) {
|
||||||
return Network.connectionStatus;
|
|
||||||
} else if (connectButton.connecting) {
|
|
||||||
return qsTr("Connecting...");
|
return qsTr("Connecting...");
|
||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
color: {
|
color: Colours.palette.m3onSurfaceVariant
|
||||||
const status = Network.connectionStatus;
|
|
||||||
if (status.includes("Error") || status.includes("error") || status.includes("failed")) {
|
|
||||||
return Colours.palette.m3error;
|
|
||||||
} else if (status.includes("successful") || status.includes("Connected") || status.includes("success")) {
|
|
||||||
return Colours.palette.m3primary;
|
|
||||||
}
|
|
||||||
return Colours.palette.m3onSurfaceVariant;
|
|
||||||
}
|
|
||||||
font.pointSize: Appearance.font.size.small
|
font.pointSize: Appearance.font.size.small
|
||||||
font.weight: (Network.connectionStatus.includes("Error") || Network.connectionStatus.includes("error")) ? 500 : 400
|
font.weight: 400
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
Layout.maximumWidth: parent.width - Appearance.padding.large * 2
|
||||||
}
|
}
|
||||||
|
|
@ -166,7 +156,6 @@ Item {
|
||||||
if (root.visible) {
|
if (root.visible) {
|
||||||
passwordField.forceActiveFocus();
|
passwordField.forceActiveFocus();
|
||||||
passwordField.text = "";
|
passwordField.text = "";
|
||||||
Network.clearConnectionStatus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -225,10 +214,9 @@ Item {
|
||||||
connecting = true;
|
connecting = true;
|
||||||
enabled = false;
|
enabled = false;
|
||||||
text = qsTr("Connecting...");
|
text = qsTr("Connecting...");
|
||||||
Network.clearConnectionStatus();
|
|
||||||
|
|
||||||
// Connect to network
|
// Connect to network
|
||||||
Network.connectToNetwork(
|
Nmcli.connectToNetwork(
|
||||||
root.network.ssid,
|
root.network.ssid,
|
||||||
password,
|
password,
|
||||||
root.network.bssid || "",
|
root.network.bssid || "",
|
||||||
|
|
@ -248,27 +236,17 @@ Item {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check connection status message for success indicators
|
|
||||||
const status = Network.connectionStatus;
|
|
||||||
const statusLower = status.toLowerCase();
|
|
||||||
|
|
||||||
// Check for success indicators in status message
|
|
||||||
const hasSuccessIndicator = statusLower.includes("connection activated") ||
|
|
||||||
statusLower.includes("successfully") ||
|
|
||||||
statusLower.includes("connected successfully") ||
|
|
||||||
(statusLower.includes("connected") && !statusLower.includes("error") && !statusLower.includes("failed"));
|
|
||||||
|
|
||||||
// Check if we're connected to the target network (case-insensitive SSID comparison)
|
// Check if we're connected to the target network (case-insensitive SSID comparison)
|
||||||
const isConnected = root.network && Network.active && Network.active.ssid &&
|
const isConnected = root.network && Nmcli.active && Nmcli.active.ssid &&
|
||||||
Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||||
|
|
||||||
if (isConnected || hasSuccessIndicator) {
|
if (isConnected) {
|
||||||
// Successfully connected - give it a moment for network list to update
|
// Successfully connected - give it a moment for network list to update
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
// Double-check connection is still active
|
// Double-check connection is still active
|
||||||
if (root.visible && Network.active && Network.active.ssid) {
|
if (root.visible && Nmcli.active && Nmcli.active.ssid) {
|
||||||
const stillConnected = Network.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
const stillConnected = Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim();
|
||||||
if (stillConnected || hasSuccessIndicator) {
|
if (stillConnected) {
|
||||||
connectionMonitor.stop();
|
connectionMonitor.stop();
|
||||||
connectButton.connecting = false;
|
connectButton.connecting = false;
|
||||||
connectButton.text = qsTr("Connect");
|
connectButton.text = qsTr("Connect");
|
||||||
|
|
@ -279,11 +257,10 @@ Item {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for connection errors (but not warnings about duplicate names)
|
// Check for connection failures - if pending connection was cleared but we're not connected
|
||||||
if (status.includes("Error") || (status.includes("error") && !status.includes("Warning"))) {
|
if (Nmcli.pendingConnection === null && connectButton.connecting) {
|
||||||
// Only treat as error if it's not just a warning about duplicate names
|
// Wait a bit more before giving up (allow time for connection to establish)
|
||||||
if (!status.includes("another connection with the name") && !status.includes("Reference the connection by its uuid")) {
|
if (connectionMonitor.repeatCount > 10) {
|
||||||
// Connection failed
|
|
||||||
connectionMonitor.stop();
|
connectionMonitor.stop();
|
||||||
connectButton.connecting = false;
|
connectButton.connecting = false;
|
||||||
connectButton.enabled = true;
|
connectButton.enabled = true;
|
||||||
|
|
@ -297,14 +274,22 @@ Item {
|
||||||
interval: 1000
|
interval: 1000
|
||||||
repeat: true
|
repeat: true
|
||||||
triggeredOnStart: false
|
triggeredOnStart: false
|
||||||
|
property int repeatCount: 0
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
|
repeatCount++;
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!running) {
|
||||||
|
repeatCount = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: Network
|
target: Nmcli
|
||||||
function onActiveChanged() {
|
function onActiveChanged() {
|
||||||
if (root.visible) {
|
if (root.visible) {
|
||||||
checkConnectionStatus();
|
checkConnectionStatus();
|
||||||
|
|
@ -318,6 +303,5 @@ Item {
|
||||||
connectButton.connecting = false;
|
connectButton.connecting = false;
|
||||||
connectButton.text = qsTr("Connect");
|
connectButton.text = qsTr("Connect");
|
||||||
connectionMonitor.stop();
|
connectionMonitor.stop();
|
||||||
Network.clearConnectionStatus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,9 +39,9 @@ ColumnLayout {
|
||||||
SectionContainer {
|
SectionContainer {
|
||||||
ToggleRow {
|
ToggleRow {
|
||||||
label: qsTr("WiFi enabled")
|
label: qsTr("WiFi enabled")
|
||||||
checked: Network.wifiEnabled
|
checked: Nmcli.wifiEnabled
|
||||||
toggle.onToggled: {
|
toggle.onToggled: {
|
||||||
Network.enableWifi(checked);
|
Nmcli.enableWifi(checked);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -57,25 +57,25 @@ ColumnLayout {
|
||||||
|
|
||||||
PropertyRow {
|
PropertyRow {
|
||||||
label: qsTr("Connected network")
|
label: qsTr("Connected network")
|
||||||
value: Network.active ? Network.active.ssid : qsTr("Not connected")
|
value: Nmcli.active ? Nmcli.active.ssid : qsTr("Not connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyRow {
|
PropertyRow {
|
||||||
showTopMargin: true
|
showTopMargin: true
|
||||||
label: qsTr("Signal strength")
|
label: qsTr("Signal strength")
|
||||||
value: Network.active ? qsTr("%1%").arg(Network.active.strength) : qsTr("N/A")
|
value: Nmcli.active ? qsTr("%1%").arg(Nmcli.active.strength) : qsTr("N/A")
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyRow {
|
PropertyRow {
|
||||||
showTopMargin: true
|
showTopMargin: true
|
||||||
label: qsTr("Security")
|
label: qsTr("Security")
|
||||||
value: Network.active ? (Network.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
value: Nmcli.active ? (Nmcli.active.isSecure ? qsTr("Secured") : qsTr("Open")) : qsTr("N/A")
|
||||||
}
|
}
|
||||||
|
|
||||||
PropertyRow {
|
PropertyRow {
|
||||||
showTopMargin: true
|
showTopMargin: true
|
||||||
label: qsTr("Frequency")
|
label: qsTr("Frequency")
|
||||||
value: Network.active ? qsTr("%1 MHz").arg(Network.active.frequency) : qsTr("N/A")
|
value: Nmcli.active ? qsTr("%1 MHz").arg(Nmcli.active.frequency) : qsTr("N/A")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Loading…
Add table
Reference in a new issue