mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 07:22:50 -05:00
redesign control center
This commit is contained in:
10
CLAUDE.md
10
CLAUDE.md
@@ -87,7 +87,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
│ ├── AudioService.qml
|
│ ├── AudioService.qml
|
||||||
│ ├── NetworkService.qml
|
│ ├── NetworkService.qml
|
||||||
│ ├── BluetoothService.qml
|
│ ├── BluetoothService.qml
|
||||||
│ ├── BrightnessService.qml
|
│ ├── DisplayService.qml
|
||||||
│ ├── NotificationService.qml
|
│ ├── NotificationService.qml
|
||||||
│ ├── WeatherService.qml
|
│ ├── WeatherService.qml
|
||||||
│ └── [14 more services]
|
│ └── [14 more services]
|
||||||
@@ -131,7 +131,7 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
3. **Services/** - System integration singletons
|
3. **Services/** - System integration singletons
|
||||||
- **Pattern**: All services use `Singleton` type with `id: root`
|
- **Pattern**: All services use `Singleton` type with `id: root`
|
||||||
- **Independence**: No cross-service dependencies
|
- **Independence**: No cross-service dependencies
|
||||||
- **Examples**: AudioService, NetworkService, BluetoothService, BrightnessService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
|
- **Examples**: AudioService, NetworkService, BluetoothService, DisplayService, WeatherService, NotificationService, CalendarService, BatteryService, NiriService, MprisController
|
||||||
- Services handle system commands, state management, and hardware integration
|
- Services handle system commands, state management, and hardware integration
|
||||||
|
|
||||||
4. **Modules/** - UI components (93 files)
|
4. **Modules/** - UI components (93 files)
|
||||||
@@ -294,9 +294,9 @@ shell.qml # Main entry point (minimal orchestration)
|
|||||||
|
|
||||||
// In modules - adapt UI accordingly
|
// In modules - adapt UI accordingly
|
||||||
DankSlider {
|
DankSlider {
|
||||||
visible: BrightnessService.brightnessAvailable
|
visible: DisplayService.brightnessAvailable
|
||||||
enabled: BrightnessService.brightnessAvailable
|
enabled: DisplayService.brightnessAvailable
|
||||||
value: BrightnessService.brightnessLevel
|
value: DisplayService.brightnessLevel
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentSinkDisplayName: AudioService.sink ? AudioService.displayName(
|
|
||||||
AudioService.sink) : ""
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Output Device"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
visible: AudioService.sink !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "check_circle"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.parent.width - parent.anchors.leftMargin - Theme.spacingS - Theme.iconSize
|
|
||||||
text: "Current: " + (root.currentSinkDisplayName || "None")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Pipewire.nodes.values.filter(node => {
|
|
||||||
return node.audio && node.isSink && !node.isStream
|
|
||||||
})
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: deviceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08) : (modelData === AudioService.sink ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData === AudioService.sink ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (modelData.name.includes("bluez"))
|
|
||||||
return "headset"
|
|
||||||
else if (modelData.name.includes("hdmi"))
|
|
||||||
return "tv"
|
|
||||||
else if (modelData.name.includes("usb"))
|
|
||||||
return "headset"
|
|
||||||
else
|
|
||||||
return "speaker"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - parent.spacing - Theme.iconSize
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: AudioService.displayName(modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: {
|
|
||||||
if (AudioService.subtitle(modelData.name)
|
|
||||||
&& AudioService.subtitle(
|
|
||||||
modelData.name) !== "")
|
|
||||||
return AudioService.subtitle(modelData.name)
|
|
||||||
+ (modelData === AudioService.sink ? " • Selected" : "")
|
|
||||||
else
|
|
||||||
return modelData === AudioService.sink ? "Selected" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text !== ""
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: deviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
Pipewire.preferredDefaultAudioSink = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property string currentSourceDisplayName: AudioService.source ? AudioService.displayName(
|
|
||||||
AudioService.source) : ""
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Input Device"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 35
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
|
||||||
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.3)
|
|
||||||
border.width: 1
|
|
||||||
visible: AudioService.source !== null
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "check_circle"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.parent.width - parent.anchors.leftMargin - Theme.spacingS - Theme.iconSize
|
|
||||||
text: "Current: " + (root.currentSourceDisplayName || "None")
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: Pipewire.nodes.values.filter(node => {
|
|
||||||
return node.audio && !node.isSink && !node.isStream
|
|
||||||
})
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 50
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: sourceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08) : (modelData === AudioService.source ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData === AudioService.source ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: {
|
|
||||||
if (modelData.name.includes("bluez"))
|
|
||||||
return "headset_mic"
|
|
||||||
else if (modelData.name.includes("usb"))
|
|
||||||
return "headset_mic"
|
|
||||||
else
|
|
||||||
return "mic"
|
|
||||||
}
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - parent.spacing - Theme.iconSize
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: AudioService.displayName(modelData)
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: {
|
|
||||||
if (AudioService.subtitle(modelData.name)
|
|
||||||
&& AudioService.subtitle(
|
|
||||||
modelData.name) !== "")
|
|
||||||
return AudioService.subtitle(modelData.name)
|
|
||||||
+ (modelData === AudioService.source ? " • Selected" : "")
|
|
||||||
else
|
|
||||||
return modelData === AudioService.source ? "Selected" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text !== ""
|
|
||||||
elide: Text.ElideRight
|
|
||||||
wrapMode: Text.NoWrap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: sourceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
Pipewire.preferredDefaultAudioSource = modelData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property real micLevel: Math.min(100,
|
|
||||||
(AudioService.source
|
|
||||||
&& AudioService.source.audio
|
|
||||||
&& AudioService.source.audio.volume * 100)
|
|
||||||
|| 0)
|
|
||||||
property bool micMuted: (AudioService.source && AudioService.source.audio
|
|
||||||
&& AudioService.source.audio.muted) || false
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Microphone Level"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.micMuted ? "mic_off" : "mic"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: root.micMuted ? Theme.error : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.source && AudioService.source.audio)
|
|
||||||
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: micSliderContainer
|
|
||||||
|
|
||||||
width: parent.width - 80
|
|
||||||
height: 32
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micSliderTrack
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 8
|
|
||||||
radius: 4
|
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micSliderFill
|
|
||||||
|
|
||||||
width: parent.width * (root.micLevel / 100)
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.standardDecel
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micHandle
|
|
||||||
|
|
||||||
width: 18
|
|
||||||
height: 18
|
|
||||||
radius: 9
|
|
||||||
color: Theme.primary
|
|
||||||
border.color: Qt.lighter(Theme.primary, 1.3)
|
|
||||||
border.width: 2
|
|
||||||
x: Math.max(0, Math.min(parent.width - width,
|
|
||||||
micSliderFill.width - width / 2))
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
scale: micMouseArea.containsMouse
|
|
||||||
|| micMouseArea.pressed ? 1.2 : 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: micTooltip
|
|
||||||
|
|
||||||
width: tooltipText.contentWidth + Theme.spacingS * 2
|
|
||||||
height: tooltipText.contentHeight + Theme.spacingXS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.surfaceContainer
|
|
||||||
border.color: Theme.outline
|
|
||||||
border.width: 1
|
|
||||||
anchors.bottom: parent.top
|
|
||||||
anchors.bottomMargin: Theme.spacingS
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
visible: (micMouseArea.containsMouse && !root.micMuted)
|
|
||||||
|| micMouseArea.isDragging
|
|
||||||
opacity: visible ? 1 : 0
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: tooltipText
|
|
||||||
|
|
||||||
text: Math.round(root.micLevel) + "%"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Anims.durShort
|
|
||||||
easing.type: Easing.BezierSpline
|
|
||||||
easing.bezierCurve: Anims.standard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: micMouseArea
|
|
||||||
|
|
||||||
property bool isDragging: false
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
preventStealing: true
|
|
||||||
onPressed: mouse => {
|
|
||||||
isDragging = true
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.round(ratio * 100)
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
isDragging = false
|
|
||||||
}
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (pressed && isDragging) {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.max(
|
|
||||||
0, Math.min(100, Math.round(
|
|
||||||
ratio * 100)))
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onClicked: mouse => {
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(1,
|
|
||||||
mouse.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.round(ratio * 100)
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: micGlobalMouseArea
|
|
||||||
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: root.parent ? root.parent.width : 0
|
|
||||||
height: root.parent ? root.parent.height : 0
|
|
||||||
enabled: micMouseArea.isDragging
|
|
||||||
visible: false
|
|
||||||
preventStealing: true
|
|
||||||
onPositionChanged: mouse => {
|
|
||||||
if (micMouseArea.isDragging) {
|
|
||||||
let globalPos = mapToItem(
|
|
||||||
micSliderTrack, mouse.x, mouse.y)
|
|
||||||
let ratio = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
1,
|
|
||||||
globalPos.x / micSliderTrack.width))
|
|
||||||
let newMicLevel = Math.max(
|
|
||||||
0, Math.min(100, Math.round(
|
|
||||||
ratio * 100)))
|
|
||||||
if (AudioService.source
|
|
||||||
&& AudioService.source.audio) {
|
|
||||||
AudioService.source.audio.muted = false
|
|
||||||
AudioService.source.audio.volume = newMicLevel / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onReleased: {
|
|
||||||
micMouseArea.isDragging = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "mic"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Volume"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: volumeSlider
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
minimum: 0
|
|
||||||
maximum: 100
|
|
||||||
value: AudioService.sink.audio ? Math.round(AudioService.sink.audio.volume * 100) : 0
|
|
||||||
leftIcon: (AudioService.sink.audio && AudioService.sink.audio.muted) ? "volume_off" : "volume_down"
|
|
||||||
rightIcon: "volume_up"
|
|
||||||
enabled: !(AudioService.sink.audio && AudioService.sink.audio.muted)
|
|
||||||
showValue: true
|
|
||||||
unit: "%"
|
|
||||||
|
|
||||||
onSliderValueChanged: newValue => {
|
|
||||||
if (AudioService.sink?.ready && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.volume = newValue / 100
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: 0
|
|
||||||
y: 0
|
|
||||||
width: Theme.iconSize
|
|
||||||
height: parent.height
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (AudioService.sink?.ready && AudioService.sink.audio) {
|
|
||||||
AudioService.sink.audio.muted = !AudioService.sink.audio.muted
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Pipewire
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter.Audio
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: audioTab
|
|
||||||
|
|
||||||
property int audioSubTab: 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankTabBar {
|
|
||||||
width: parent.width
|
|
||||||
tabHeight: 40
|
|
||||||
currentIndex: audioTab.audioSubTab
|
|
||||||
showIcons: false
|
|
||||||
model: [{
|
|
||||||
"text": "Output"
|
|
||||||
}, {
|
|
||||||
"text": "Input"
|
|
||||||
}]
|
|
||||||
onTabClicked: function (index) {
|
|
||||||
audioTab.audioSubTab = index
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Single Loader that switches between Output and Input
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 48
|
|
||||||
asynchronous: true
|
|
||||||
sourceComponent: audioTab.audioSubTab === 0 ? outputTabComponent : inputTabComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output Tab Component
|
|
||||||
Component {
|
|
||||||
id: outputTabComponent
|
|
||||||
DankFlickable {
|
|
||||||
clip: true
|
|
||||||
contentHeight: outputColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: outputColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: volumeComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: outputDevicesComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Tab Component
|
|
||||||
Component {
|
|
||||||
id: inputTabComponent
|
|
||||||
DankFlickable {
|
|
||||||
clip: true
|
|
||||||
contentHeight: inputColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: inputColumn
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: microphoneComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: inputDevicesComponent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Volume Control Component
|
|
||||||
Component {
|
|
||||||
id: volumeComponent
|
|
||||||
VolumeControl {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Microphone Control Component
|
|
||||||
Component {
|
|
||||||
id: microphoneComponent
|
|
||||||
MicrophoneControl {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Output Devices Component
|
|
||||||
Component {
|
|
||||||
id: outputDevicesComponent
|
|
||||||
AudioDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Input Devices Component
|
|
||||||
Component {
|
|
||||||
id: inputDevicesComponent
|
|
||||||
AudioInputDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,452 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: availableDevicesText
|
|
||||||
text: "Available Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - availableDevicesText.width - scanButton.width - parent.spacing * 2
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: scanButton
|
|
||||||
|
|
||||||
width: Math.min(Math.max(100, scanText.contentWidth + Theme.spacingL * 2), parent.width * 0.3)
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: scanArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08)
|
|
||||||
border.color: Theme.primary
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: scanText
|
|
||||||
|
|
||||||
text: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.discovering ? "Stop Scanning" : "Scan"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: scanArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (BluetoothService.adapter)
|
|
||||||
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: noteColumn.implicitHeight + Theme.spacingM * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.08)
|
|
||||||
border.color: Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: noteColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingM
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.warning
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Pairing Limitation"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.warning
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Quickshell does not support pairing devices that require pin or confirmation."
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.8)
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: {
|
|
||||||
if (!BluetoothService.adapter
|
|
||||||
|| !BluetoothService.adapter.discovering
|
|
||||||
|| !Bluetooth.devices)
|
|
||||||
return []
|
|
||||||
|
|
||||||
var filtered = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength === undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
})
|
|
||||||
return BluetoothService.sortDevices(filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
property bool canConnect: BluetoothService.canConnect(modelData)
|
|
||||||
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 70
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (availableDeviceArea.containsMouse && !isBusy)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.08)
|
|
||||||
|
|
||||||
if (modelData.pairing
|
|
||||||
|| modelData.state === BluetoothDeviceState.Connecting)
|
|
||||||
return Qt.rgba(Theme.warning.r, Theme.warning.g,
|
|
||||||
Theme.warning.b, 0.12)
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Qt.rgba(Theme.error.r, Theme.error.g,
|
|
||||||
Theme.error.b, 0.08)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.08)
|
|
||||||
}
|
|
||||||
border.color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name || modelData.deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Theme.surfaceText
|
|
||||||
}
|
|
||||||
font.weight: modelData.pairing ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return "Pairing..."
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return "Blocked"
|
|
||||||
|
|
||||||
return BluetoothService.getSignalStrength(
|
|
||||||
modelData)
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return Theme.warning
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getSignalIcon(modelData)
|
|
||||||
size: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength > 0
|
|
||||||
&& !modelData.pairing
|
|
||||||
&& !modelData.blocked
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: (modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength
|
|
||||||
> 0) ? modelData.signalStrength + "%" : ""
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
visible: modelData.signalStrength !== undefined
|
|
||||||
&& modelData.signalStrength > 0
|
|
||||||
&& !modelData.pairing
|
|
||||||
&& !modelData.blocked
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 80
|
|
||||||
height: 28
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: modelData.state !== BluetoothDeviceState.Connecting
|
|
||||||
color: {
|
|
||||||
if (!canConnect && !isBusy)
|
|
||||||
return Qt.rgba(Theme.surfaceVariant.r,
|
|
||||||
Theme.surfaceVariant.g,
|
|
||||||
Theme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
if (actionButtonArea.containsMouse && !isBusy)
|
|
||||||
return Qt.rgba(Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12)
|
|
||||||
|
|
||||||
return "transparent"
|
|
||||||
}
|
|
||||||
border.color: canConnect || isBusy ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
border.width: 1
|
|
||||||
opacity: canConnect || isBusy ? 1 : 0.5
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: {
|
|
||||||
if (modelData.pairing)
|
|
||||||
return "Pairing..."
|
|
||||||
|
|
||||||
if (modelData.blocked)
|
|
||||||
return "Blocked"
|
|
||||||
|
|
||||||
return "Connect"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: canConnect || isBusy ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.5)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: actionButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: canConnect
|
|
||||||
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
|
||||||
enabled: canConnect && !isBusy
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: availableDeviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 90
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: canConnect
|
|
||||||
&& !isBusy ? Qt.PointingHandCursor : (isBusy ? Qt.BusyCursor : Qt.ArrowCursor)
|
|
||||||
enabled: canConnect && !isBusy
|
|
||||||
onClicked: {
|
|
||||||
if (modelData)
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: {
|
|
||||||
if (!BluetoothService.adapter
|
|
||||||
|| !BluetoothService.adapter.discovering
|
|
||||||
|| !Bluetooth.devices)
|
|
||||||
return false
|
|
||||||
|
|
||||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength
|
|
||||||
=== undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
}).length
|
|
||||||
return availableCount === 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "sync"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
RotationAnimation on rotation {
|
|
||||||
running: true
|
|
||||||
loops: Animation.Infinite
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 2000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Scanning for devices..."
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Make sure your device is in pairing mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "No devices found. Put your device in pairing mode and click Start Scanning."
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: {
|
|
||||||
if (!BluetoothService.adapter || !Bluetooth.devices)
|
|
||||||
return true
|
|
||||||
|
|
||||||
var availableCount = Bluetooth.devices.values.filter(dev => {
|
|
||||||
return dev
|
|
||||||
&& !dev.paired
|
|
||||||
&& !dev.pairing
|
|
||||||
&& !dev.blocked
|
|
||||||
&& (dev.signalStrength
|
|
||||||
=== undefined
|
|
||||||
|| dev.signalStrength > 0)
|
|
||||||
}).length
|
|
||||||
return availableCount === 0 && !BluetoothService.adapter.discovering
|
|
||||||
}
|
|
||||||
wrapMode: Text.WordWrap
|
|
||||||
width: parent.width
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var deviceData: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
property var parentItem
|
|
||||||
property var codecSelector
|
|
||||||
|
|
||||||
function show(x, y) {
|
|
||||||
const menuWidth = 160;
|
|
||||||
const menuHeight = menuColumn.implicitHeight + Theme.spacingS * 2;
|
|
||||||
let finalX = x - menuWidth / 2;
|
|
||||||
let finalY = y;
|
|
||||||
finalX = Math.max(0, Math.min(finalX, parentItem.width - menuWidth));
|
|
||||||
finalY = Math.max(0, Math.min(finalY, parentItem.height - menuHeight));
|
|
||||||
root.x = finalX;
|
|
||||||
root.y = finalY;
|
|
||||||
root.visible = true;
|
|
||||||
root.menuVisible = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
root.menuVisible = false;
|
|
||||||
Qt.callLater(() => {
|
|
||||||
root.visible = false;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 160
|
|
||||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: menuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: root.deviceData && root.deviceData.connected ? "link_off" : "link"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: root.deviceData && root.deviceData.connected ? "Disconnect" : "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.deviceData) {
|
|
||||||
if (root.deviceData.connected)
|
|
||||||
root.deviceData.disconnect();
|
|
||||||
else
|
|
||||||
BluetoothService.connectDeviceWithTrust(root.deviceData);
|
|
||||||
}
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: codecArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
visible: root.deviceData && BluetoothService.isAudioDevice(root.deviceData) && root.deviceData.connected
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "high_quality"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Audio Codec"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: codecArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
codecSelector.show(root.deviceData);
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: forgetArea.containsMouse ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "delete"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Forget Device"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: forgetArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: forgetArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (root.deviceData)
|
|
||||||
root.deviceData.forget();
|
|
||||||
|
|
||||||
root.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: bluetoothToggle.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r, Theme.primary.g,
|
|
||||||
Theme.primary.b, 0.12) : (BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.16) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.12))
|
|
||||||
border.color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : "transparent"
|
|
||||||
border.width: 2
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingL
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "bluetooth"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Bluetooth"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.enabled ? "Enabled" : "Disabled"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: bluetoothToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (BluetoothService.adapter)
|
|
||||||
BluetoothService.adapter.enabled = !BluetoothService.adapter.enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
function findBluetoothContextMenu() {
|
|
||||||
var p = parent
|
|
||||||
while (p) {
|
|
||||||
if (p.bluetoothContextMenuWindow)
|
|
||||||
return p.bluetoothContextMenuWindow
|
|
||||||
p = p.parent
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Paired Devices"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: BluetoothService.adapter
|
|
||||||
&& BluetoothService.adapter.devices ? BluetoothService.adapter.devices.values.filter(
|
|
||||||
dev => {
|
|
||||||
return dev
|
|
||||||
&& (dev.paired
|
|
||||||
|| dev.trusted)
|
|
||||||
}) : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 60
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: btDeviceArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : (modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: BluetoothService.getDeviceIcon(modelData)
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
spacing: 2
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: modelData.name || modelData.deviceName
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: BluetoothDeviceState.toString(modelData.state)
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (modelData.batteryAvailable
|
|
||||||
&& modelData.battery > 0)
|
|
||||||
return "• " + Math.round(
|
|
||||||
modelData.battery * 100) + "%"
|
|
||||||
|
|
||||||
var btBattery = BatteryService.bluetoothDevices.find(
|
|
||||||
dev => {
|
|
||||||
return dev.name === (modelData.name
|
|
||||||
|| modelData.deviceName)
|
|
||||||
|| dev.name.toLowerCase(
|
|
||||||
).includes(
|
|
||||||
(modelData.name
|
|
||||||
|| modelData.deviceName).toLowerCase(
|
|
||||||
))
|
|
||||||
|| (modelData.name
|
|
||||||
|| modelData.deviceName).toLowerCase(
|
|
||||||
).includes(
|
|
||||||
dev.name.toLowerCase())
|
|
||||||
})
|
|
||||||
return btBattery ? "• " + btBattery.percentage + "%" : ""
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: btMenuButton
|
|
||||||
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: btMenuButtonArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b,
|
|
||||||
0.08) : "transparent"
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "more_vert"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: btMenuButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
var contextMenu = root.findBluetoothContextMenu()
|
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.deviceData = modelData
|
|
||||||
let localPos = btMenuButtonArea.mapToItem(
|
|
||||||
contextMenu.parentItem,
|
|
||||||
btMenuButtonArea.width / 2,
|
|
||||||
btMenuButtonArea.height)
|
|
||||||
contextMenu.show(localPos.x, localPos.y)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: btDeviceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 40
|
|
||||||
hoverEnabled: true
|
|
||||||
enabled: !BluetoothService.isDeviceBusy(modelData)
|
|
||||||
cursorShape: enabled ? Qt.PointingHandCursor : Qt.BusyCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.connected)
|
|
||||||
modelData.disconnect()
|
|
||||||
else
|
|
||||||
BluetoothService.connectDeviceWithTrust(modelData)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,108 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules.ControlCenter.Bluetooth
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: bluetoothTab
|
|
||||||
|
|
||||||
property alias bluetoothContextMenuWindow: bluetoothContextMenuWindow
|
|
||||||
property alias contentHeight: mainColumn.height
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: toggleComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: pairedComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: availableComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothContextMenu {
|
|
||||||
id: bluetoothContextMenuWindow
|
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
|
||||||
codecSelector: codecSelector
|
|
||||||
}
|
|
||||||
|
|
||||||
BluetoothCodecSelector {
|
|
||||||
id: codecSelector
|
|
||||||
|
|
||||||
parentItem: bluetoothTab
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: bluetoothContextMenuWindow.visible || codecSelector.visible
|
|
||||||
onClicked: {
|
|
||||||
bluetoothContextMenuWindow.hide();
|
|
||||||
codecSelector.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: bluetoothContextMenuWindow.x
|
|
||||||
y: bluetoothContextMenuWindow.y
|
|
||||||
width: bluetoothContextMenuWindow.width
|
|
||||||
height: bluetoothContextMenuWindow.height
|
|
||||||
onClicked: {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: toggleComponent
|
|
||||||
|
|
||||||
BluetoothToggle {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: pairedComponent
|
|
||||||
|
|
||||||
PairedDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: availableComponent
|
|
||||||
|
|
||||||
AvailableDevicesList {
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
178
Modules/ControlCenter/Details/AudioInputDetail.qml
Normal file
178
Modules/ControlCenter/Details/AudioInputDetail.qml
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: headerRow.height + volumeSlider.height + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Input Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
id: volumeSlider
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingXS
|
||||||
|
height: 35
|
||||||
|
value: AudioService.source && AudioService.source.audio ? Math.round(AudioService.source.audio.volume * 100) : 0
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
leftIcon: {
|
||||||
|
if (!AudioService.source || !AudioService.source.audio) return "mic_off"
|
||||||
|
let muted = AudioService.source.audio.muted
|
||||||
|
return muted ? "mic_off" : "mic"
|
||||||
|
}
|
||||||
|
rightIcon: "volume_up"
|
||||||
|
enabled: AudioService.source && AudioService.source.audio
|
||||||
|
unit: "%"
|
||||||
|
showValue: true
|
||||||
|
visible: AudioService.source && AudioService.source.audio
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.volume = newValue / 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: parent.height
|
||||||
|
onClicked: {
|
||||||
|
if (AudioService.source && AudioService.source.audio) {
|
||||||
|
AudioService.source.audio.muted = !AudioService.source.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: volumeSlider.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && !node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData === AudioService.source ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.source ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.source ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.source ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSource = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
138
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
138
Modules/ControlCenter/Details/AudioOutputDetail.qml
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: headerRow.height + audioContent.height + Theme.spacingM
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Audio Devices"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: audioContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
contentHeight: audioColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: audioColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: Pipewire.nodes.values.filter(node => {
|
||||||
|
return node.audio && node.isSink && !node.isStream
|
||||||
|
})
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData === AudioService.sink ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
if (modelData.name.includes("bluez"))
|
||||||
|
return "headset"
|
||||||
|
else if (modelData.name.includes("hdmi"))
|
||||||
|
return "tv"
|
||||||
|
else if (modelData.name.includes("usb"))
|
||||||
|
return "headset"
|
||||||
|
else
|
||||||
|
return "speaker"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.parent.width - parent.parent.anchors.leftMargin - parent.spacing - Theme.iconSize - Theme.spacingM
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: AudioService.displayName(modelData)
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData === AudioService.sink ? "Active" : "Available"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
Pipewire.preferredDefaultAudioSink = modelData
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
530
Modules/ControlCenter/Details/BluetoothDetail.qml
Normal file
530
Modules/ControlCenter/Details/BluetoothDetail.qml
Normal file
@@ -0,0 +1,530 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Bluetooth
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: BluetoothService.adapter && BluetoothService.adapter.enabled ? headerRow.height + bluetoothContent.height + Theme.spacingM : headerRow.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
if (BluetoothService.adapter && BluetoothService.adapter.enabled && !BluetoothService.adapter.discovering) {
|
||||||
|
BluetoothService.adapter.discovering = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
property var bluetoothCodecModalRef: bluetoothCodecModal
|
||||||
|
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Bluetooth Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - scanButton.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: scanButton
|
||||||
|
width: 100
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled)
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
return scanMouseArea.containsMouse ? Theme.surfaceContainerHigh : "transparent"
|
||||||
|
}
|
||||||
|
border.color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.adapter && BluetoothService.adapter.discovering ? "stop" : "bluetooth_searching"
|
||||||
|
size: 18
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: BluetoothService.adapter && BluetoothService.adapter.discovering ? "Scanning" : "Scan"
|
||||||
|
color: BluetoothService.adapter && BluetoothService.adapter.enabled ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: scanMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
onClicked: {
|
||||||
|
if (BluetoothService.adapter)
|
||||||
|
BluetoothService.adapter.discovering = !BluetoothService.adapter.discovering
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: bluetoothContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.enabled
|
||||||
|
contentHeight: bluetoothColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bluetoothColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: pairedRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
devices.sort((a, b) => {
|
||||||
|
if (a.connected && !b.connected) return -1
|
||||||
|
if (!a.connected && b.connected) return 1
|
||||||
|
return (b.signalStrength || 0) - (a.signalStrength || 0)
|
||||||
|
})
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Qt.rgba(Theme.warning.r, Theme.warning.g, Theme.warning.b, 0.12)
|
||||||
|
if (deviceMouseArea.containsMouse)
|
||||||
|
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08)
|
||||||
|
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
}
|
||||||
|
border.color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
border.width: (modelData.connected || modelData.state === BluetoothDeviceState.Connecting) ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
if (modelData.connected)
|
||||||
|
return Theme.primary
|
||||||
|
return Theme.surfaceText
|
||||||
|
}
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return "Connecting..."
|
||||||
|
if (modelData.connected)
|
||||||
|
return "Connected"
|
||||||
|
return "Paired"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: {
|
||||||
|
if (modelData.state === BluetoothDeviceState.Connecting)
|
||||||
|
return Theme.warning
|
||||||
|
return Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.batteryAvailable && modelData.battery > 0)
|
||||||
|
return "• " + Math.round(modelData.battery * 100) + "%"
|
||||||
|
|
||||||
|
var btBattery = BatteryService.bluetoothDevices.find(dev => {
|
||||||
|
return dev.name === (modelData.name || modelData.deviceName) ||
|
||||||
|
dev.name.toLowerCase().includes((modelData.name || modelData.deviceName).toLowerCase()) ||
|
||||||
|
(modelData.name || modelData.deviceName).toLowerCase().includes(dev.name.toLowerCase())
|
||||||
|
})
|
||||||
|
return btBattery ? "• " + btBattery.percentage + "%" : ""
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: pairedOptionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (bluetoothContextMenu.visible) {
|
||||||
|
bluetoothContextMenu.close()
|
||||||
|
} else {
|
||||||
|
bluetoothContextMenu.currentDevice = modelData
|
||||||
|
bluetoothContextMenu.popup(pairedOptionsButton, -bluetoothContextMenu.width + pairedOptionsButton.width, pairedOptionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: pairedOptionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (modelData.connected) {
|
||||||
|
modelData.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
visible: pairedRepeater.count > 0 && availableRepeater.count > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 80
|
||||||
|
visible: BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "sync"
|
||||||
|
size: 24
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.4)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: parent.visible && BluetoothService.adapter && BluetoothService.adapter.discovering && availableRepeater.count === 0
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1500
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
id: availableRepeater
|
||||||
|
model: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.discovering || !Bluetooth.devices)
|
||||||
|
return []
|
||||||
|
|
||||||
|
var filtered = Bluetooth.devices.values.filter(dev => {
|
||||||
|
return dev && !dev.paired && !dev.pairing && !dev.blocked &&
|
||||||
|
(dev.signalStrength === undefined || dev.signalStrength > 0)
|
||||||
|
})
|
||||||
|
return BluetoothService.sortDevices(filtered)
|
||||||
|
}
|
||||||
|
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property bool canConnect: BluetoothService.canConnect(modelData)
|
||||||
|
property bool isBusy: BluetoothService.isDeviceBusy(modelData)
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: availableMouseArea.containsMouse && !isBusy ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.15)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
opacity: canConnect ? 1 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: BluetoothService.getDeviceIcon(modelData)
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.name || modelData.deviceName || "Unknown Device"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (modelData.blocked) return "Blocked"
|
||||||
|
return BluetoothService.getSignalStrength(modelData)
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.signalStrength !== undefined && modelData.signalStrength > 0 ? "• " + modelData.signalStrength + "%" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0 && !modelData.pairing && !modelData.blocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
text: {
|
||||||
|
if (modelData.pairing) return "Pairing..."
|
||||||
|
if (!canConnect) return "Cannot pair"
|
||||||
|
return "Pair"
|
||||||
|
}
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: canConnect ? Theme.primary : Theme.surfaceVariantText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: availableMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: canConnect && !isBusy ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
enabled: canConnect && !isBusy
|
||||||
|
onClicked: {
|
||||||
|
if (modelData) {
|
||||||
|
BluetoothService.connectDeviceWithTrust(modelData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 60
|
||||||
|
visible: !BluetoothService.adapter
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "No Bluetooth adapter found"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: bluetoothContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property var currentDevice: null
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: bluetoothContextMenu.currentDevice && bluetoothContextMenu.currentDevice.connected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
if (bluetoothContextMenu.currentDevice.connected) {
|
||||||
|
bluetoothContextMenu.currentDevice.disconnect()
|
||||||
|
} else {
|
||||||
|
BluetoothService.connectDeviceWithTrust(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Audio Codec"
|
||||||
|
height: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected ? 32 : 0
|
||||||
|
visible: bluetoothContextMenu.currentDevice && BluetoothService.isAudioDevice(bluetoothContextMenu.currentDevice) && bluetoothContextMenu.currentDevice.connected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothCodecModalRef && bluetoothContextMenu.currentDevice) {
|
||||||
|
bluetoothCodecModalRef.show(bluetoothContextMenu.currentDevice)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Device"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (bluetoothContextMenu.currentDevice) {
|
||||||
|
bluetoothContextMenu.currentDevice.forget()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
BluetoothCodecSelector {
|
||||||
|
id: bluetoothCodecModal
|
||||||
|
anchors.fill: parent
|
||||||
|
z: 3000
|
||||||
|
}
|
||||||
|
}
|
||||||
420
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
420
Modules/ControlCenter/Details/NetworkDetail.qml
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modals
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
implicitHeight: NetworkService.wifiEnabled ? headerRow.height + wifiContent.height + Theme.spacingM : headerRow.height
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
NetworkService.addRef()
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
NetworkService.scanWifi()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onDestruction: {
|
||||||
|
NetworkService.removeRef()
|
||||||
|
}
|
||||||
|
|
||||||
|
property var wifiPasswordModalRef: {
|
||||||
|
wifiPasswordModalLoader.active = true
|
||||||
|
return wifiPasswordModalLoader.item
|
||||||
|
}
|
||||||
|
property var networkInfoModalRef: {
|
||||||
|
networkInfoModalLoader.active = true
|
||||||
|
return networkInfoModalLoader.item
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: headerRow
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingS
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
id: headerText
|
||||||
|
text: "Network Settings"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM)
|
||||||
|
height: parent.height
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: preferenceControls
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: NetworkService.ethernetConnected && NetworkService.wifiConnected
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: NetworkService.userPreference === "ethernet"
|
||||||
|
|
||||||
|
width: 90
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: isActive ? Theme.surfaceContainerHigh : ethernetMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Ethernet"
|
||||||
|
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: ethernetMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: NetworkService.setNetworkPreference("ethernet")
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
property bool isActive: NetworkService.userPreference === "wifi"
|
||||||
|
|
||||||
|
width: 70
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: isActive ? Theme.surfaceContainerHigh : wifiMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "WiFi"
|
||||||
|
color: parent.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
font.weight: parent.isActive ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: wifiMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
onClicked: NetworkService.setNetworkPreference("wifi")
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankFlickable {
|
||||||
|
id: wifiContent
|
||||||
|
anchors.top: headerRow.bottom
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
anchors.margins: Theme.spacingM
|
||||||
|
anchors.topMargin: Theme.spacingM
|
||||||
|
visible: NetworkService.wifiInterface
|
||||||
|
contentHeight: wifiColumn.height
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: wifiColumn
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width
|
||||||
|
height: 200
|
||||||
|
visible: NetworkService.wifiInterface && NetworkService.wifiNetworks?.length < 1
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "refresh"
|
||||||
|
size: 48
|
||||||
|
color: Qt.rgba(Theme.surfaceText.r || 0.8, Theme.surfaceText.g || 0.8, Theme.surfaceText.b || 0.8, 0.3)
|
||||||
|
|
||||||
|
RotationAnimation on rotation {
|
||||||
|
running: true
|
||||||
|
loops: Animation.Infinite
|
||||||
|
from: 0
|
||||||
|
to: 360
|
||||||
|
duration: 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: {
|
||||||
|
let networks = [...NetworkService.wifiNetworks]
|
||||||
|
networks.sort((a, b) => {
|
||||||
|
if (a.ssid === NetworkService.currentWifiSSID) return -1
|
||||||
|
if (b.ssid === NetworkService.currentWifiSSID) return 1
|
||||||
|
return b.signal - a.signal
|
||||||
|
})
|
||||||
|
return networks
|
||||||
|
}
|
||||||
|
delegate: Rectangle {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 50
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: networkMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, index % 2 === 0 ? 0.3 : 0.2)
|
||||||
|
border.color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
border.width: modelData.ssid === NetworkService.currentWifiSSID ? 2 : 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: {
|
||||||
|
let strength = modelData.signal || 0
|
||||||
|
if (strength >= 70) return "signal_wifi_4_bar"
|
||||||
|
if (strength >= 50) return "network_wifi_3_bar"
|
||||||
|
if (strength >= 25) return "network_wifi_2_bar"
|
||||||
|
if (strength >= 10) return "network_wifi_1_bar"
|
||||||
|
return "signal_wifi_bad"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize - 4
|
||||||
|
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 200
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid || "Unknown Network"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: modelData.ssid === NetworkService.currentWifiSSID ? Font.Medium : Font.Normal
|
||||||
|
elide: Text.ElideRight
|
||||||
|
width: parent.width
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: Theme.spacingXS
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.ssid === NetworkService.currentWifiSSID ? "Connected" : (modelData.secured ? "Secured" : "Open")
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: modelData.saved ? "• Saved" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.primary
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: "• " + modelData.signal + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankActionButton {
|
||||||
|
id: optionsButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
iconName: "more_horiz"
|
||||||
|
buttonSize: 28
|
||||||
|
onClicked: {
|
||||||
|
if (networkContextMenu.visible) {
|
||||||
|
networkContextMenu.close()
|
||||||
|
} else {
|
||||||
|
networkContextMenu.currentSSID = modelData.ssid
|
||||||
|
networkContextMenu.currentSecured = modelData.secured
|
||||||
|
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID
|
||||||
|
networkContextMenu.currentSaved = modelData.saved
|
||||||
|
networkContextMenu.currentSignal = modelData.signal
|
||||||
|
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: networkMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.rightMargin: optionsButton.width + Theme.spacingS
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (modelData.ssid !== NetworkService.currentWifiSSID) {
|
||||||
|
if (modelData.secured && !modelData.saved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(modelData.ssid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(modelData.ssid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: networkContextMenu
|
||||||
|
width: 150
|
||||||
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
|
||||||
|
|
||||||
|
property string currentSSID: ""
|
||||||
|
property bool currentSecured: false
|
||||||
|
property bool currentConnected: false
|
||||||
|
property bool currentSaved: false
|
||||||
|
property int currentSignal: 0
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: networkContextMenu.currentConnected ? "Disconnect" : "Connect"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkContextMenu.currentConnected) {
|
||||||
|
NetworkService.disconnectWifi()
|
||||||
|
} else {
|
||||||
|
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
|
||||||
|
if (wifiPasswordModalRef) {
|
||||||
|
wifiPasswordModalRef.show(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
NetworkService.connectToWifi(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Network Info"
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
if (networkInfoModalRef) {
|
||||||
|
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID)
|
||||||
|
networkInfoModalRef.showNetworkInfo(networkContextMenu.currentSSID, networkData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuItem {
|
||||||
|
text: "Forget Network"
|
||||||
|
height: networkContextMenu.currentSaved || networkContextMenu.currentConnected ? 32 : 0
|
||||||
|
visible: networkContextMenu.currentSaved || networkContextMenu.currentConnected
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.error
|
||||||
|
leftPadding: Theme.spacingS
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: wifiPasswordModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
WifiPasswordModal {
|
||||||
|
id: wifiPasswordModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyLoader {
|
||||||
|
id: networkInfoModalLoader
|
||||||
|
active: false
|
||||||
|
|
||||||
|
NetworkInfoModal {
|
||||||
|
id: networkInfoModal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Modules
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: displayTab
|
|
||||||
|
|
||||||
property var brightnessDebounceTimer
|
|
||||||
|
|
||||||
DankFlickable {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
contentHeight: mainColumn.height
|
|
||||||
contentWidth: width
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: mainColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingL
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: brightnessComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
Loader {
|
|
||||||
width: parent.width
|
|
||||||
sourceComponent: settingsComponent
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: brightnessComponent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
visible: DisplayService.brightnessAvailable
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Brightness"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
DankDropdown {
|
|
||||||
id: deviceDropdown
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 40
|
|
||||||
visible: DisplayService.devices.length > 1
|
|
||||||
text: "Device"
|
|
||||||
description: {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo();
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc")
|
|
||||||
return "DDC changes can be slow and unreliable";
|
|
||||||
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
currentValue: DisplayService.currentDevice
|
|
||||||
options: DisplayService.devices.map(function(d) {
|
|
||||||
return d.name;
|
|
||||||
})
|
|
||||||
optionIcons: DisplayService.devices.map(function(d) {
|
|
||||||
if (d.class === "backlight")
|
|
||||||
return "desktop_windows";
|
|
||||||
|
|
||||||
if (d.class === "ddc")
|
|
||||||
return "tv";
|
|
||||||
|
|
||||||
if (d.name.includes("kbd"))
|
|
||||||
return "keyboard";
|
|
||||||
|
|
||||||
return "lightbulb";
|
|
||||||
})
|
|
||||||
onValueChanged: function(value) {
|
|
||||||
DisplayService.setCurrentDevice(value, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onDevicesChanged() {
|
|
||||||
if (DisplayService.currentDevice)
|
|
||||||
deviceDropdown.currentValue = DisplayService.currentDevice;
|
|
||||||
|
|
||||||
// Check if saved device is now available
|
|
||||||
const lastDevice = SessionData.lastBrightnessDevice || "";
|
|
||||||
if (lastDevice) {
|
|
||||||
const deviceExists = DisplayService.devices.some((d) => {
|
|
||||||
return d.name === lastDevice;
|
|
||||||
});
|
|
||||||
if (deviceExists && (!DisplayService.currentDevice || DisplayService.currentDevice !== lastDevice))
|
|
||||||
DisplayService.setCurrentDevice(lastDevice, false);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
// Force update the description when device switches
|
|
||||||
deviceDropdown.description = Qt.binding(function() {
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo();
|
|
||||||
if (deviceInfo && deviceInfo.class === "ddc")
|
|
||||||
return "DDC changes can be slow and unreliable";
|
|
||||||
|
|
||||||
return "";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
target: DisplayService
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSlider {
|
|
||||||
id: brightnessSlider
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
value: DisplayService.brightnessLevel
|
|
||||||
leftIcon: "brightness_low"
|
|
||||||
rightIcon: "brightness_high"
|
|
||||||
enabled: DisplayService.brightnessAvailable && DisplayService.isCurrentDeviceReady()
|
|
||||||
opacity: DisplayService.isCurrentDeviceReady() ? 1 : 0.5
|
|
||||||
onSliderValueChanged: function(newValue) {
|
|
||||||
brightnessDebounceTimer.pendingValue = newValue;
|
|
||||||
brightnessDebounceTimer.restart();
|
|
||||||
}
|
|
||||||
onSliderDragFinished: function(finalValue) {
|
|
||||||
brightnessDebounceTimer.stop();
|
|
||||||
DisplayService.setBrightnessInternal(finalValue, DisplayService.currentDevice);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
function onBrightnessChanged() {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDeviceSwitched() {
|
|
||||||
brightnessSlider.value = DisplayService.brightnessLevel;
|
|
||||||
}
|
|
||||||
|
|
||||||
target: DisplayService
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Component {
|
|
||||||
id: settingsComponent
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Display Settings"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: DisplayService.nightModeActive ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (nightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: DisplayService.nightModeActive ? Theme.primary : "transparent"
|
|
||||||
border.width: DisplayService.nightModeActive ? 1 : 0
|
|
||||||
opacity: SessionData.nightModeAutoEnabled ? 0.6 : 1
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: DisplayService.nightModeActive ? "nightlight" : "dark_mode"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: DisplayService.nightModeActive ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: SessionData.nightModeAutoEnabled ? "Night Mode (Auto)" : "Night Mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: DisplayService.nightModeActive ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "schedule"
|
|
||||||
size: 16
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
visible: SessionData.nightModeAutoEnabled
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nightModeToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
DisplayService.toggleNightMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: SessionData.isLightMode ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : (lightModeToggle.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08))
|
|
||||||
border.color: SessionData.isLightMode ? Theme.primary : "transparent"
|
|
||||||
border.width: SessionData.isLightMode ? 1 : 0
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: SessionData.isLightMode ? "light_mode" : "palette"
|
|
||||||
size: Theme.iconSizeLarge
|
|
||||||
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: SessionData.isLightMode ? "Light Mode" : "Dark Mode"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: SessionData.isLightMode ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: lightModeToggle
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
Theme.toggleLightMode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
brightnessDebounceTimer: Timer {
|
|
||||||
property int pendingValue: 0
|
|
||||||
|
|
||||||
interval: {
|
|
||||||
// Use longer interval for DDC devices since ddcutil is slow
|
|
||||||
const deviceInfo = DisplayService.getCurrentDeviceInfo();
|
|
||||||
return (deviceInfo && deviceInfo.class === "ddc") ? 100 : 50;
|
|
||||||
}
|
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
|
||||||
DisplayService.setBrightnessInternal(pendingValue, DisplayService.currentDevice);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: ethernetCard
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (ethernetPreferenceArea.containsMouse
|
|
||||||
&& NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "ethernet")
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.8)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
}
|
|
||||||
border.color: NetworkService.networkStatus
|
|
||||||
=== "ethernet" ? Theme.primary : Qt.rgba(Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.12)
|
|
||||||
border.width: NetworkService.networkStatus === "ethernet" ? 2 : 1
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lan"
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: NetworkService.networkStatus === "ethernet" ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Ethernet"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: NetworkService.networkStatus
|
|
||||||
=== "ethernet" ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: NetworkService.ethernetConnected ? (NetworkService.ethernetIP
|
|
||||||
|| "Connected") : "Disconnected"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: ethernetLoadingSpinner
|
|
||||||
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NetworkService.changingPreference
|
|
||||||
&& NetworkService.targetPreference === "ethernet"
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: ethernetLoadingSpinner
|
|
||||||
property: "rotation"
|
|
||||||
running: ethernetLoadingSpinner.visible
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: ethernetPreferenceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus
|
|
||||||
!== "ethernet") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "ethernet"
|
|
||||||
&& !NetworkService.changingNetworkPreference
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled) {
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus !== "ethernet")
|
|
||||||
NetworkService.setNetworkPreference("ethernet")
|
|
||||||
else
|
|
||||||
NetworkService.setNetworkPreference("auto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiCard
|
|
||||||
|
|
||||||
property var refreshTimer
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
height: 80
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: {
|
|
||||||
if (wifiPreferenceArea.containsMouse && NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "wifi")
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.8)
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g,
|
|
||||||
Theme.surfaceContainer.b, 0.5)
|
|
||||||
}
|
|
||||||
border.color: NetworkService.networkStatus === "wifi" ? Theme.primary : Qt.rgba(
|
|
||||||
Theme.outline.r,
|
|
||||||
Theme.outline.g,
|
|
||||||
Theme.outline.b,
|
|
||||||
0.12)
|
|
||||||
border.width: NetworkService.networkStatus === "wifi" ? 2 : 1
|
|
||||||
visible: NetworkService.wifiAvailable
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
anchors.right: wifiToggle.left
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: NetworkService.wifiSignalIcon
|
|
||||||
size: Theme.iconSize
|
|
||||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!NetworkService.wifiEnabled)
|
|
||||||
return "WiFi is off"
|
|
||||||
else if (NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.currentWifiSSID)
|
|
||||||
return NetworkService.currentWifiSSID || "Connected"
|
|
||||||
else
|
|
||||||
return "Not Connected"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: NetworkService.networkStatus === "wifi" ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: {
|
|
||||||
if (!NetworkService.wifiEnabled)
|
|
||||||
return "Turn on WiFi to see networks"
|
|
||||||
else if (NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.currentWifiSSID)
|
|
||||||
return NetworkService.wifiIP || "Connected"
|
|
||||||
else
|
|
||||||
return "Select a network below"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: wifiLoadingSpinner
|
|
||||||
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 4
|
|
||||||
color: Theme.primary
|
|
||||||
anchors.right: wifiToggle.left
|
|
||||||
anchors.rightMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
visible: NetworkService.changingPreference
|
|
||||||
&& NetworkService.targetPreference === "wifi"
|
|
||||||
z: 10
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: wifiLoadingSpinner
|
|
||||||
property: "rotation"
|
|
||||||
running: wifiLoadingSpinner.visible
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DankToggle {
|
|
||||||
id: wifiToggle
|
|
||||||
|
|
||||||
checked: NetworkService.wifiEnabled
|
|
||||||
enabled: true
|
|
||||||
toggling: NetworkService.wifiToggling
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.rightMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.wifiEnabled) {
|
|
||||||
NetworkService.currentWifiSSID = ""
|
|
||||||
NetworkService.wifiSignalStrength = 100
|
|
||||||
NetworkService.wifiNetworks = []
|
|
||||||
NetworkService.savedWifiNetworks = []
|
|
||||||
NetworkService.connectionStatus = ""
|
|
||||||
NetworkService.connectingSSID = ""
|
|
||||||
NetworkService.isScanning = false
|
|
||||||
NetworkService.refreshNetworkStatus()
|
|
||||||
}
|
|
||||||
NetworkService.toggleWifiRadio()
|
|
||||||
if (refreshTimer)
|
|
||||||
refreshTimer.triggered = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wifiPreferenceArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 60 // Exclude toggle area
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus
|
|
||||||
!== "wifi") ? Qt.PointingHandCursor : Qt.ArrowCursor
|
|
||||||
enabled: NetworkService.ethernetConnected && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.networkStatus !== "wifi"
|
|
||||||
&& !NetworkService.changingNetworkPreference
|
|
||||||
onClicked: {
|
|
||||||
if (NetworkService.ethernetConnected
|
|
||||||
&& NetworkService.wifiEnabled) {
|
|
||||||
|
|
||||||
if (NetworkService.networkStatus !== "wifi")
|
|
||||||
NetworkService.setNetworkPreference("wifi")
|
|
||||||
else
|
|
||||||
NetworkService.setNetworkPreference("auto")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,300 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiContextMenuWindow
|
|
||||||
|
|
||||||
property var networkData: null
|
|
||||||
property bool menuVisible: false
|
|
||||||
property var parentItem
|
|
||||||
property var wifiPasswordModalRef
|
|
||||||
property var networkInfoModalRef
|
|
||||||
|
|
||||||
function show(x, y) {
|
|
||||||
const menuWidth = 160
|
|
||||||
wifiContextMenuWindow.visible = true
|
|
||||||
Qt.callLater(() => {
|
|
||||||
const menuHeight = wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
let finalX = x - menuWidth / 2
|
|
||||||
let finalY = y + 4
|
|
||||||
finalX = Math.max(
|
|
||||||
Theme.spacingS, Math.min(
|
|
||||||
finalX,
|
|
||||||
parentItem.width - menuWidth - Theme.spacingS))
|
|
||||||
finalY = Math.max(
|
|
||||||
Theme.spacingS, Math.min(
|
|
||||||
finalY,
|
|
||||||
parentItem.height - menuHeight - Theme.spacingS))
|
|
||||||
if (finalY + menuHeight > parentItem.height - Theme.spacingS) {
|
|
||||||
finalY = y - menuHeight - 4
|
|
||||||
finalY = Math.max(Theme.spacingS, finalY)
|
|
||||||
}
|
|
||||||
wifiContextMenuWindow.x = finalX
|
|
||||||
wifiContextMenuWindow.y = finalY
|
|
||||||
wifiContextMenuWindow.menuVisible = true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
wifiContextMenuWindow.menuVisible = false
|
|
||||||
Qt.callLater(() => {
|
|
||||||
wifiContextMenuWindow.visible = false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
visible: false
|
|
||||||
width: 160
|
|
||||||
height: wifiMenuColumn.implicitHeight + Theme.spacingS * 2
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: Theme.popupBackground()
|
|
||||||
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: 1000
|
|
||||||
opacity: menuVisible ? 1 : 0
|
|
||||||
scale: menuVisible ? 1 : 0.85
|
|
||||||
Component.onCompleted: {
|
|
||||||
menuVisible = false
|
|
||||||
visible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.topMargin: 4
|
|
||||||
anchors.leftMargin: 2
|
|
||||||
anchors.rightMargin: -2
|
|
||||||
anchors.bottomMargin: -4
|
|
||||||
radius: parent.radius
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
z: parent.z - 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: wifiMenuColumn
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingS
|
|
||||||
spacing: 1
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: connectWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: wifiContextMenuWindow.networkData
|
|
||||||
&& wifiContextMenuWindow.networkData.connected ? "wifi_off" : "wifi"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: wifiContextMenuWindow.networkData
|
|
||||||
&& wifiContextMenuWindow.networkData.connected ? "Disconnect" : "Connect"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: connectWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData) {
|
|
||||||
if (wifiContextMenuWindow.networkData.connected) {
|
|
||||||
NetworkService.disconnectWifi()
|
|
||||||
} else {
|
|
||||||
if (wifiContextMenuWindow.networkData.saved) {
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
} else if (wifiContextMenuWindow.networkData.secured) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width - Theme.spacingS * 2
|
|
||||||
height: 5
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
width: parent.width
|
|
||||||
height: 1
|
|
||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g,
|
|
||||||
Theme.outline.b, 0.2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: forgetWifiArea.containsMouse ? Qt.rgba(Theme.error.r,
|
|
||||||
Theme.error.g,
|
|
||||||
Theme.error.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
visible: wifiContextMenuWindow.networkData
|
|
||||||
&& (wifiContextMenuWindow.networkData.saved
|
|
||||||
|| wifiContextMenuWindow.networkData.connected)
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "delete"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Forget Network"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: forgetWifiArea.containsMouse ? Theme.error : Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: forgetWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData)
|
|
||||||
NetworkService.forgetWifiNetwork(
|
|
||||||
wifiContextMenuWindow.networkData.ssid)
|
|
||||||
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: infoWifiArea.containsMouse ? Qt.rgba(Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : "transparent"
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.leftMargin: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "info"
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.7
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
text: "Network Info"
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Normal
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: infoWifiArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (wifiContextMenuWindow.networkData
|
|
||||||
&& networkInfoModalRef)
|
|
||||||
networkInfoModalRef.showNetworkInfo(
|
|
||||||
wifiContextMenuWindow.networkData.ssid,
|
|
||||||
wifiContextMenuWindow.networkData)
|
|
||||||
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
easing.type: Theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: Theme.mediumDuration
|
|
||||||
easing.type: Theme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,322 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: root
|
|
||||||
|
|
||||||
property var wifiContextMenuWindow
|
|
||||||
property var sortedWifiNetworks
|
|
||||||
property var wifiPasswordModalRef
|
|
||||||
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 100
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
visible: NetworkService.wifiEnabled
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
// Compute icon name from a signal percentage (0-100)
|
|
||||||
function iconForSignal(pct) {
|
|
||||||
const s = Math.max(0, Math.min(100, pct | 0))
|
|
||||||
if (s >= 70) return "signal_wifi_4_bar"
|
|
||||||
if (s >= 50) return "network_wifi_3_bar"
|
|
||||||
if (s >= 25) return "network_wifi_2_bar"
|
|
||||||
if (s >= 10) return "network_wifi_1_bar"
|
|
||||||
return "signal_wifi_bad"
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
id: availableNetworksText
|
|
||||||
text: "Available Networks"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
width: parent.width - availableNetworksText.width - refreshButtonContainer.width - parent.spacing * 2
|
|
||||||
height: 1
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: refreshButtonContainer
|
|
||||||
width: 28
|
|
||||||
height: 28
|
|
||||||
radius: 14
|
|
||||||
color: refreshAreaSpan.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.12) : NetworkService.isScanning ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.06) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: refreshIconSpan
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
name: "refresh"
|
|
||||||
size: Theme.iconSize - 6
|
|
||||||
color: refreshAreaSpan.containsMouse ? Theme.primary : Theme.surfaceText
|
|
||||||
rotation: NetworkService.isScanning ? refreshIconSpan.rotation : 0
|
|
||||||
|
|
||||||
RotationAnimation {
|
|
||||||
target: refreshIconSpan
|
|
||||||
property: "rotation"
|
|
||||||
running: NetworkService.isScanning
|
|
||||||
from: 0
|
|
||||||
to: 360
|
|
||||||
duration: 1000
|
|
||||||
loops: Animation.Infinite
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on rotation {
|
|
||||||
RotationAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: refreshAreaSpan
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (!NetworkService.isScanning) {
|
|
||||||
refreshIconSpan.rotation += 30
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 40
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: spanningNetworksColumn.height
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: spanningNetworksColumn
|
|
||||||
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
Repeater {
|
|
||||||
model: NetworkService.wifiAvailable
|
|
||||||
&& NetworkService.wifiEnabled ? sortedWifiNetworks : []
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: spanningNetworksColumn.width
|
|
||||||
height: 38
|
|
||||||
radius: Theme.cornerRadius
|
|
||||||
color: networkArea2.containsMouse ? Qt.rgba(
|
|
||||||
Theme.primary.r,
|
|
||||||
Theme.primary.g,
|
|
||||||
Theme.primary.b,
|
|
||||||
0.08) : modelData.connected ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
|
||||||
border.color: modelData.connected ? Theme.primary : "transparent"
|
|
||||||
border.width: modelData.connected ? 1 : 0
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: Theme.spacingXS
|
|
||||||
anchors.rightMargin: Theme.spacingM // Extra right margin for scrollbar
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
id: signalIcon2
|
|
||||||
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
name: iconForSignal(modelData.signal)
|
|
||||||
size: Theme.iconSize - 2
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.left: signalIcon2.right
|
|
||||||
anchors.leftMargin: Theme.spacingXS
|
|
||||||
anchors.right: rightIcons2.left
|
|
||||||
anchors.rightMargin: Theme.spacingXS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: 2
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: modelData.ssid
|
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
|
||||||
color: modelData.connected ? Theme.primary : Theme.surfaceText
|
|
||||||
font.weight: modelData.connected ? Font.Medium : Font.Normal
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
width: parent.width
|
|
||||||
text: {
|
|
||||||
if (modelData.connected)
|
|
||||||
return "Connected"
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "connecting"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return "Connecting..."
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "invalid_password"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return "Invalid password"
|
|
||||||
|
|
||||||
if (modelData.saved)
|
|
||||||
return "Saved"
|
|
||||||
+ (modelData.secured ? " • Secured" : " • Open")
|
|
||||||
|
|
||||||
return modelData.secured ? "Secured" : "Open"
|
|
||||||
}
|
|
||||||
font.pixelSize: Theme.fontSizeSmall - 1
|
|
||||||
color: {
|
|
||||||
if (NetworkService.connectionStatus === "connecting"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return Theme.primary
|
|
||||||
|
|
||||||
if (NetworkService.connectionStatus === "invalid_password"
|
|
||||||
&& NetworkService.connectingSSID === modelData.ssid)
|
|
||||||
return Theme.error
|
|
||||||
|
|
||||||
return Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.7)
|
|
||||||
}
|
|
||||||
elide: Text.ElideRight
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
id: rightIcons2
|
|
||||||
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingXS
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "lock"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
visible: modelData.secured
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: wifiMenuButton
|
|
||||||
|
|
||||||
width: 24
|
|
||||||
height: 24
|
|
||||||
radius: 12
|
|
||||||
color: wifiMenuButtonArea.containsMouse ? Qt.rgba(
|
|
||||||
Theme.surfaceText.r,
|
|
||||||
Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b,
|
|
||||||
0.08) : "transparent"
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
name: "more_vert"
|
|
||||||
size: Theme.iconSize - 8
|
|
||||||
color: Theme.surfaceText
|
|
||||||
opacity: 0.6
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: wifiMenuButtonArea
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
wifiContextMenuWindow.networkData = modelData
|
|
||||||
let buttonCenter = wifiMenuButtonArea.width / 2
|
|
||||||
let buttonBottom = wifiMenuButtonArea.height
|
|
||||||
let globalPos = wifiMenuButtonArea.mapToItem(
|
|
||||||
wifiContextMenuWindow.parentItem,
|
|
||||||
buttonCenter, buttonBottom)
|
|
||||||
Qt.callLater(() => {
|
|
||||||
wifiContextMenuWindow.show(
|
|
||||||
globalPos.x,
|
|
||||||
globalPos.y)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on color {
|
|
||||||
ColorAnimation {
|
|
||||||
duration: Theme.shortDuration
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: networkArea2
|
|
||||||
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.rightMargin: 32 // Exclude menu button area
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: {
|
|
||||||
if (modelData.connected)
|
|
||||||
return
|
|
||||||
|
|
||||||
if (modelData.saved) {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid)
|
|
||||||
} else if (modelData.secured) {
|
|
||||||
if (wifiPasswordModalRef) {
|
|
||||||
wifiPasswordModalRef.show(modelData.ssid)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
NetworkService.connectToWifi(modelData.ssid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import qs.Common
|
|
||||||
import qs.Services
|
|
||||||
import qs.Widgets
|
|
||||||
import qs.Modules.ControlCenter.Network
|
|
||||||
|
|
||||||
Item {
|
|
||||||
id: networkTab
|
|
||||||
|
|
||||||
property var wifiPasswordModalRef: {
|
|
||||||
wifiPasswordModalLoader.active = true
|
|
||||||
return wifiPasswordModalLoader.item
|
|
||||||
}
|
|
||||||
property var networkInfoModalRef: {
|
|
||||||
networkInfoModalLoader.active = true
|
|
||||||
return networkInfoModalLoader.item
|
|
||||||
}
|
|
||||||
|
|
||||||
property var sortedWifiNetworks: {
|
|
||||||
if (!NetworkService.wifiAvailable || !NetworkService.wifiEnabled) {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
|
|
||||||
var allNetworks = NetworkService.wifiNetworks
|
|
||||||
var savedNetworks = NetworkService.savedWifiNetworks
|
|
||||||
var currentSSID = NetworkService.currentWifiSSID
|
|
||||||
var signalStrength = NetworkService.wifiSignalStrengthStr
|
|
||||||
var refreshTrigger = forceRefresh
|
|
||||||
|
|
||||||
// Force recalculation
|
|
||||||
var networks = [...allNetworks]
|
|
||||||
|
|
||||||
networks.forEach(function (network) {
|
|
||||||
network.connected = (network.ssid === currentSSID)
|
|
||||||
network.saved = savedNetworks.some(function (saved) {
|
|
||||||
return saved.ssid === network.ssid
|
|
||||||
})
|
|
||||||
if (network.connected && signalStrength) {
|
|
||||||
network.signalStrength = signalStrength
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
networks.sort(function (a, b) {
|
|
||||||
if (a.connected && !b.connected)
|
|
||||||
return -1
|
|
||||||
if (!a.connected && b.connected)
|
|
||||||
return 1
|
|
||||||
return b.signal - a.signal
|
|
||||||
})
|
|
||||||
|
|
||||||
return networks
|
|
||||||
}
|
|
||||||
|
|
||||||
property int forceRefresh: 0
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
function onNetworksUpdated() {
|
|
||||||
forceRefresh++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onCompleted: {
|
|
||||||
NetworkService.addRef()
|
|
||||||
if (NetworkService.wifiEnabled)
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
|
|
||||||
Component.onDestruction: {
|
|
||||||
NetworkService.removeRef()
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.fill: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 30
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: wifiContent.height
|
|
||||||
boundsBehavior: Flickable.DragAndOvershootBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: wifiContent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
WiFiCard {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: (parent.width - Theme.spacingM) / 2
|
|
||||||
height: parent.height
|
|
||||||
spacing: Theme.spacingS
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Flickable {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 30
|
|
||||||
clip: true
|
|
||||||
contentWidth: width
|
|
||||||
contentHeight: ethernetContent.height
|
|
||||||
boundsBehavior: Flickable.StopAtBounds
|
|
||||||
// Qt 6.9+ scrolling: flickDeceleration/maximumFlickVelocity only affect touch now
|
|
||||||
flickDeceleration: 1500
|
|
||||||
maximumFlickVelocity: 2000
|
|
||||||
|
|
||||||
// Custom wheel handler for Qt 6.9+ responsive mouse wheel scrolling
|
|
||||||
WheelHandler {
|
|
||||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
|
||||||
onWheel: event => {
|
|
||||||
let delta = event.pixelDelta.y
|
|
||||||
!== 0 ? event.pixelDelta.y * 1.8 : event.angleDelta.y / 120 * 60
|
|
||||||
let newY = parent.contentY - delta
|
|
||||||
newY = Math.max(
|
|
||||||
0, Math.min(
|
|
||||||
parent.contentHeight - parent.height,
|
|
||||||
newY))
|
|
||||||
parent.contentY = newY
|
|
||||||
event.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
id: ethernetContent
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
EthernetCard {}
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {
|
|
||||||
policy: ScrollBar.AsNeeded
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.topMargin: 100
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
color: "transparent"
|
|
||||||
visible: !NetworkService.wifiEnabled
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
DankIcon {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
name: "wifi_off"
|
|
||||||
size: 48
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: "WiFi is turned off"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.6)
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
StyledText {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
text: "Turn on WiFi to see networks"
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g,
|
|
||||||
Theme.surfaceText.b, 0.4)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiNetworksList {
|
|
||||||
wifiContextMenuWindow: wifiContextMenuWindow
|
|
||||||
sortedWifiNetworks: networkTab.sortedWifiNetworks
|
|
||||||
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: NetworkService
|
|
||||||
function onWifiEnabledChanged() {
|
|
||||||
if (NetworkService.wifiEnabled && visible) {
|
|
||||||
// Trigger a scan when WiFi is enabled
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onVisibleChanged: {
|
|
||||||
if (visible && NetworkService.wifiEnabled
|
|
||||||
&& NetworkService.wifiNetworks.length === 0) {
|
|
||||||
// Scan when tab becomes visible if we don't have networks cached
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
WiFiContextMenu {
|
|
||||||
id: wifiContextMenuWindow
|
|
||||||
parentItem: networkTab
|
|
||||||
wifiPasswordModalRef: networkTab.wifiPasswordModalRef
|
|
||||||
networkInfoModalRef: networkTab.networkInfoModalRef
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: wifiContextMenuWindow.visible
|
|
||||||
propagateComposedEvents: true
|
|
||||||
onClicked: function(mouse) {
|
|
||||||
wifiContextMenuWindow.hide()
|
|
||||||
mouse.accepted = false
|
|
||||||
}
|
|
||||||
onWheel: function(wheel) {
|
|
||||||
wheel.accepted = false
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
x: wifiContextMenuWindow.x
|
|
||||||
y: wifiContextMenuWindow.y
|
|
||||||
width: wifiContextMenuWindow.width
|
|
||||||
height: wifiContextMenuWindow.height
|
|
||||||
onClicked: function(mouse) {
|
|
||||||
mouse.accepted = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
57
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
57
Modules/ControlCenter/Widgets/AudioInputPill.qml
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSource: AudioService.source
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSource) return "mic_off"
|
||||||
|
|
||||||
|
let volume = defaultSource.audio.volume
|
||||||
|
let muted = defaultSource.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "mic_off"
|
||||||
|
return "mic"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSource && !defaultSource.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "No input device"
|
||||||
|
}
|
||||||
|
return defaultSource.description || "Audio Input"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSource) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSource.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSource.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSource || !defaultSource.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSource.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSource.audio.muted = false
|
||||||
|
defaultSource.audio.volume = newVolume / 100
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
60
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
60
Modules/ControlCenter/Widgets/AudioOutputPill.qml
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: defaultSink && !defaultSink.audio.muted
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "No output device"
|
||||||
|
}
|
||||||
|
return defaultSink.description || "Audio Output"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!defaultSink) {
|
||||||
|
return "Select device"
|
||||||
|
}
|
||||||
|
if (defaultSink.audio.muted) {
|
||||||
|
return "Muted"
|
||||||
|
}
|
||||||
|
return Math.round(defaultSink.audio.volume * 100) + "%"
|
||||||
|
}
|
||||||
|
|
||||||
|
onWheelEvent: function (wheelEvent) {
|
||||||
|
if (!defaultSink || !defaultSink.audio) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentVolume = defaultSink.audio.volume * 100
|
||||||
|
let newVolume
|
||||||
|
if (delta > 0)
|
||||||
|
newVolume = Math.min(100, currentVolume + 5)
|
||||||
|
else
|
||||||
|
newVolume = Math.max(0, currentVolume - 5)
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
defaultSink.audio.volume = newVolume / 100
|
||||||
|
AudioService.volumeChanged()
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Modules/ControlCenter/Widgets/AudioSlider.qml
Normal file
50
Modules/ControlCenter/Widgets/AudioSlider.qml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
SimpleSlider {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
allowIconClick: defaultSink !== null
|
||||||
|
|
||||||
|
value: defaultSink ? defaultSink.audio.volume : 0.0
|
||||||
|
maximumValue: 1.0
|
||||||
|
minimumValue: 0.0
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onIconClicked: function() {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
78
Modules/ControlCenter/Widgets/AudioSliderRow.qml
Normal file
78
Modules/ControlCenter/Widgets/AudioSliderRow.qml
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Pipewire
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var defaultSink: AudioService.sink
|
||||||
|
|
||||||
|
height: 60
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2 // Make it circular
|
||||||
|
color: iconArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: defaultSink !== null
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.muted = !defaultSink.audio.muted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!defaultSink) return "volume_off"
|
||||||
|
|
||||||
|
let volume = defaultSink.audio.volume
|
||||||
|
let muted = defaultSink.audio.muted
|
||||||
|
|
||||||
|
if (muted || volume === 0.0) return "volume_off"
|
||||||
|
if (volume <= 0.33) return "volume_down"
|
||||||
|
if (volume <= 0.66) return "volume_up"
|
||||||
|
return "volume_up"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: defaultSink && !defaultSink.audio.muted && defaultSink.audio.volume > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: {
|
||||||
|
if (parent.width <= 0) return 80
|
||||||
|
return Math.max(80, Math.min(400, parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM))
|
||||||
|
}
|
||||||
|
enabled: defaultSink !== null
|
||||||
|
minimum: 0
|
||||||
|
maximum: 100
|
||||||
|
value: defaultSink ? Math.round(defaultSink.audio.volume * 100) : 0
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (defaultSink) {
|
||||||
|
defaultSink.audio.volume = newValue / 100.0
|
||||||
|
if (newValue > 0 && defaultSink.audio.muted) {
|
||||||
|
defaultSink.audio.muted = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
149
Modules/ControlCenter/Widgets/BasePill.qml
Normal file
149
Modules/ControlCenter/Widgets/BasePill.qml
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string primaryText: ""
|
||||||
|
property string secondaryText: ""
|
||||||
|
property bool expanded: false
|
||||||
|
property bool isActive: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
signal expandClicked()
|
||||||
|
signal wheelEvent(var wheelEvent)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: mainArea
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width - expandArea.width
|
||||||
|
topLeftRadius: Theme.cornerRadius
|
||||||
|
bottomLeftRadius: Theme.cornerRadius
|
||||||
|
topRightRadius: 0
|
||||||
|
bottomRightRadius: 0
|
||||||
|
color: mainAreaMouse.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.isActive ? Theme.primary : root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.primaryText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mainAreaMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
onWheel: function (wheelEvent) {
|
||||||
|
root.wheelEvent(wheelEvent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: expandArea
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: Theme.iconSize + Theme.spacingM * 2
|
||||||
|
topLeftRadius: 0
|
||||||
|
bottomLeftRadius: 0
|
||||||
|
topRightRadius: Theme.cornerRadius
|
||||||
|
bottomRightRadius: Theme.cornerRadius
|
||||||
|
color: expandAreaMouse.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
width: 1
|
||||||
|
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
id: expandIcon
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: expanded ? "expand_less" : "expand_more"
|
||||||
|
size: Theme.iconSize - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: expandAreaMouse
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.expandClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
66
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
66
Modules/ControlCenter/Widgets/BluetoothPill.qml
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property var primaryDevice: {
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.devices) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
let devices = [...BluetoothService.adapter.devices.values.filter(dev => dev && (dev.paired || dev.trusted))]
|
||||||
|
for (let device of devices) {
|
||||||
|
if (device && device.connected) {
|
||||||
|
return device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "bluetooth_disabled"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return BluetoothService.getDeviceIcon(primaryDevice)
|
||||||
|
}
|
||||||
|
return "bluetooth"
|
||||||
|
}
|
||||||
|
|
||||||
|
isActive: !!(BluetoothService.available && BluetoothService.adapter && BluetoothService.adapter.enabled)
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Bluetooth unavailable"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter) {
|
||||||
|
return "No adapter"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter.enabled) {
|
||||||
|
return "Disabled"
|
||||||
|
}
|
||||||
|
return "Enabled"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (!BluetoothService.available) {
|
||||||
|
return "Hardware not found"
|
||||||
|
}
|
||||||
|
if (!BluetoothService.adapter || !BluetoothService.adapter.enabled) {
|
||||||
|
return "Off"
|
||||||
|
}
|
||||||
|
if (primaryDevice) {
|
||||||
|
return primaryDevice.name || primaryDevice.alias || primaryDevice.deviceName || "Connected Device"
|
||||||
|
}
|
||||||
|
return "No devices"
|
||||||
|
}
|
||||||
|
}
|
||||||
34
Modules/ControlCenter/Widgets/BrightnessSlider.qml
Normal file
34
Modules/ControlCenter/Widgets/BrightnessSlider.qml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
SimpleSlider {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
|
||||||
|
iconColor: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
|
||||||
|
value: DisplayService.brightnessLevel
|
||||||
|
maximumValue: 100.0
|
||||||
|
minimumValue: 0.0
|
||||||
|
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.brightnessLevel = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
170
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
170
Modules/ControlCenter/Widgets/BrightnessSliderRow.qml
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
height: 60
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
height: Theme.iconSize + Theme.spacingS * 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
radius: (Theme.iconSize + Theme.spacingS * 2) / 2
|
||||||
|
color: iconArea.containsMouse && DisplayService.devices.length > 1
|
||||||
|
? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12)
|
||||||
|
: "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: iconArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: DisplayService.devices.length > 1
|
||||||
|
cursorShape: DisplayService.devices.length > 1 ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
|
|
||||||
|
onClicked: function(event) {
|
||||||
|
if (DisplayService.devices.length > 1) {
|
||||||
|
if (deviceMenu.visible) {
|
||||||
|
deviceMenu.close()
|
||||||
|
} else {
|
||||||
|
deviceMenu.popup(iconArea, 0, iconArea.height + Theme.spacingXS)
|
||||||
|
}
|
||||||
|
event.accepted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: {
|
||||||
|
if (!DisplayService.brightnessAvailable) return "brightness_low"
|
||||||
|
|
||||||
|
let brightness = DisplayService.brightnessLevel
|
||||||
|
if (brightness <= 33) return "brightness_low"
|
||||||
|
if (brightness <= 66) return "brightness_medium"
|
||||||
|
return "brightness_high"
|
||||||
|
}
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: DisplayService.brightnessAvailable && DisplayService.brightnessLevel > 0 ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - (Theme.iconSize + Theme.spacingS * 2) - Theme.spacingM
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
width: parent.width
|
||||||
|
enabled: DisplayService.brightnessAvailable
|
||||||
|
minimum: 1
|
||||||
|
maximum: 100
|
||||||
|
value: {
|
||||||
|
let level = DisplayService.brightnessLevel
|
||||||
|
if (level > 100) {
|
||||||
|
let deviceInfo = DisplayService.getCurrentDeviceInfo()
|
||||||
|
if (deviceInfo && deviceInfo.max > 0) {
|
||||||
|
return Math.round((level / deviceInfo.max) * 100)
|
||||||
|
}
|
||||||
|
return 50
|
||||||
|
}
|
||||||
|
return level
|
||||||
|
}
|
||||||
|
onSliderValueChanged: function(newValue) {
|
||||||
|
if (DisplayService.brightnessAvailable) {
|
||||||
|
DisplayService.setBrightness(newValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
visible: {
|
||||||
|
if (DisplayService.devices.length <= 1) return false
|
||||||
|
if (!DisplayService.currentDevice) return false
|
||||||
|
|
||||||
|
let currentIndex = -1
|
||||||
|
for (let i = 0; i < DisplayService.devices.length; i++) {
|
||||||
|
if (DisplayService.devices[i].name === DisplayService.currentDevice) {
|
||||||
|
currentIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return currentIndex !== 0
|
||||||
|
}
|
||||||
|
width: parent.width
|
||||||
|
text: DisplayService.currentDevice || ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall - 2
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignLeft
|
||||||
|
topPadding: 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Menu {
|
||||||
|
id: deviceMenu
|
||||||
|
width: 200
|
||||||
|
closePolicy: Popup.CloseOnEscape
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: Theme.popupBackground()
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
border.width: 1
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: DisplayService.devices
|
||||||
|
delegate: MenuItem {
|
||||||
|
required property var modelData
|
||||||
|
required property int index
|
||||||
|
|
||||||
|
property string deviceName: modelData.name || ""
|
||||||
|
property string deviceClass: modelData.class || ""
|
||||||
|
|
||||||
|
text: deviceName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
indicator: Rectangle {
|
||||||
|
visible: DisplayService.currentDevice === parent.deviceName
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingS
|
||||||
|
width: 4
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
radius: 2
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
contentItem: StyledText {
|
||||||
|
text: parent.text
|
||||||
|
font: parent.font
|
||||||
|
color: DisplayService.currentDevice === parent.deviceName ? Theme.primary : Theme.surfaceText
|
||||||
|
leftPadding: Theme.spacingL
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
background: Rectangle {
|
||||||
|
color: parent.hovered ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) : "transparent"
|
||||||
|
radius: Theme.cornerRadius / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
onTriggered: {
|
||||||
|
DisplayService.setCurrentDevice(deviceName, true)
|
||||||
|
deviceMenu.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onObjectAdded: (index, object) => deviceMenu.insertItem(index, object)
|
||||||
|
onObjectRemoved: (index, object) => deviceMenu.removeItem(object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
70
Modules/ControlCenter/Widgets/CompactSlider.qml
Normal file
70
Modules/ControlCenter/Widgets/CompactSlider.qml
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property string labelText: ""
|
||||||
|
property real value: 0.0
|
||||||
|
property real maximumValue: 1.0
|
||||||
|
property real minimumValue: 0.0
|
||||||
|
property bool enabled: true
|
||||||
|
|
||||||
|
signal sliderValueChanged(real value)
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.right: sliderContainer.left
|
||||||
|
anchors.rightMargin: Theme.spacingS
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
text: root.labelText
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: sliderContainer
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
width: 120
|
||||||
|
height: parent.height - Theme.spacingS * 2
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: parent.width
|
||||||
|
enabled: root.enabled
|
||||||
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
|
value: Math.round(root.value * 100)
|
||||||
|
onSliderValueChanged: root.sliderValueChanged(newValue / 100.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
Modules/ControlCenter/Widgets/DetailView.qml
Normal file
29
Modules/ControlCenter/Widgets/DetailView.qml
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string title: ""
|
||||||
|
property Component content: null
|
||||||
|
property bool isVisible: true
|
||||||
|
property int contentHeight: 300
|
||||||
|
|
||||||
|
width: parent ? parent.width : 400
|
||||||
|
implicitHeight: isVisible ? contentHeight : 0
|
||||||
|
height: implicitHeight
|
||||||
|
color: "transparent"
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Loader {
|
||||||
|
id: contentLoader
|
||||||
|
anchors.fill: parent
|
||||||
|
sourceComponent: root.content
|
||||||
|
asynchronous: true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
52
Modules/ControlCenter/Widgets/NetworkPill.qml
Normal file
52
Modules/ControlCenter/Widgets/NetworkPill.qml
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
import qs.Widgets
|
||||||
|
import qs.Modules.ControlCenter.Widgets
|
||||||
|
|
||||||
|
BasePill {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
isActive: NetworkService.networkStatus !== "disconnected"
|
||||||
|
|
||||||
|
iconName: {
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "settings_ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalIcon
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "signal_wifi_off"
|
||||||
|
}
|
||||||
|
return "wifi_off"
|
||||||
|
}
|
||||||
|
|
||||||
|
primaryText: {
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Ethernet"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi" && NetworkService.currentWifiSSID) {
|
||||||
|
return NetworkService.currentWifiSSID
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Not connected"
|
||||||
|
}
|
||||||
|
return "WiFi off"
|
||||||
|
}
|
||||||
|
|
||||||
|
secondaryText: {
|
||||||
|
if (NetworkService.networkStatus === "ethernet") {
|
||||||
|
return "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.networkStatus === "wifi") {
|
||||||
|
return NetworkService.wifiSignalStrength > 0 ? NetworkService.wifiSignalStrength + "%" : "Connected"
|
||||||
|
}
|
||||||
|
if (NetworkService.wifiEnabled) {
|
||||||
|
return "Select network"
|
||||||
|
}
|
||||||
|
return "Tap to enable"
|
||||||
|
}
|
||||||
|
}
|
||||||
50
Modules/ControlCenter/Widgets/SimpleSlider.qml
Normal file
50
Modules/ControlCenter/Widgets/SimpleSlider.qml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property color iconColor: Theme.surfaceText
|
||||||
|
property real value: 0.0
|
||||||
|
property real maximumValue: 1.0
|
||||||
|
property real minimumValue: 0.0
|
||||||
|
property bool enabled: true
|
||||||
|
property bool allowIconClick: false
|
||||||
|
|
||||||
|
signal sliderValueChanged(real value)
|
||||||
|
signal iconClicked()
|
||||||
|
|
||||||
|
height: 60
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.iconColor
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: root.allowIconClick
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.iconClicked()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DankSlider {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: {
|
||||||
|
if (parent.width <= 0) return 80
|
||||||
|
return Math.max(80, Math.min(400, parent.width - Theme.iconSize - Theme.spacingM))
|
||||||
|
}
|
||||||
|
enabled: root.enabled
|
||||||
|
minimum: Math.round(root.minimumValue * 100)
|
||||||
|
maximum: Math.round(root.maximumValue * 100)
|
||||||
|
value: Math.round(root.value * 100)
|
||||||
|
onSliderValueChanged: function(newValue) { root.sliderValueChanged(newValue / 100.0) }
|
||||||
|
}
|
||||||
|
}
|
||||||
95
Modules/ControlCenter/Widgets/ToggleButton.qml
Normal file
95
Modules/ControlCenter/Widgets/ToggleButton.qml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Quickshell
|
||||||
|
import qs.Common
|
||||||
|
import qs.Widgets
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string iconName: ""
|
||||||
|
property string text: ""
|
||||||
|
property bool isActive: false
|
||||||
|
property bool enabled: true
|
||||||
|
property string secondaryText: ""
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
width: parent ? parent.width : 200
|
||||||
|
height: 60
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, Theme.getContentBackgroundAlpha() * 0.6)
|
||||||
|
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.08)
|
||||||
|
border.width: 1
|
||||||
|
opacity: enabled ? 1.0 : 0.6
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.cornerRadius
|
||||||
|
color: mouseArea.containsMouse ?
|
||||||
|
Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
"transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation { duration: Theme.shortDuration }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.rightMargin: Theme.spacingM
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
name: root.iconName
|
||||||
|
size: Theme.iconSize
|
||||||
|
color: root.isActive ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - Theme.iconSize - Theme.spacingS
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.text
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
|
||||||
|
StyledText {
|
||||||
|
width: parent.width
|
||||||
|
text: root.secondaryText
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Theme.surfaceVariantText
|
||||||
|
visible: text.length > 0
|
||||||
|
elide: Text.ElideRight
|
||||||
|
wrapMode: Text.NoWrap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: mouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
enabled: root.enabled
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.shortDuration
|
||||||
|
easing.type: Theme.standardEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,6 @@ Rectangle {
|
|||||||
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
readonly property real horizontalPadding: SettingsData.topBarNoBackground ? 0 : Math.max(Theme.spacingXS, Theme.spacingS * (widgetHeight / 30))
|
||||||
|
|
||||||
signal clicked
|
signal clicked
|
||||||
signal iconClicked(string tab)
|
|
||||||
|
|
||||||
width: controlIndicators.implicitWidth + horizontalPadding * 2
|
width: controlIndicators.implicitWidth + horizontalPadding * 2
|
||||||
height: widgetHeight
|
height: widgetHeight
|
||||||
@@ -150,46 +149,8 @@ Rectangle {
|
|||||||
relativeX, barHeight + Theme.spacingXS,
|
relativeX, barHeight + Theme.spacingXS,
|
||||||
width, section, currentScreen)
|
width, section, currentScreen)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate which zone was clicked based on mouse position relative to controlIndicators
|
|
||||||
var indicatorsX = controlIndicators.x
|
|
||||||
var relativeX = mouseX - indicatorsX
|
|
||||||
|
|
||||||
var iconSpacing = Theme.spacingXS
|
root.clicked()
|
||||||
var iconSize = Theme.iconSize - 8
|
|
||||||
var networkWidth = networkIcon.visible ? iconSize : 0
|
|
||||||
var bluetoothWidth = bluetoothIcon.visible ? iconSize : 0
|
|
||||||
var audioWidth = audioIcon.parent.visible ? (iconSize + 4) : 0
|
|
||||||
|
|
||||||
var currentX = 0
|
|
||||||
var clickedZone = ""
|
|
||||||
|
|
||||||
// Network zone
|
|
||||||
if (networkIcon.visible && relativeX >= currentX && relativeX < currentX + networkWidth) {
|
|
||||||
clickedZone = "network"
|
|
||||||
}
|
|
||||||
if (networkIcon.visible) {
|
|
||||||
currentX += networkWidth + iconSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bluetooth zone
|
|
||||||
if (bluetoothIcon.visible && relativeX >= currentX && relativeX < currentX + bluetoothWidth) {
|
|
||||||
clickedZone = "bluetooth"
|
|
||||||
}
|
|
||||||
if (bluetoothIcon.visible) {
|
|
||||||
currentX += bluetoothWidth + iconSpacing
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audio zone
|
|
||||||
if (audioIcon.parent.visible && relativeX >= currentX && relativeX < currentX + audioWidth) {
|
|
||||||
clickedZone = "audio"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (clickedZone !== "") {
|
|
||||||
root.iconClicked(clickedZone)
|
|
||||||
} else {
|
|
||||||
root.clicked()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1051,15 +1051,6 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onIconClicked: (tab) => {
|
|
||||||
controlCenterLoader.active = true
|
|
||||||
if (controlCenterLoader.item) {
|
|
||||||
controlCenterLoader.item.triggerScreen = root.screen
|
|
||||||
controlCenterLoader.item.openWithTab(tab)
|
|
||||||
if (NetworkService.wifiEnabled)
|
|
||||||
NetworkService.scanWifi()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ Singleton {
|
|||||||
property int wifiSignalStrength: 0
|
property int wifiSignalStrength: 0
|
||||||
property var wifiNetworks: []
|
property var wifiNetworks: []
|
||||||
property var savedConnections: []
|
property var savedConnections: []
|
||||||
|
property var ssidToConnectionName: {}
|
||||||
property var wifiSignalIcon: {
|
property var wifiSignalIcon: {
|
||||||
if (!wifiConnected || networkStatus !== "wifi") {
|
if (!wifiConnected || networkStatus !== "wifi") {
|
||||||
return "signal_wifi_off"
|
return "signal_wifi_off"
|
||||||
@@ -611,26 +612,33 @@ Singleton {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: getSavedConnections
|
id: getSavedConnections
|
||||||
command: ["nmcli", "-t", "-f", "NAME,TYPE", "connection", "show"]
|
command: ["bash", "-c", "nmcli -t -f NAME,TYPE connection show | grep ':802-11-wireless$' | cut -d: -f1 | while read name; do ssid=$(nmcli -g 802-11-wireless.ssid connection show \"$name\"); echo \"$ssid:$name\"; done"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
let saved = []
|
let saved = []
|
||||||
|
let mapping = {}
|
||||||
const lines = text.trim().split('\n')
|
const lines = text.trim().split('\n')
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const parts = line.split(':')
|
const parts = line.trim().split(':')
|
||||||
if (parts.length >= 2 && parts[1] === "802-11-wireless") {
|
if (parts.length >= 2) {
|
||||||
saved.push({
|
const ssid = parts[0]
|
||||||
"ssid": parts[0],
|
const connectionName = parts[1]
|
||||||
"saved": true
|
if (ssid && ssid.length > 0 && connectionName && connectionName.length > 0) {
|
||||||
})
|
saved.push({
|
||||||
|
"ssid": ssid,
|
||||||
|
"saved": true
|
||||||
|
})
|
||||||
|
mapping[ssid] = connectionName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.savedConnections = saved
|
root.savedConnections = saved
|
||||||
root.savedWifiNetworks = saved
|
root.savedWifiNetworks = saved
|
||||||
|
root.ssidToConnectionName = mapping
|
||||||
|
|
||||||
let updated = [...root.wifiNetworks]
|
let updated = [...root.wifiNetworks]
|
||||||
for (let network of updated) {
|
for (let network of updated) {
|
||||||
@@ -650,7 +658,11 @@ Singleton {
|
|||||||
root.connectionError = ""
|
root.connectionError = ""
|
||||||
root.connectionStatus = "connecting"
|
root.connectionStatus = "connecting"
|
||||||
|
|
||||||
if (password) {
|
// For saved networks without password, try connection up first
|
||||||
|
if (!password && root.ssidToConnectionName[ssid]) {
|
||||||
|
const connectionName = root.ssidToConnectionName[ssid]
|
||||||
|
wifiConnector.command = ["nmcli", "connection", "up", connectionName]
|
||||||
|
} else if (password) {
|
||||||
wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid, "password", password]
|
wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid, "password", password]
|
||||||
} else {
|
} else {
|
||||||
wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid]
|
wifiConnector.command = ["nmcli", "dev", "wifi", "connect", ssid]
|
||||||
@@ -751,7 +763,8 @@ Singleton {
|
|||||||
|
|
||||||
function forgetWifiNetwork(ssid) {
|
function forgetWifiNetwork(ssid) {
|
||||||
root.forgetSSID = ssid
|
root.forgetSSID = ssid
|
||||||
networkForgetter.command = ["nmcli", "connection", "delete", ssid]
|
const connectionName = root.ssidToConnectionName[ssid] || ssid
|
||||||
|
networkForgetter.command = ["nmcli", "connection", "delete", connectionName]
|
||||||
networkForgetter.running = true
|
networkForgetter.running = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,24 @@ Item {
|
|||||||
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
cursorShape: slider.enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||||
enabled: slider.enabled
|
enabled: slider.enabled
|
||||||
preventStealing: true
|
preventStealing: true
|
||||||
|
acceptedButtons: Qt.LeftButton
|
||||||
|
onWheel: function (wheelEvent) {
|
||||||
|
if (!slider.enabled) return
|
||||||
|
let delta = wheelEvent.angleDelta.y
|
||||||
|
let currentValue = slider.value
|
||||||
|
let step = Math.max(1, (slider.maximum - slider.minimum) / 20)
|
||||||
|
let newValue
|
||||||
|
if (delta > 0)
|
||||||
|
newValue = Math.min(slider.maximum, currentValue + step)
|
||||||
|
else
|
||||||
|
newValue = Math.max(slider.minimum, currentValue - step)
|
||||||
|
newValue = Math.round(newValue)
|
||||||
|
if (newValue !== slider.value) {
|
||||||
|
slider.value = newValue
|
||||||
|
slider.sliderValueChanged(newValue)
|
||||||
|
}
|
||||||
|
wheelEvent.accepted = true
|
||||||
|
}
|
||||||
onPressed: mouse => {
|
onPressed: mouse => {
|
||||||
if (slider.enabled) {
|
if (slider.enabled) {
|
||||||
slider.isDragging = true
|
slider.isDragging = true
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import qs.Modules
|
|||||||
import qs.Modules.AppDrawer
|
import qs.Modules.AppDrawer
|
||||||
import qs.Modules.CentcomCenter
|
import qs.Modules.CentcomCenter
|
||||||
import qs.Modules.ControlCenter
|
import qs.Modules.ControlCenter
|
||||||
import qs.Modules.ControlCenter.Network
|
|
||||||
import qs.Modules.Dock
|
import qs.Modules.Dock
|
||||||
import qs.Modules.Lock
|
import qs.Modules.Lock
|
||||||
import qs.Modules.Notifications.Center
|
import qs.Modules.Notifications.Center
|
||||||
|
|||||||
Reference in New Issue
Block a user