Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 2m11s
C++ / build (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m30s
Python / verify (pull_request) Successful in 2m55s
C++ / clang-tidy (pull_request) Successful in 3m59s
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 2m11s
C++ / build (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m30s
Python / verify (pull_request) Successful in 2m55s
C++ / clang-tidy (pull_request) Successful in 3m59s
This commit is contained in:
@@ -0,0 +1,339 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Daemons
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property int rounding: Tokens.rounding.small - Tokens.padding.small
|
||||
readonly property int topMargin: 0
|
||||
required property var wrapper
|
||||
|
||||
implicitHeight: vol.implicitHeight + Tokens.padding.small * 2
|
||||
implicitWidth: 500 + Tokens.padding.small * 2
|
||||
|
||||
CustomRect {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: parent.implicitHeight
|
||||
radius: Tokens.rounding.small
|
||||
y: parent.y
|
||||
|
||||
Behavior on implicitHeight {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VolumesTab {
|
||||
id: vol
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.small
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
component VolumesTab: ColumnLayout {
|
||||
spacing: 12
|
||||
|
||||
CustomRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 65 + Tokens.spacing.smaller * 2
|
||||
Layout.topMargin: root.topMargin
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: root.rounding
|
||||
|
||||
ColumnLayout {
|
||||
anchors.bottomMargin: Tokens.padding.smaller
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.larger
|
||||
anchors.rightMargin: Tokens.padding.larger
|
||||
anchors.topMargin: Tokens.padding.smaller
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
text: "Output volume"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
color: Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||
font.bold: true
|
||||
text: qsTr("%1").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomMouseArea {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Tokens.padding.larger * 3
|
||||
|
||||
CustomSlider {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
bgColor: Audio.muted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer
|
||||
fgColor: Audio.muted ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
implicitHeight: parent.height
|
||||
insetColor: Audio.muted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary)
|
||||
insetIcon: Audio.muted || Audio.volume < 0.001 ? "volume_off" : "volume_up"
|
||||
value: Audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setVolume(value)
|
||||
}
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
checked: !Audio.muted
|
||||
|
||||
onToggled: {
|
||||
const audio = Audio.sink?.audio;
|
||||
if (audio)
|
||||
audio.muted = !audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomClippingRect {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 65 + Tokens.spacing.smaller * 2
|
||||
Layout.topMargin: root.topMargin
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: root.rounding
|
||||
|
||||
PwNodePeakMonitor {
|
||||
id: sourcePeak
|
||||
|
||||
node: Audio.source
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: sourcePeakFill
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * sourcePeak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.bottomMargin: Tokens.padding.smaller
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.larger
|
||||
anchors.rightMargin: Tokens.padding.larger
|
||||
anchors.topMargin: Tokens.padding.smaller
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
text: "Input volume"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
color: Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||
font.bold: true
|
||||
text: qsTr("%1").arg(Audio.sourceMuted ? qsTr("Muted") : `${Math.round(Audio.sourceVolume * 100)}%`)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomMouseArea {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Tokens.padding.larger * 3
|
||||
|
||||
CustomSlider {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
bgColor: Audio.sourceMuted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer
|
||||
fgColor: Audio.sourceMuted ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
implicitHeight: parent.height
|
||||
insetColor: Audio.sourceMuted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary)
|
||||
insetIcon: Audio.sourceMuted || Audio.sourceVolume < 0.001 ? "mic_off" : "mic"
|
||||
value: Audio.sourceVolume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setSourceVolume(value)
|
||||
}
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
checked: !Audio.sourceMuted
|
||||
|
||||
onToggled: {
|
||||
const audio = Audio.source?.audio;
|
||||
if (audio)
|
||||
audio.muted = !audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 1
|
||||
Layout.topMargin: root.topMargin
|
||||
color: Colors.tPalette.m3outline
|
||||
visible: appTracks.model.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: appTracks
|
||||
|
||||
model: Audio.streams.filter(s => s.isSink)
|
||||
|
||||
CustomClippingRect {
|
||||
id: appBox
|
||||
|
||||
required property int index
|
||||
required property var modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 65 + Tokens.spacing.smaller * 2
|
||||
Layout.topMargin: root.topMargin
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
radius: root.rounding
|
||||
|
||||
PwNodePeakMonitor {
|
||||
id: peak
|
||||
|
||||
node: appBox.modelData
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: peakFill
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.top: parent.top
|
||||
color: Qt.alpha(Colors.palette.m3primary, 0.15)
|
||||
implicitWidth: parent.width * peak.peak
|
||||
|
||||
Behavior on implicitWidth {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TextMetrics {
|
||||
id: metrics
|
||||
|
||||
elide: Text.ElideRight
|
||||
elideWidth: root.width - 50
|
||||
text: Audio.getStreamName(appBox.modelData)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
anchors.bottomMargin: Tokens.padding.smaller
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.larger
|
||||
anchors.rightMargin: Tokens.padding.larger
|
||||
anchors.topMargin: Tokens.padding.smaller
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignLeft
|
||||
Layout.fillWidth: true
|
||||
elide: Text.ElideRight
|
||||
text: metrics.elidedText
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignVCenter | Qt.AlignRight
|
||||
color: Qt.alpha(Colors.palette.m3onSurface, 0.7)
|
||||
font.bold: true
|
||||
text: qsTr("%1").arg(appBox.modelData.audio.muted ? qsTr("Muted") : `${Math.round(appBox.modelData.audio.volume * 100)}%`)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
spacing: Tokens.spacing.large
|
||||
|
||||
CustomMouseArea {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: Tokens.padding.larger * 3
|
||||
|
||||
CustomSlider {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
bgColor: appBox.modelData.audio.muted ? Qt.alpha(Colors.palette.m3errorContainer, 0.5) : Colors.palette.m3secondaryContainer
|
||||
fgColor: appBox.modelData.audio.muted ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
implicitHeight: parent.height
|
||||
insetColor: appBox.modelData.audio.muted ? (inset.attached ? Colors.palette.m3onErrorContainer : Colors.palette.m3onError) : (inset.attached ? Colors.palette.m3onSecondaryContainer : Colors.palette.m3onPrimary)
|
||||
insetIcon: appBox.modelData.audio.muted || appBox.modelData.audio.volume < 0.001 ? "volume_off" : "volume_up"
|
||||
value: appBox.modelData.audio.volume
|
||||
|
||||
Behavior on value {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onInteraction: value => Audio.setStreamVolume(appBox.modelData, value)
|
||||
}
|
||||
}
|
||||
|
||||
CustomSwitch {
|
||||
Layout.bottomMargin: Tokens.padding.normal
|
||||
checked: !appBox.modelData.audio.muted
|
||||
|
||||
onToggled: {
|
||||
appBox.modelData.audio.muted = !appBox.modelData.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQml.Models
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
implicitWidth: 500
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.larger
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomText {
|
||||
Layout.rightMargin: Tokens.padding.extraSmall
|
||||
Layout.topMargin: Tokens.padding.normal
|
||||
font.pointSize: Tokens.font.size.large
|
||||
text: qsTr("Bluetooth")
|
||||
}
|
||||
|
||||
Toggle {
|
||||
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
|
||||
first: true
|
||||
subtext: qsTr("Toggle bluetooth device state")
|
||||
text: checked ? qsTr("Enabled") : qsTr("Disabled")
|
||||
|
||||
onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
||||
if (adapter)
|
||||
adapter.enabled = checked;
|
||||
}
|
||||
}
|
||||
|
||||
Toggle {
|
||||
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
|
||||
last: rows.sortedConnected.length === 0
|
||||
subtext: qsTr("Enable scanning for new devices")
|
||||
text: qsTr("Discover")
|
||||
|
||||
onToggled: {
|
||||
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
|
||||
if (adapter)
|
||||
adapter.discovering = checked;
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: rows
|
||||
|
||||
readonly property var sortedConnected: {
|
||||
const byName = (a, b) => a.name.localeCompare(b.name);
|
||||
return [...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5);
|
||||
}
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: contentHeight
|
||||
interactive: false
|
||||
section.property: "connected"
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
add: Transition {
|
||||
Anim {
|
||||
from: 0
|
||||
property: "opacity"
|
||||
to: 1
|
||||
}
|
||||
}
|
||||
delegate: DelegateChooser {
|
||||
role: "isDivider"
|
||||
|
||||
DelegateChoice {
|
||||
roleValue: true
|
||||
|
||||
delegate: StatusDivider {
|
||||
}
|
||||
}
|
||||
|
||||
DelegateChoice {
|
||||
delegate: BluetoothItem {
|
||||
list: rows
|
||||
width: rows.width
|
||||
}
|
||||
}
|
||||
}
|
||||
displaced: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
objectProp: "address"
|
||||
values: {
|
||||
const byName = (a, b) => a.name.localeCompare(b.name);
|
||||
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
|
||||
{
|
||||
isDivider: true
|
||||
},
|
||||
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
|
||||
}
|
||||
}
|
||||
move: Transition {
|
||||
Anim {
|
||||
easing.type: Easing.OutCubic
|
||||
property: "y"
|
||||
}
|
||||
}
|
||||
remove: Transition {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
to: 0
|
||||
}
|
||||
}
|
||||
section.delegate: Rectangle {
|
||||
required property string section
|
||||
|
||||
color: "transparent"
|
||||
height: childrenRect.height
|
||||
width: ListView.view.width
|
||||
|
||||
CustomText {
|
||||
text: parent.section
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component BluetoothItem: ConnectedRect {
|
||||
id: btItem
|
||||
|
||||
readonly property int connectedCount: rows.sortedConnected.length
|
||||
readonly property bool connecting: Network.connectingNetwork === modelData
|
||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||
readonly property bool inConnectedGroup: index < connectedCount
|
||||
required property int index
|
||||
required property ListView list
|
||||
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
|
||||
|
||||
required property BluetoothDevice modelData
|
||||
property int verticalPadding: Tokens.padding.smaller
|
||||
|
||||
first: !inConnectedGroup && index === connectedCount + 1
|
||||
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
|
||||
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Timer {
|
||||
interval: 50
|
||||
running: btItem.ListView.delayRemove
|
||||
|
||||
onTriggered: btItem.ListView.delayRemove = false
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onConnectedChanged(): void {
|
||||
if (!btItem.modelData.connected)
|
||||
btItem.ListView.delayRemove = true;
|
||||
}
|
||||
|
||||
function onPairedChanged(): void {
|
||||
if (btItem.modelData.paired)
|
||||
btItem.modelData.connect();
|
||||
}
|
||||
|
||||
target: btItem.modelData
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: btItem.horizontalPadding
|
||||
anchors.rightMargin: btItem.horizontalPadding
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.extraSmall
|
||||
Layout.rightMargin: Tokens.spacing.extraSmall
|
||||
elide: Text.ElideRight
|
||||
text: btItem.modelData.name
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
implicitHeight: icon.height
|
||||
implicitWidth: icon.width
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: btItem.connecting ? 0 : 1
|
||||
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
|
||||
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
opacity: btItem.connecting ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
IconTextButton {
|
||||
font.pointSize: Tokens.font.size.small
|
||||
icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
|
||||
text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
|
||||
|
||||
onClicked: {
|
||||
if (btItem.modelData.connected)
|
||||
btItem.modelData.disconnect();
|
||||
else if (btItem.modelData.paired)
|
||||
btItem.modelData.connect();
|
||||
else if (!btItem.modelData.paired)
|
||||
btItem.modelData.pair();
|
||||
else if (btItem.modelData.pairing)
|
||||
btItem.modelData.cancelPair();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component ConnectedRect: CustomRect {
|
||||
id: bg
|
||||
|
||||
property bool first
|
||||
property bool last
|
||||
|
||||
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
|
||||
Behavior on bottomLeftRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on bottomRightRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on topLeftRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on topRightRadius {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
component StatusDivider: CustomText {
|
||||
bottomPadding: Tokens.spacing.extraSmall
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.smaller
|
||||
text: {
|
||||
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
|
||||
const connectedCount = devices.filter(d => d.connected).length;
|
||||
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
|
||||
return available;
|
||||
}
|
||||
topPadding: Tokens.spacing.small
|
||||
}
|
||||
component Toggle: CustomSwitch {
|
||||
id: toggleItem
|
||||
|
||||
readonly property alias bg: bg
|
||||
property alias first: bg.first
|
||||
property alias last: bg.last
|
||||
property string subtext
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
|
||||
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
|
||||
indicator.anchors.right: right
|
||||
indicator.anchors.rightMargin: toggleItem.horizontalPadding
|
||||
indicator.anchors.verticalCenter: verticalCenter
|
||||
verticalPadding: Tokens.padding.normal
|
||||
|
||||
background: ConnectedRect {
|
||||
id: bg
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
manualPressOverride: toggleItem.pressed
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: toggleItem.horizontalPadding
|
||||
anchors.right: toggleItem.indicator.left
|
||||
anchors.rightMargin: Tokens.spacing.normal
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: column.implicitWidth
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(label.font);
|
||||
f.pointSize = Tokens.font.size.smaller;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(subtext.font);
|
||||
f.pointSize = Tokens.font.size.small;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.subtext
|
||||
visible: toggleItem.subtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
required property real borderThickness
|
||||
readonly property alias content: content
|
||||
readonly property bool horizontal: position !== "left"
|
||||
// property real offsetScale: y > 0 || content.hasCurrent ? 0 : 1
|
||||
property real offsetScale: {
|
||||
if (content.hasCurrent)
|
||||
return 0;
|
||||
if (position === "top")
|
||||
return y > 0 ? 0 : 1;
|
||||
if (position === "bottom")
|
||||
return 1;
|
||||
return x > 0 ? 0 : 1;
|
||||
}
|
||||
required property string position
|
||||
required property ShellScreen screen
|
||||
|
||||
anchors.bottom: position === "bottom" ? parent.bottom : undefined
|
||||
clip: true
|
||||
implicitHeight: horizontal ? content.implicitHeight * (1 - offsetScale) : content.implicitHeight
|
||||
implicitWidth: horizontal ? content.implicitWidth : content.implicitWidth * (1 - offsetScale)
|
||||
visible: width > 0 && height > 0
|
||||
x: {
|
||||
if (!horizontal)
|
||||
return 0;
|
||||
const off = content.currentCenter - borderThickness - content.nonAnimWidth / 2;
|
||||
const diff = parent.width - Math.floor(off + content.nonAnimWidth);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return Math.floor(Math.max(off, 0));
|
||||
}
|
||||
y: {
|
||||
if (horizontal)
|
||||
return 0;
|
||||
|
||||
const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2;
|
||||
const diff = parent.height - Math.floor(off + content.nonAnimHeight);
|
||||
if (diff < 0)
|
||||
return off + diff;
|
||||
return Math.max(off, 0);
|
||||
}
|
||||
|
||||
Behavior on offsetScale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on x {
|
||||
enabled: !root.horizontal || root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
enabled: root.horizontal || root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: content.animLength
|
||||
easing.bezierCurve: content.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
Wrapper {
|
||||
id: content
|
||||
|
||||
anchors.bottom: root.position === "bottom" ? parent.bottom : undefined
|
||||
anchors.bottomMargin: (-implicitHeight - 5) * root.offsetScale
|
||||
anchors.horizontalCenter: root.horizontal ? parent.horizontalCenter : undefined
|
||||
anchors.left: root.horizontal ? undefined : parent.left
|
||||
anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale
|
||||
anchors.top: root.position === "top" ? parent.top : undefined
|
||||
anchors.topMargin: (-implicitHeight - 5) * root.offsetScale
|
||||
anchors.verticalCenter: root.horizontal ? undefined : parent.verticalCenter
|
||||
offsetScale: root.offsetScale
|
||||
screen: root.screen
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Services.SystemTray
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
readonly property Item current: currentPopout?.item ?? null
|
||||
readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null
|
||||
required property PopoutState popouts
|
||||
|
||||
implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.small * 2
|
||||
implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.small * 2
|
||||
|
||||
Item {
|
||||
id: content
|
||||
|
||||
anchors.fill: parent
|
||||
|
||||
Popout {
|
||||
name: "audio"
|
||||
|
||||
sourceComponent: AudioPopout {
|
||||
wrapper: root.wrapper
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: [...SystemTray.items.values]
|
||||
}
|
||||
|
||||
Popout {
|
||||
id: trayMenu
|
||||
|
||||
required property int index
|
||||
required property SystemTrayItem modelData
|
||||
|
||||
name: `traymenu${index}`
|
||||
sourceComponent: trayMenuComponent
|
||||
|
||||
Connections {
|
||||
function onHasCurrentChanged(): void {
|
||||
if (root.popouts.hasCurrent && trayMenu.shouldBeActive) {
|
||||
trayMenu.sourceComponent = null;
|
||||
trayMenu.sourceComponent = trayMenuComponent;
|
||||
}
|
||||
}
|
||||
|
||||
target: root.popouts
|
||||
}
|
||||
|
||||
Component {
|
||||
id: trayMenuComponent
|
||||
|
||||
TrayMenuPopout {
|
||||
popouts: root.popouts
|
||||
trayItem: trayMenu.modelData.menu
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "upower"
|
||||
|
||||
sourceComponent: UPowerPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "network"
|
||||
|
||||
sourceComponent: NetworkPopout {
|
||||
wrapper: root.popouts
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "bluetooth"
|
||||
|
||||
sourceComponent: BluetoothPopout {
|
||||
}
|
||||
}
|
||||
|
||||
Popout {
|
||||
name: "updates"
|
||||
|
||||
sourceComponent: UpdatesPopout {
|
||||
wrapper: root.popouts
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component Popout: Loader {
|
||||
id: popout
|
||||
|
||||
required property string name
|
||||
readonly property bool shouldBeActive: root.popouts.currentName === name
|
||||
|
||||
active: false
|
||||
// anchors.horizontalCenter: parent.horizontalCenter
|
||||
// anchors.top: parent.top
|
||||
// anchors.topMargin: 5
|
||||
anchors.centerIn: parent
|
||||
opacity: 0
|
||||
scale: 0.8
|
||||
|
||||
states: State {
|
||||
name: "active"
|
||||
when: popout.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
popout.active: true
|
||||
popout.opacity: 1
|
||||
popout.scale: 1
|
||||
}
|
||||
}
|
||||
transitions: [
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
target: popout
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
target: popout
|
||||
}
|
||||
|
||||
Anim {
|
||||
properties: "opacity,scale"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import QtQuick
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
|
||||
CustomRect {
|
||||
id: root
|
||||
|
||||
readonly property bool closing: PopupManager.closed
|
||||
readonly property url currentPopup: PopupManager.currentPopup
|
||||
property bool shouldBeVisible: loader.status === Loader.Ready
|
||||
|
||||
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
|
||||
opacity: shouldBeVisible && !closing ? 1 : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
PopupManager.requestClose();
|
||||
PopupManager.currentPopup = "";
|
||||
}
|
||||
}
|
||||
|
||||
CustomMouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
preventStealing: true
|
||||
propagateComposedEvents: false
|
||||
|
||||
onClicked: {
|
||||
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
|
||||
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
|
||||
if (insideItemHeight && insideItemWidth)
|
||||
return;
|
||||
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Connections {
|
||||
function onCurrentPopupChanged(): void {
|
||||
if (PopupManager.currentPopup) {
|
||||
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
|
||||
} else {
|
||||
loader.source = null;
|
||||
}
|
||||
}
|
||||
|
||||
target: PopupManager
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNetworkConnected(): void {
|
||||
PopupManager.requestClose();
|
||||
}
|
||||
|
||||
target: Network
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import ZShell.Components
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var network
|
||||
|
||||
color: Colors.palette.m3surfaceContainer
|
||||
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
|
||||
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
ColumnLayout {
|
||||
id: layout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.margins: Tokens.padding.extraLarge
|
||||
spacing: Tokens.spacing.normal
|
||||
|
||||
MaterialIcon {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
font.pointSize: Tokens.font.size.extraLarge * 2
|
||||
text: "lock"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: qsTr("Connect to %1").arg(root.network.name)
|
||||
}
|
||||
|
||||
CustomTextField {
|
||||
id: passwordField
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredWidth: 400
|
||||
echoMode: CustomTextField.Password
|
||||
errorText: qsTr("Failed to connect with given password")
|
||||
isError: Network.failedNetwork
|
||||
leadingIcon: "lock"
|
||||
passwordMaskDelay: 5000
|
||||
placeholderText: qsTr("Input password")
|
||||
}
|
||||
|
||||
ButtonRow {
|
||||
id: buttonRow
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: Tokens.spacing.smaller
|
||||
|
||||
IconTextButton {
|
||||
fillWidth: true
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
icon: "close"
|
||||
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
|
||||
inactiveOnColor: Colors.palette.m3onSurfaceVariant
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
text: "Cancel"
|
||||
|
||||
onClicked: PopupManager.requestClose()
|
||||
}
|
||||
|
||||
IconTextButton {
|
||||
fillWidth: true
|
||||
font.pointSize: Tokens.font.size.normal
|
||||
icon: Network.connectingNetwork ? "" : "check"
|
||||
inactiveColor: Network.connectingNetwork ? Colors.palette.m3primaryContainer : Network.failedNetwork ? Colors.palette.m3error : Colors.palette.m3primary
|
||||
inactiveOnColor: Network.connectingNetwork ? Colors.palette.m3onPrimaryContainer : Network.failedNetwork ? Colors.palette.m3onError : Colors.palette.m3onPrimary
|
||||
isRound: true
|
||||
isToggle: false
|
||||
shapeMorph: true
|
||||
text: Network.connectingNetwork ? "" : "Apply"
|
||||
|
||||
onClicked: Network.requestConnect(root.network, passwordField.text)
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.centerIn: parent
|
||||
opacity: Network.connectingNetwork ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
implicitSize: buttonRow.height - Tokens.padding.normal
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
property bool closed: true
|
||||
property url currentPopup: ""
|
||||
property var currentPopupProps: ({})
|
||||
|
||||
function requestClose(): void {
|
||||
closed = true;
|
||||
}
|
||||
|
||||
function requestOpen(source: url, properties = {}): void {
|
||||
currentPopupProps = properties;
|
||||
currentPopup = source;
|
||||
closed = false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
import qs.Modules.Bar.Popouts.Network
|
||||
import qs.Components
|
||||
import qs.Helpers
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
required property var wrapper
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
implicitHeight: networkPopContent.height + networks.implicitHeight + networkPopContent.anchors.margins + networks.anchors.margins * 2
|
||||
implicitWidth: 500 + 8 * 2
|
||||
radius: (20 - Tokens.padding.small) * Tokens.rounding.scale
|
||||
|
||||
Component.onCompleted: Network.active = true
|
||||
Component.onDestruction: Network.active = false
|
||||
|
||||
ColumnLayout {
|
||||
id: networkPopContent
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
CustomText {
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
Layout.rightMargin: Tokens.padding.extraSmall
|
||||
font.pointSize: Tokens.font.size.large
|
||||
text: qsTr("Wifi")
|
||||
}
|
||||
|
||||
Toggle {
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
checked: Network.wifiEnabled
|
||||
first: true
|
||||
last: connected.count === 0
|
||||
subtext: qsTr("Toggle WiFi device state")
|
||||
text: checked ? qsTr("Enabled") : qsTr("Disabled")
|
||||
|
||||
onToggled: Network.setWifi(checked)
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: connected
|
||||
|
||||
visible: Network.connectedNetworks.length > 0
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Network.connectedNetworks].sort((a, b) => {
|
||||
return b.signalStrength - a.signalStrength;
|
||||
})
|
||||
}
|
||||
|
||||
NetworkItem {
|
||||
first: false
|
||||
known: true
|
||||
repeater: connected
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: networks
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.margins: Tokens.padding.large
|
||||
anchors.right: parent.right
|
||||
anchors.top: networkPopContent.bottom
|
||||
spacing: Tokens.spacing.extraSmall / 2
|
||||
|
||||
Spacer {
|
||||
repeater: known
|
||||
visible: Network.connectedNetworks.length > 0
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.leftMargin: Tokens.padding.normal
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: Network.knownNetworks.length > 1 ? qsTr("%1 known networks available").arg(Network.knownNetworks.length) : qsTr("1 known network available") // qmllint disable missing-property
|
||||
visible: Network.knownNetworks.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: known
|
||||
|
||||
visible: Network.knownNetworks.length > 0
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Network.knownNetworks].sort((a, b) => {
|
||||
return b.signalStrength - a.signalStrength;
|
||||
})
|
||||
}
|
||||
|
||||
NetworkItem {
|
||||
known: true
|
||||
repeater: known
|
||||
}
|
||||
}
|
||||
|
||||
Spacer {
|
||||
repeater: unknown
|
||||
visible: Network.knownNetworks.length > 0
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.leftMargin: Tokens.padding.normal
|
||||
Layout.preferredHeight: visible ? implicitHeight : 0
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: Network.unknownNetworks.length > 1 ? qsTr("%1 networks available").arg(Network.unknownNetworks.length) : qsTr("1 network available") // qmllint disable missing-property
|
||||
visible: Network.unknownNetworks.length > 0
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: unknown
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Network.unknownNetworks].sort((a, b) => {
|
||||
return b.signalStrength - a.signalStrength;
|
||||
})
|
||||
}
|
||||
|
||||
NetworkItem {
|
||||
known: false
|
||||
repeater: unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
NetworkOverlay {
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
component ConnectedRect: CustomRect {
|
||||
id: bg
|
||||
|
||||
property bool first
|
||||
property bool last
|
||||
|
||||
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
|
||||
}
|
||||
component NetworkItem: ConnectedRect {
|
||||
id: networkItem
|
||||
|
||||
readonly property bool connecting: Network.connectingNetwork === modelData
|
||||
property int horizontalPadding: Tokens.padding.largeIncreased
|
||||
required property int index
|
||||
readonly property bool isSecure: Network.isSecure(modelData.security)
|
||||
required property bool known
|
||||
required property var modelData
|
||||
required property Repeater repeater
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
|
||||
first: index === 0
|
||||
last: index === (repeater.count - 1)
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: networkItem.horizontalPadding
|
||||
anchors.rightMargin: networkItem.horizontalPadding
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: Tokens.spacing.extraSmall
|
||||
Layout.rightMargin: Tokens.spacing.extraSmall
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
|
||||
elide: Text.ElideRight
|
||||
text: networkItem.modelData.name
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.small
|
||||
text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.alignment: Qt.AlignRight
|
||||
implicitHeight: icon.height
|
||||
implicitWidth: icon.width
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.large
|
||||
opacity: networkItem.connecting ? 0 : 1
|
||||
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: opacity > 0
|
||||
anchors.fill: parent
|
||||
opacity: networkItem.connecting ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
sourceComponent: CircularIndicator {
|
||||
bgColor: "transparent"
|
||||
running: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
|
||||
onClicked: {
|
||||
if (networkItem.isSecure && !networkItem.known) {
|
||||
PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), {
|
||||
network: networkItem.modelData
|
||||
});
|
||||
} else {
|
||||
Network.requestConnect(networkItem.modelData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
component Spacer: Item {
|
||||
id: spacer
|
||||
|
||||
required property Repeater repeater
|
||||
|
||||
Layout.preferredHeight: repeater.count > 0 ? Tokens.spacing.extraSmall : 0
|
||||
}
|
||||
component Toggle: CustomSwitch {
|
||||
id: toggleItem
|
||||
|
||||
readonly property alias bg: bg
|
||||
property alias first: bg.first
|
||||
property alias last: bg.last
|
||||
property string subtext
|
||||
|
||||
Layout.fillWidth: true
|
||||
cLayer: 2
|
||||
horizontalPadding: Tokens.padding.largeIncreased
|
||||
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
|
||||
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
|
||||
indicator.anchors.right: right
|
||||
indicator.anchors.rightMargin: toggleItem.horizontalPadding
|
||||
indicator.anchors.verticalCenter: verticalCenter
|
||||
verticalPadding: Tokens.padding.normal
|
||||
|
||||
background: ConnectedRect {
|
||||
id: bg
|
||||
|
||||
StateLayer {
|
||||
id: stateLayer
|
||||
|
||||
manualPressOverride: toggleItem.pressed
|
||||
}
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: toggleItem.horizontalPadding
|
||||
anchors.right: toggleItem.indicator.left
|
||||
anchors.rightMargin: Tokens.spacing.normal
|
||||
implicitHeight: column.implicitHeight
|
||||
implicitWidth: column.implicitWidth
|
||||
|
||||
Column {
|
||||
id: column
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 0
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(label.font);
|
||||
f.pointSize = Tokens.font.size.smaller;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.text
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: subtext
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
color: Colors.palette.m3outline
|
||||
elide: Text.ElideRight
|
||||
font: {
|
||||
const f = Qt.font(subtext.font);
|
||||
f.pointSize = Tokens.font.size.small;
|
||||
return f;
|
||||
}
|
||||
text: toggleItem.subtext
|
||||
visible: toggleItem.subtext
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
property string currentName
|
||||
property bool hasCurrent
|
||||
|
||||
signal detachRequested(mode: string)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Effects
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import ZShell.Config
|
||||
import qs.Services
|
||||
|
||||
StackView {
|
||||
id: root
|
||||
|
||||
readonly property int itemHeight: 30
|
||||
readonly property int panelRadius: ((itemHeight / 2) + Tokens.padding.small) * Tokens.rounding.scale
|
||||
required property PopoutState popouts
|
||||
property int rootWidth: 0
|
||||
required property QsMenuHandle trayItem
|
||||
|
||||
implicitHeight: currentItem.isSubMenu ? currentItem.implicitHeight : currentItem.implicitHeight - currentItem.spacing
|
||||
implicitWidth: currentItem.implicitWidth
|
||||
|
||||
initialItem: SubMenu {
|
||||
handle: root.trayItem
|
||||
}
|
||||
popEnter: NoAnim {
|
||||
}
|
||||
popExit: NoAnim {
|
||||
}
|
||||
pushEnter: NoAnim {
|
||||
}
|
||||
pushExit: NoAnim {
|
||||
}
|
||||
|
||||
Component {
|
||||
id: subMenuComp
|
||||
|
||||
SubMenu {
|
||||
}
|
||||
}
|
||||
|
||||
component NoAnim: Transition {
|
||||
NumberAnimation {
|
||||
duration: 0
|
||||
}
|
||||
}
|
||||
component SubMenu: ColumnLayout {
|
||||
id: menu
|
||||
|
||||
required property QsMenuHandle handle
|
||||
property bool isSubMenu
|
||||
property bool shown
|
||||
|
||||
opacity: shown ? 1 : 0
|
||||
scale: shown ? 1 : 0.8
|
||||
spacing: Tokens.spacing.extraSmall
|
||||
|
||||
Behavior on opacity {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
Behavior on scale {
|
||||
Anim {
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: shown = true
|
||||
StackView.onActivating: shown = true
|
||||
StackView.onDeactivating: shown = false
|
||||
StackView.onRemoved: destroy()
|
||||
|
||||
QsMenuOpener {
|
||||
id: menuOpener
|
||||
|
||||
menu: menu.handle
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: menuOpener.children
|
||||
|
||||
CustomRect {
|
||||
id: item
|
||||
|
||||
required property int index
|
||||
required property QsMenuEntry modelData
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: modelData.isSeparator ? Tokens.padding.normal : 0
|
||||
Layout.rightMargin: modelData.isSeparator ? Tokens.padding.normal : 0
|
||||
color: modelData.isSeparator ? Colors.palette.m3outlineVariant : "transparent"
|
||||
implicitHeight: modelData.isSeparator ? (visible ? 1 : 0) : childrenLoader.item.implicitHeight
|
||||
implicitWidth: childrenLoader.item?.implicitWidth ?? 0
|
||||
radius: Tokens.rounding.full
|
||||
visible: index !== (menuOpener.children.values.length - 1) ? true : (modelData.isSeparator ? false : true)
|
||||
|
||||
Loader {
|
||||
id: childrenLoader
|
||||
|
||||
active: !item.modelData.isSeparator
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Item {
|
||||
property int iconWidth: icon.active ? icon.width + Tokens.spacing.normal + icon.anchors.rightMargin : 0
|
||||
|
||||
implicitHeight: root.itemHeight
|
||||
implicitWidth: label.width + label.anchors.leftMargin * 2 + iconWidth + (expand.item?.width ?? 0)
|
||||
|
||||
StateLayer {
|
||||
enabled: item.modelData.enabled
|
||||
radius: item.radius
|
||||
|
||||
onClicked: {
|
||||
const entry = item.modelData;
|
||||
if (entry.hasChildren) {
|
||||
root.push(subMenuComp.createObject(null, {
|
||||
handle: entry,
|
||||
isSubMenu: true
|
||||
}));
|
||||
} else {
|
||||
item.modelData.triggered();
|
||||
root.popouts.hasCurrent = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: icon
|
||||
|
||||
active: item.modelData.icon !== ""
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: label.implicitHeight
|
||||
implicitWidth: label.implicitHeight
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
|
||||
implicitSize: parent.implicitHeight
|
||||
source: item.modelData.icon
|
||||
visible: false
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: iconImage
|
||||
colorization: 1.0
|
||||
colorizationColor: item.modelData.enabled ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
source: iconImage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
id: label
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: 10
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: item.modelData.enabled ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
text: item.modelData.text
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: expand
|
||||
|
||||
active: item.modelData.hasChildren
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: MaterialIcon {
|
||||
color: item.modelData.enabled ? Colors.palette.m3onSurface : Colors.palette.m3outline
|
||||
text: "chevron_right"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: loader
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.maximumHeight: active ? implicitHeight : 0
|
||||
active: menu.isSubMenu
|
||||
asynchronous: true
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: 30
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
implicitHeight: 30
|
||||
|
||||
CustomRect {
|
||||
anchors.fill: parent
|
||||
color: Colors.palette.m3secondaryContainer
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
StateLayer {
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
radius: parent.radius
|
||||
|
||||
onClicked: {
|
||||
root.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
id: back
|
||||
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
text: "chevron_left"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onSecondaryContainer
|
||||
text: qsTr("Back")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.UPower
|
||||
import ZShell.Config
|
||||
import qs.Modules.Bar.Components.Common
|
||||
import qs.Helpers
|
||||
import qs.Components
|
||||
import qs.Services
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
readonly property int panelRadius: ((profiles.height / 2) + Tokens.padding.small) * Tokens.rounding.scale
|
||||
|
||||
spacing: Tokens.spacing.normal
|
||||
|
||||
Loader {
|
||||
active: Battery.isLaptop
|
||||
|
||||
CustomText {
|
||||
text: qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100))
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: Battery.allPeripherals
|
||||
|
||||
RowLayout {
|
||||
id: layout
|
||||
|
||||
required property var modelData
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.normal
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.normal
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
text: layout.modelData.model
|
||||
}
|
||||
|
||||
BatteryIcon {
|
||||
devState: layout.modelData.state
|
||||
percentage: layout.modelData.percentage
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
function formatSeconds(s: int, fallback: string): string {
|
||||
const day = Math.floor(s / 86400);
|
||||
const hr = Math.floor(s / 3600) % 60;
|
||||
const min = Math.floor(s / 60) % 60;
|
||||
|
||||
let comps = [];
|
||||
if (day > 0)
|
||||
comps.push(`${day} days`);
|
||||
if (hr > 0)
|
||||
comps.push(`${hr} hours`);
|
||||
if (min > 0)
|
||||
comps.push(`${min} mins`);
|
||||
|
||||
return comps.join(", ") || fallback;
|
||||
}
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.normal
|
||||
text: Battery.isLaptop ? qsTr("Time %1: %2").arg(Battery.onBattery ? "remaining" : "until charged").arg(Battery.onBattery ? formatSeconds(Battery.timeToEmpty, "Calculating...") : formatSeconds(Battery.timeToFull, "Fully charged!")) : qsTr("Power profile: %1").arg(PowerProfile.toString(PowerProfiles.profile))
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
asynchronous: true
|
||||
height: active ? ((item as Item)?.implicitHeight ?? 0) : 0
|
||||
|
||||
sourceComponent: CustomRect {
|
||||
color: Colors.palette.m3error
|
||||
implicitHeight: child.implicitHeight + Tokens.padding.large
|
||||
implicitWidth: child.implicitWidth + Tokens.padding.larger * 2
|
||||
radius: Tokens.rounding.large
|
||||
|
||||
Column {
|
||||
id: child
|
||||
|
||||
anchors.centerIn: parent
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Tokens.spacing.small
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
color: Colors.palette.m3onError
|
||||
text: "warning"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: Colors.palette.m3onError
|
||||
font.family: Config.appearance.font.family.mono
|
||||
text: qsTr("Performance Degraded")
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.verticalCenterOffset: -font.pointSize / 10
|
||||
color: Colors.palette.m3onError
|
||||
text: "warning"
|
||||
}
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Colors.palette.m3onError
|
||||
text: qsTr("Reason: %1").arg(PerformanceDegradationReason.toString(PowerProfiles.degradationReason))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CustomRect {
|
||||
id: profiles
|
||||
|
||||
property string current: {
|
||||
const p = PowerProfiles.profile;
|
||||
if (p === PowerProfile.PowerSaver)
|
||||
return saver.icon;
|
||||
if (p === PowerProfile.Performance)
|
||||
return perf.icon;
|
||||
return balance.icon;
|
||||
}
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: indicator.height + Tokens.padding.extraSmall * 2
|
||||
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.larger * 2 + Tokens.spacing.small * 8
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
CustomRect {
|
||||
id: indicator
|
||||
|
||||
color: Colors.palette.m3primary
|
||||
radius: Tokens.rounding.full
|
||||
state: profiles.current
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: saver.icon
|
||||
|
||||
Fill {
|
||||
item: saver
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: balance.icon
|
||||
|
||||
Fill {
|
||||
item: balance
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: perf.icon
|
||||
|
||||
Fill {
|
||||
item: perf
|
||||
}
|
||||
}
|
||||
]
|
||||
transitions: Transition {
|
||||
AnchorAnim {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: saver
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Tokens.padding.extraSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
icon: "energy_savings_leaf"
|
||||
profile: PowerProfile.PowerSaver
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: balance
|
||||
|
||||
anchors.centerIn: parent
|
||||
icon: "balance"
|
||||
profile: PowerProfile.Balanced
|
||||
}
|
||||
|
||||
Profile {
|
||||
id: perf
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Tokens.padding.extraSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
icon: "bolt"
|
||||
profile: PowerProfile.Performance
|
||||
}
|
||||
}
|
||||
|
||||
component Fill: AnchorChanges {
|
||||
required property Item item
|
||||
|
||||
anchors.bottom: item.bottom
|
||||
anchors.left: item.left
|
||||
anchors.right: item.right
|
||||
anchors.top: item.top
|
||||
target: indicator
|
||||
}
|
||||
component Profile: Item {
|
||||
required property string icon
|
||||
required property int profile
|
||||
|
||||
implicitHeight: icon.implicitHeight + Tokens.padding.small
|
||||
implicitWidth: icon.implicitHeight + Tokens.padding.small
|
||||
|
||||
StateLayer {
|
||||
color: profiles.current === parent.icon ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
radius: Tokens.rounding.full
|
||||
|
||||
onClicked: {
|
||||
PowerProfiles.profile = parent.profile;
|
||||
}
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
id: icon
|
||||
|
||||
anchors.centerIn: parent
|
||||
color: profiles.current === text ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
|
||||
fill: profiles.current === text ? 1 : 0
|
||||
font.pointSize: Tokens.font.size.large * 2
|
||||
grade: Colors.light ? 0 : (text === "balance" ? -25 : 0)
|
||||
text: parent.icon
|
||||
|
||||
Behavior on fill {
|
||||
Anim {
|
||||
type: Anim.DefaultEffects
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import ZShell.Config
|
||||
import qs.Components
|
||||
import qs.Modules
|
||||
import qs.Helpers
|
||||
import qs.Services
|
||||
|
||||
CustomClippingRect {
|
||||
id: root
|
||||
|
||||
readonly property bool hasUpdates: Object.keys(Updates.updates)?.length > 0
|
||||
readonly property int itemHeight: 50 + Tokens.padding.smaller * 2
|
||||
required property var wrapper
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: hasUpdates ? updatesListLoader.item?.implicitHeight + Tokens.padding.small * 2 : noUpdatesLoader.item.height
|
||||
implicitWidth: hasUpdates ? updatesListLoader.item?.contentWidth + Tokens.padding.small * 2 : noUpdatesLoader.item.width
|
||||
radius: Tokens.rounding.small
|
||||
|
||||
Loader {
|
||||
id: noUpdatesLoader
|
||||
|
||||
active: !root.hasUpdates
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: Item {
|
||||
id: noUpdates
|
||||
|
||||
height: 200
|
||||
width: 300
|
||||
|
||||
MaterialIcon {
|
||||
id: noUpdatesIcon
|
||||
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: parent.top
|
||||
color: Colors.tPalette.m3onSurfaceVariant
|
||||
font.pointSize: Tokens.font.size.extraLarge * 3
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "check"
|
||||
}
|
||||
|
||||
CustomText {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.top: noUpdatesIcon.bottom
|
||||
color: Colors.tPalette.m3onSurfaceVariant
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: qsTr("No updates available")
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: updatesListLoader
|
||||
|
||||
active: root.hasUpdates
|
||||
anchors.centerIn: parent
|
||||
|
||||
sourceComponent: CustomListView {
|
||||
id: updatesList
|
||||
|
||||
contentHeight: childrenRect.height
|
||||
contentWidth: 600
|
||||
displayMarginBeginning: root.itemHeight
|
||||
displayMarginEnd: root.itemHeight
|
||||
implicitHeight: Math.min(contentHeight, (root.itemHeight + spacing) * 5 - spacing)
|
||||
implicitWidth: contentWidth
|
||||
spacing: Tokens.spacing.normal
|
||||
|
||||
delegate: CustomRect {
|
||||
id: update
|
||||
|
||||
required property var modelData
|
||||
readonly property list<string> sections: modelData.update.split(" ")
|
||||
|
||||
color: Colors.tPalette.m3surfaceContainer
|
||||
implicitHeight: root.itemHeight
|
||||
implicitWidth: 600
|
||||
radius: Tokens.rounding.small - Tokens.padding.small
|
||||
|
||||
RowLayout {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Tokens.padding.smaller
|
||||
anchors.rightMargin: Tokens.padding.smaller
|
||||
|
||||
MaterialIcon {
|
||||
font.pointSize: Tokens.font.size.large * 2
|
||||
text: "package_2"
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 25
|
||||
elide: Text.ElideRight
|
||||
font.pointSize: Tokens.font.size.large
|
||||
text: update.sections[0]
|
||||
}
|
||||
|
||||
CustomText {
|
||||
Layout.fillWidth: true
|
||||
color: Colors.palette.m3onSurfaceVariant
|
||||
text: Updates.formatUpdateTime(update.modelData.timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 300
|
||||
|
||||
MarqueeText {
|
||||
id: versionFrom
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 125
|
||||
animate: true
|
||||
color: Colors.palette.m3tertiary
|
||||
font.pointSize: Tokens.font.size.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
marqueeEnabled: true
|
||||
pauseMs: 4000
|
||||
text: update.sections[1]
|
||||
width: 125
|
||||
}
|
||||
|
||||
MaterialIcon {
|
||||
Layout.fillHeight: true
|
||||
color: Colors.palette.m3secondary
|
||||
font.pointSize: Tokens.font.size.extraLarge
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "arrow_right_alt"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
|
||||
MarqueeText {
|
||||
id: versionTo
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.preferredWidth: 120
|
||||
animate: true
|
||||
color: Colors.palette.m3primary
|
||||
font.pointSize: Tokens.font.size.large
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
marqueeEnabled: true
|
||||
pauseMs: 4000
|
||||
text: update.sections[3]
|
||||
width: 125
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
model: ScriptModel {
|
||||
id: script
|
||||
|
||||
objectProp: "update"
|
||||
values: Object.entries(Updates.updates).sort((a, b) => b[1] - a[1]).map(([update, timestamp]) => ({
|
||||
update,
|
||||
timestamp
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
import QtQuick
|
||||
import qs.Components
|
||||
import ZShell.Config
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property list<real> animCurve: Tokens.anim.curves.expressiveDefaultSpatial
|
||||
property int animLength: Tokens.anim.durations.expressiveDefaultSpatial
|
||||
readonly property alias content: content
|
||||
readonly property Item current: (content.item as Content)?.current ?? null
|
||||
property real currentCenter
|
||||
property alias currentName: popoutState.currentName
|
||||
property string detachedMode
|
||||
property alias hasCurrent: popoutState.hasCurrent
|
||||
readonly property real nonAnimHeight: content.implicitHeight || 150
|
||||
readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth
|
||||
required property real offsetScale
|
||||
property string queuedMode
|
||||
required property ShellScreen screen
|
||||
|
||||
function close(): void {
|
||||
hasCurrent = false;
|
||||
detachedMode = "";
|
||||
}
|
||||
|
||||
implicitHeight: nonAnimHeight
|
||||
implicitWidth: nonAnimWidth
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.offsetScale < 1
|
||||
|
||||
Anim {
|
||||
duration: root.animLength
|
||||
easing.bezierCurve: root.animCurve
|
||||
}
|
||||
}
|
||||
|
||||
PopoutState {
|
||||
id: popoutState
|
||||
}
|
||||
|
||||
Comp {
|
||||
id: content
|
||||
|
||||
anchors.centerIn: parent
|
||||
shouldBeActive: root.hasCurrent
|
||||
|
||||
sourceComponent: Content {
|
||||
popouts: popoutState
|
||||
}
|
||||
}
|
||||
|
||||
component Comp: Loader {
|
||||
id: comp
|
||||
|
||||
property bool shouldBeActive
|
||||
|
||||
active: false
|
||||
opacity: 0
|
||||
|
||||
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||
states: State {
|
||||
name: "active"
|
||||
when: comp.shouldBeActive
|
||||
|
||||
PropertyChanges {
|
||||
comp.active: true
|
||||
comp.opacity: 1
|
||||
}
|
||||
}
|
||||
transitions: [
|
||||
Transition {
|
||||
from: ""
|
||||
to: "active"
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
}
|
||||
},
|
||||
Transition {
|
||||
from: "active"
|
||||
to: ""
|
||||
|
||||
SequentialAnimation {
|
||||
Anim {
|
||||
property: "opacity"
|
||||
}
|
||||
|
||||
PropertyAction {
|
||||
property: "active"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user