diff --git a/modules/nexus/common/ItemList.qml b/modules/nexus/common/ItemList.qml new file mode 100644 index 00000000..e5a7e0a2 --- /dev/null +++ b/modules/nexus/common/ItemList.qml @@ -0,0 +1,123 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +StyledRect { + id: root + + property bool showList + property string placeholderIcon + property string placeholderText + property int extraHeight + + property alias model: list.model + property alias delegate: list.delegate + readonly property alias list: list + + Layout.fillWidth: true + implicitHeight: (showList && list.count > 0 ? list.contentHeight : placeholder.implicitHeight + Tokens.padding.extraLarge * 2) + extraHeight + radius: Tokens.rounding.extraSmall + color: Colours.tPalette.m3surfaceContainer + clip: true + + Behavior on implicitHeight { + Anim {} + } + + Loader { + id: placeholder + + anchors.centerIn: parent + active: opacity > 0 + asynchronous: true + opacity: root.showList && list.count > 0 ? 0 : 1 + + sourceComponent: ColumnLayout { + spacing: Tokens.spacing.extraSmall + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.placeholderIcon + color: Colours.palette.m3outline + font: Tokens.font.icon.large + animate: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.placeholderText + color: Colours.palette.m3outline + font: Tokens.font.body.large + animate: true + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } + + ListView { + id: list + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + + spacing: 0 + interactive: false + opacity: root.showList ? 1 : 0 + + add: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 + type: Anim.DefaultEffects + } + } + + remove: Transition { + Anim { + property: "opacity" + to: 0 + type: Anim.DefaultEffects + } + } + + move: Transition { + Anim { + property: "opacity" + to: 1 + type: Anim.DefaultEffects + } + Anim { + property: "y" + } + } + + displaced: Transition { + Anim { + property: "opacity" + to: 1 + type: Anim.DefaultEffects + } + Anim { + property: "y" + } + } + + Behavior on opacity { + Anim { + type: Anim.DefaultEffects + } + } + } +} diff --git a/modules/nexus/pages/NetworkPage.qml b/modules/nexus/pages/NetworkPage.qml index f32b854b..c6cbd6ac 100644 --- a/modules/nexus/pages/NetworkPage.qml +++ b/modules/nexus/pages/NetworkPage.qml @@ -59,226 +59,130 @@ PageBase { verticalPadding: Tokens.padding.large } - StyledRect { - Layout.fillWidth: true - implicitHeight: (Nmcli.wifiEnabled && networkList.count > 0 ? networkList.contentHeight : noNetworksPlaceholder.implicitHeight + Tokens.padding.extraLarge * 2) + (Nmcli.scanning ? Tokens.rounding.extraSmall : 0) // Inline so it isn't affected by anim - radius: Tokens.rounding.extraSmall - color: Colours.tPalette.m3surfaceContainer - clip: true + ItemList { + id: networkList - Behavior on implicitHeight { - Anim {} + showList: Nmcli.wifiEnabled + placeholderIcon: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off" + placeholderText: Nmcli.wifiEnabled ? qsTr("No networks found") : qsTr("Wi-Fi disabled") + extraHeight: Nmcli.scanning ? Tokens.rounding.extraSmall : 0 // Inline so it isn't affected by anim + list.anchors.top: scanningIndicator.bottom + + model: ScriptModel { + values: [...Nmcli.networks].sort((a, b) => { + if (a.active) + return -1; + if (b.active) + return 1; + return b.strength - a.strength; + }) } - Loader { - id: noNetworksPlaceholder + delegate: StateLayer { + id: network - anchors.centerIn: parent - active: opacity > 0 - asynchronous: true - opacity: Nmcli.wifiEnabled && networkList.count > 0 ? 0 : 1 + required property Nmcli.AccessPoint modelData + property bool currentSelected + property real textOpacity: disabled ? 0.5 : 1 - sourceComponent: ColumnLayout { - spacing: Tokens.spacing.extraSmall + disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid + + anchors.left: networkList.list.contentItem.left + anchors.right: networkList.list.contentItem.right + implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2 + radius: Tokens.rounding.extraSmall + anchors.fill: undefined + + onClicked: { + if (!modelData.active) { + NetworkConnection.handleConnect(modelData); + currentSelected = true; + root.networkSelected(modelData); + } + } + + Behavior on textOpacity { + Anim { + type: Anim.DefaultEffects + } + } + + Connections { + function onActiveChanged(): void { + if (network.modelData.active) + network.currentSelected = false; + } + + target: network.modelData + } + + Connections { + function onNetworkSelected(ap: Nmcli.AccessPoint): void { + if (ap !== network.modelData) + network.currentSelected = false; + } + + target: root + } + + RowLayout { + id: networkLayout + + anchors.fill: parent + anchors.margins: Tokens.padding.medium + anchors.leftMargin: Tokens.padding.largeIncreased + anchors.rightMargin: Tokens.padding.largeIncreased + spacing: Tokens.spacing.medium MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: Nmcli.wifiEnabled ? "wifi_find" : "signal_wifi_off" - color: Colours.palette.m3outline - font: Tokens.font.icon.large - animate: true + text: Icons.getNetworkIcon(network.modelData.strength) + color: network.modelData.active ? Colours.palette.m3primary : Colours.tPalette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: network.textOpacity } - StyledText { - Layout.alignment: Qt.AlignHCenter - text: Nmcli.wifiEnabled ? qsTr("No networks found") : qsTr("Wi-Fi disabled") - color: Colours.palette.m3outline - font: Tokens.font.body.large - animate: true - } - } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + opacity: network.textOpacity - Behavior on opacity { - Anim { - type: Anim.DefaultEffects - } - } - } - - ListView { - id: networkList - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: scanningIndicator.bottom - anchors.bottom: parent.bottom - - spacing: 0 - interactive: false - - model: ScriptModel { - values: [...Nmcli.networks].sort((a, b) => { - if (a.active) - return -1; - if (b.active) - return 1; - return b.strength - a.strength; - }) - } - - add: Transition { - Anim { - property: "opacity" - from: 0 - to: 1 - type: Anim.DefaultEffects - } - } - - remove: Transition { - Anim { - property: "opacity" - to: 0 - type: Anim.DefaultEffects - } - } - - move: Transition { - Anim { - property: "opacity" - to: 1 - type: Anim.DefaultEffects - } - Anim { - property: "y" - } - } - - displaced: Transition { - Anim { - property: "opacity" - to: 1 - type: Anim.DefaultEffects - } - Anim { - property: "y" - } - } - - delegate: StateLayer { - id: network - - required property Nmcli.AccessPoint modelData - property bool currentSelected - property real textOpacity: disabled ? 0.5 : 1 - - disabled: currentSelected || Nmcli.connectingSsid() === modelData.ssid - - anchors.left: networkList.contentItem.left - anchors.right: networkList.contentItem.right - implicitHeight: networkLayout.implicitHeight + networkLayout.anchors.margins * 2 - radius: Tokens.rounding.extraSmall - anchors.fill: undefined - - onClicked: { - if (!modelData.active) { - NetworkConnection.handleConnect(modelData); - currentSelected = true; - root.networkSelected(modelData); - } - } - - Behavior on textOpacity { - Anim { - type: Anim.DefaultEffects - } - } - - Connections { - function onActiveChanged(): void { - if (network.modelData.active) - network.currentSelected = false; - } - - target: network.modelData - } - - Connections { - function onNetworkSelected(ap: Nmcli.AccessPoint): void { - if (ap !== network.modelData) - network.currentSelected = false; - } - - target: root - } - - RowLayout { - id: networkLayout - - anchors.fill: parent - anchors.margins: Tokens.padding.medium - anchors.leftMargin: Tokens.padding.largeIncreased - anchors.rightMargin: Tokens.padding.largeIncreased - spacing: Tokens.spacing.medium - - MaterialIcon { - text: Icons.getNetworkIcon(network.modelData.strength) - color: network.modelData.active ? Colours.palette.m3primary : Colours.tPalette.m3onSurfaceVariant - font: Tokens.font.icon.medium - opacity: network.textOpacity - } - - ColumnLayout { + StyledText { Layout.fillWidth: true - spacing: 0 - opacity: network.textOpacity - - StyledText { - Layout.fillWidth: true - text: network.modelData.ssid - font: Tokens.font.body.small - elide: Text.ElideRight - } - - StyledText { - Layout.fillWidth: true - text: qsTr("Security: %1").arg(network.modelData.security) - color: Colours.tPalette.m3outline - font: Tokens.font.label.small - elide: Text.ElideRight - } + text: network.modelData.ssid + font: Tokens.font.body.small + elide: Text.ElideRight } - AnimLoader { - sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp - - Component { - id: iconComp - - MaterialIcon { - text: network.modelData.active ? "settings" : "lock" - color: network.modelData.active ? Colours.palette.m3primary : Colours.tPalette.m3onSurfaceVariant - font: Tokens.font.icon.medium - opacity: network.textOpacity - } - } - - Component { - id: loadingComp - - LoadingIndicator { - implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) - } - } + StyledText { + Layout.fillWidth: true + text: qsTr("Security: %1").arg(network.modelData.security) + color: Colours.tPalette.m3outline + font: Tokens.font.label.small + elide: Text.ElideRight } } - } - opacity: Nmcli.wifiEnabled ? 1 : 0 + AnimLoader { + sourceComp: Nmcli.connectingSsid() === network.modelData.ssid ? loadingComp : iconComp - Behavior on opacity { - Anim { - type: Anim.DefaultEffects + Component { + id: iconComp + + MaterialIcon { + text: network.modelData.active ? "settings" : "lock" + color: network.modelData.active ? Colours.palette.m3primary : Colours.tPalette.m3onSurfaceVariant + font: Tokens.font.icon.medium + opacity: network.textOpacity + } + } + + Component { + id: loadingComp + + LoadingIndicator { + implicitSize: Math.round(Tokens.font.icon.medium.pointSize * 1.3) + } + } } } }