feat: added widgets capture, music visualiser, bluetooth, notes
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: control
|
||||
|
||||
property var daemon: null
|
||||
property string screenName: ""
|
||||
readonly property string deviceId: daemon ? daemon.brightnessDeviceForScreen(screenName) : ""
|
||||
readonly property var device: DisplayService.devices ? (DisplayService.devices.find(d => d.id === control.deviceId) || null) : null
|
||||
readonly property bool usable: DisplayService.brightnessAvailable && control.deviceId !== ""
|
||||
|
||||
readonly property int currentBrightness: {
|
||||
DisplayService.brightnessVersion;
|
||||
return control.deviceId !== "" ? DisplayService.getDeviceBrightness(control.deviceId) : 0;
|
||||
}
|
||||
|
||||
readonly property int sliderMinimum: {
|
||||
if (!control.device)
|
||||
return 1;
|
||||
|
||||
const exponential = SessionData.getBrightnessExponential(control.device.id);
|
||||
if (exponential)
|
||||
return 1;
|
||||
|
||||
return (control.device.class === "backlight" || control.device.class === "ddc") ? 1 : 0;
|
||||
}
|
||||
|
||||
readonly property int sliderMaximum: {
|
||||
if (!control.device)
|
||||
return 100;
|
||||
|
||||
const exponential = SessionData.getBrightnessExponential(control.device.id);
|
||||
if (exponential)
|
||||
return 100;
|
||||
|
||||
return control.device.displayMax || 100;
|
||||
}
|
||||
|
||||
implicitWidth: 190
|
||||
height: 38
|
||||
|
||||
Component.onCompleted: {
|
||||
if (daemon)
|
||||
daemon.ensureBrightnessMap();
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: icon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: {
|
||||
if (!control.usable)
|
||||
return "brightness_low";
|
||||
if (control.currentBrightness <= 33)
|
||||
return "brightness_low";
|
||||
if (control.currentBrightness <= 66)
|
||||
return "brightness_medium";
|
||||
return "brightness_high";
|
||||
}
|
||||
size: Theme.iconSize - 4
|
||||
color: control.usable ? Theme.primary : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: slider
|
||||
|
||||
anchors.left: icon.right
|
||||
anchors.leftMargin: Theme.spacingXS
|
||||
anchors.right: valueLabel.left
|
||||
anchors.rightMargin: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: control.usable
|
||||
minimum: control.sliderMinimum
|
||||
maximum: control.sliderMaximum
|
||||
showValue: false
|
||||
onSliderValueChanged: newValue => {
|
||||
if (control.usable)
|
||||
DisplayService.setBrightness(newValue, control.deviceId, true);
|
||||
}
|
||||
|
||||
// The slider writes its own value while dragging, so only follow the
|
||||
// service when the user is not holding it.
|
||||
Binding on value {
|
||||
value: control.currentBrightness
|
||||
when: !slider.isDragging
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: valueLabel
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 34
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: control.usable ? control.currentBrightness + (control.device && control.device.class === "ddc" && !SessionData.getBrightnessExponential(control.device.id) ? "" : "%") : "n/a"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
DankTooltipV2 {
|
||||
id: tooltip
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: icon
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.NoButton
|
||||
onEntered: tooltip.show(control.deviceId !== "" ? control.screenName + " · " + control.deviceId : "No brightness device", icon, 0, 0, "bottom")
|
||||
onExited: tooltip.hide()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.Pipewire
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankFlickable {
|
||||
id: deviceList
|
||||
|
||||
property bool isSink: true
|
||||
|
||||
readonly property var activeNode: isSink ? AudioService.sink : AudioService.source
|
||||
readonly property var deviceNodes: {
|
||||
const hidden = isSink ? (SessionData.hiddenOutputDeviceNames ?? []) : (SessionData.hiddenInputDeviceNames ?? []);
|
||||
const nodes = Pipewire.nodes.values.filter(node => {
|
||||
if (!node.audio || node.isStream)
|
||||
return false;
|
||||
if (node.isSink !== isSink)
|
||||
return false;
|
||||
return !hidden.includes(node.name);
|
||||
});
|
||||
|
||||
const active = activeNode;
|
||||
return [...nodes].sort((a, b) => {
|
||||
if (a === active)
|
||||
return -1;
|
||||
if (b === active)
|
||||
return 1;
|
||||
return AudioService.displayName(a).localeCompare(AudioService.displayName(b));
|
||||
});
|
||||
}
|
||||
|
||||
function activate(node) {
|
||||
if (!node || !node.name)
|
||||
return;
|
||||
|
||||
if (deviceList.isSink)
|
||||
AudioService.setDefaultSinkByName(node.name);
|
||||
else
|
||||
AudioService.setDefaultSourceByName(node.name);
|
||||
}
|
||||
|
||||
clip: true
|
||||
contentHeight: deviceColumn.height
|
||||
contentWidth: width
|
||||
|
||||
Column {
|
||||
id: deviceColumn
|
||||
|
||||
width: deviceList.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: deviceList.deviceNodes
|
||||
}
|
||||
|
||||
delegate: Rectangle {
|
||||
id: deviceItem
|
||||
|
||||
required property var modelData
|
||||
|
||||
readonly property bool isActive: modelData === deviceList.activeNode
|
||||
|
||||
width: deviceColumn.width
|
||||
height: 46
|
||||
radius: Theme.cornerRadius
|
||||
color: isActive ? Theme.primarySelected : (deviceArea.containsMouse ? Theme.surfaceHover : Theme.withAlpha(Theme.surfaceLight, Theme.popupTransparency))
|
||||
border.color: isActive ? Theme.primary : Theme.outlineLight
|
||||
border.width: isActive ? 2 : Theme.layerOutlineWidth
|
||||
|
||||
DankIcon {
|
||||
id: deviceIcon
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: deviceList.isSink ? AudioService.sinkIcon(deviceItem.modelData) : "mic"
|
||||
size: Theme.iconSize - 4
|
||||
color: deviceItem.isActive ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
id: activeCheck
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: "check_circle"
|
||||
size: Theme.iconSize - 6
|
||||
color: Theme.primary
|
||||
visible: deviceItem.isActive
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.left: deviceIcon.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: activeCheck.visible ? activeCheck.left : parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: 1
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: AudioService.displayName(deviceItem.modelData)
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: deviceItem.isActive ? Font.Medium : Font.Normal
|
||||
color: Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: parent.width
|
||||
text: AudioService.subtitle(deviceItem.modelData.name)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
elide: Text.ElideRight
|
||||
visible: text !== ""
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: deviceArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: deviceList.activate(deviceItem.modelData)
|
||||
}
|
||||
|
||||
PwObjectTracker {
|
||||
objects: [deviceItem.modelData]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
width: deviceColumn.width
|
||||
text: deviceList.isSink ? "No output devices found" : "No input devices found"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
visible: deviceList.deviceNodes.length === 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Item {
|
||||
id: row
|
||||
|
||||
property var node: null
|
||||
property int maxPercent: 100
|
||||
property string activeIcon: "volume_up"
|
||||
property string mutedIcon: "volume_off"
|
||||
|
||||
readonly property var audio: (node && node.audio) ? node.audio : null
|
||||
readonly property bool muted: audio ? audio.muted : false
|
||||
readonly property int percent: audio ? Math.round(audio.volume * 100) : 0
|
||||
|
||||
readonly property int sliderMaximum: Math.max(100, maxPercent)
|
||||
|
||||
// DankSlider assigns its own `value` while dragging, which breaks a declarative
|
||||
// binding, so the slider is synced imperatively whenever the node volume changes.
|
||||
function syncSlider() {
|
||||
slider.value = Math.min(row.sliderMaximum, row.percent);
|
||||
}
|
||||
|
||||
function applyVolume(newValue) {
|
||||
if (!row.audio)
|
||||
return;
|
||||
|
||||
row.audio.volume = newValue / 100;
|
||||
if (newValue > 0 && row.audio.muted)
|
||||
row.audio.muted = false;
|
||||
}
|
||||
|
||||
height: 48
|
||||
onPercentChanged: row.syncSlider()
|
||||
onSliderMaximumChanged: row.syncSlider()
|
||||
onNodeChanged: row.syncSlider()
|
||||
|
||||
Rectangle {
|
||||
id: muteButton
|
||||
|
||||
anchors.left: parent.left
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 40
|
||||
height: 40
|
||||
radius: width / 2
|
||||
color: muteArea.containsMouse ? Theme.primaryHover : "transparent"
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: (row.muted || row.percent === 0) ? row.mutedIcon : row.activeIcon
|
||||
size: Theme.iconSize
|
||||
color: row.muted ? Theme.error : Theme.primary
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: muteArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: row.audio !== null
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (row.audio)
|
||||
row.audio.muted = !row.audio.muted;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: valueLabel
|
||||
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 48
|
||||
horizontalAlignment: Text.AlignRight
|
||||
text: row.percent + "%"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
color: row.muted ? Theme.surfaceVariantText : Theme.surfaceText
|
||||
}
|
||||
|
||||
DankSlider {
|
||||
id: slider
|
||||
|
||||
anchors.left: muteButton.right
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: valueLabel.left
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
enabled: row.audio !== null
|
||||
minimum: 0
|
||||
maximum: row.sliderMaximum
|
||||
showValue: false
|
||||
onSliderValueChanged: newValue => row.applyVolume(newValue)
|
||||
Component.onCompleted: row.syncSlider()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user