mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2025-12-07 14:05:38 -05:00
iBluetooth repair, input device support, some media player improvements
This commit is contained in:
420
MediaPlayer.qml
420
MediaPlayer.qml
@@ -1,420 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Qt5Compat.GraphicalEffects
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Io
|
|
||||||
import Quickshell.Services.Mpris
|
|
||||||
import "Services"
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: mediaPlayer
|
|
||||||
|
|
||||||
property var theme
|
|
||||||
property bool isVisible: false
|
|
||||||
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
||||||
property bool hasActiveMedia: MprisController.isPlaying && (activePlayer?.trackTitle || activePlayer?.trackArtist)
|
|
||||||
|
|
||||||
property var defaultTheme: QtObject {
|
|
||||||
property color primary: "#D0BCFF"
|
|
||||||
property color background: "#10121E"
|
|
||||||
property color surfaceContainer: "#1D1B20"
|
|
||||||
property color surfaceText: "#E6E0E9"
|
|
||||||
property color surfaceVariant: "#49454F"
|
|
||||||
property color surfaceVariantText: "#CAC4D0"
|
|
||||||
property color outline: "#938F99"
|
|
||||||
property color error: "#F2B8B5"
|
|
||||||
property real cornerRadius: 12
|
|
||||||
property real cornerRadiusLarge: 16
|
|
||||||
property real cornerRadiusXLarge: 24
|
|
||||||
property real cornerRadiusSmall: 8
|
|
||||||
property real spacingXS: 4
|
|
||||||
property real spacingS: 8
|
|
||||||
property real spacingM: 12
|
|
||||||
property real spacingL: 16
|
|
||||||
property real spacingXL: 24
|
|
||||||
property real fontSizeLarge: 16
|
|
||||||
property real fontSizeMedium: 14
|
|
||||||
property real fontSizeSmall: 12
|
|
||||||
property real iconSize: 24
|
|
||||||
property real iconSizeLarge: 32
|
|
||||||
property string iconFont: "Material Symbols Rounded"
|
|
||||||
property int iconFontWeight: Font.Normal
|
|
||||||
property int shortDuration: 150
|
|
||||||
property int mediumDuration: 300
|
|
||||||
property int standardEasing: Easing.OutCubic
|
|
||||||
property int emphasizedEasing: Easing.OutQuart
|
|
||||||
}
|
|
||||||
|
|
||||||
property var activeTheme: theme || defaultTheme
|
|
||||||
|
|
||||||
onHasActiveMediaChanged: {
|
|
||||||
if (!hasActiveMedia && isVisible) {
|
|
||||||
hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
bottom: true
|
|
||||||
}
|
|
||||||
|
|
||||||
WlrLayershell.layer: WlrLayershell.Overlay
|
|
||||||
WlrLayershell.exclusiveZone: -1
|
|
||||||
WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.Exclusive : WlrKeyboardFocus.None
|
|
||||||
WlrLayershell.namespace: "quickshell-media-player"
|
|
||||||
|
|
||||||
visible: isVisible
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.3)
|
|
||||||
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
|
|
||||||
visible: mediaPlayer.isVisible
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: activeTheme.shortDuration
|
|
||||||
easing.type: activeTheme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
enabled: mediaPlayer.isVisible
|
|
||||||
onClicked: mediaPlayer.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: mediaPanel
|
|
||||||
|
|
||||||
width: 480
|
|
||||||
height: 320
|
|
||||||
|
|
||||||
anchors.centerIn: parent
|
|
||||||
|
|
||||||
color: Qt.rgba(activeTheme.surfaceContainer.r, activeTheme.surfaceContainer.g, activeTheme.surfaceContainer.b, 0.98)
|
|
||||||
radius: activeTheme.cornerRadiusXLarge
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: -2
|
|
||||||
color: "transparent"
|
|
||||||
radius: parent.radius + 2
|
|
||||||
border.color: Qt.rgba(0, 0, 0, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
z: -2
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Qt.rgba(activeTheme.outline.r, activeTheme.outline.g, activeTheme.outline.b, 0.12)
|
|
||||||
border.width: 1
|
|
||||||
radius: parent.radius
|
|
||||||
z: -1
|
|
||||||
}
|
|
||||||
|
|
||||||
transform: [
|
|
||||||
Scale {
|
|
||||||
origin.x: mediaPanel.width / 2
|
|
||||||
origin.y: mediaPanel.height / 2
|
|
||||||
xScale: mediaPlayer.isVisible ? 1.0 : 0.9
|
|
||||||
yScale: mediaPlayer.isVisible ? 1.0 : 0.9
|
|
||||||
|
|
||||||
Behavior on xScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: activeTheme.mediumDuration
|
|
||||||
easing.type: activeTheme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on yScale {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: activeTheme.mediumDuration
|
|
||||||
easing.type: activeTheme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
opacity: mediaPlayer.isVisible ? 1.0 : 0.0
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: activeTheme.mediumDuration
|
|
||||||
easing.type: activeTheme.emphasizedEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: activeTheme.spacingXL
|
|
||||||
spacing: activeTheme.spacingL
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: 32
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
text: "Now Playing"
|
|
||||||
font.pixelSize: activeTheme.fontSizeLarge + 4
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: activeTheme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: parent.width - 200; height: 1 }
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 32
|
|
||||||
height: 32
|
|
||||||
radius: activeTheme.cornerRadius
|
|
||||||
color: closeArea.containsMouse ? Qt.rgba(activeTheme.error.r, activeTheme.error.g, activeTheme.error.b, 0.12) : "transparent"
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "close"
|
|
||||||
font.family: activeTheme.iconFont
|
|
||||||
font.pixelSize: activeTheme.iconSize
|
|
||||||
color: closeArea.containsMouse ? activeTheme.error : activeTheme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: closeArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: mediaPlayer.hide()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
height: parent.height - 80
|
|
||||||
spacing: activeTheme.spacingXL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 180
|
|
||||||
height: parent.height
|
|
||||||
radius: activeTheme.cornerRadiusLarge
|
|
||||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
clip: true
|
|
||||||
|
|
||||||
Image {
|
|
||||||
id: albumArt
|
|
||||||
anchors.fill: parent
|
|
||||||
source: activePlayer?.trackArtUrl || ""
|
|
||||||
fillMode: Image.PreserveAspectCrop
|
|
||||||
smooth: true
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
visible: albumArt.status !== Image.Ready
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "album"
|
|
||||||
font.family: activeTheme.iconFont
|
|
||||||
font.pixelSize: 48
|
|
||||||
color: activeTheme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width - 180 - activeTheme.spacingXL
|
|
||||||
height: parent.height
|
|
||||||
spacing: activeTheme.spacingM
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: activeTheme.spacingS
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: activePlayer?.trackTitle || "No title"
|
|
||||||
font.pixelSize: activeTheme.fontSizeLarge + 2
|
|
||||||
font.weight: Font.Bold
|
|
||||||
color: activeTheme.surfaceText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: activePlayer?.trackArtist || "Unknown artist"
|
|
||||||
font.pixelSize: activeTheme.fontSizeLarge
|
|
||||||
color: activeTheme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: activePlayer?.trackAlbum || ""
|
|
||||||
font.pixelSize: activeTheme.fontSizeMedium
|
|
||||||
color: activeTheme.surfaceVariantText
|
|
||||||
elide: Text.ElideRight
|
|
||||||
width: parent.width
|
|
||||||
visible: text.length > 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { height: activeTheme.spacingM }
|
|
||||||
|
|
||||||
Column {
|
|
||||||
width: parent.width
|
|
||||||
spacing: activeTheme.spacingS
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
|
||||||
height: 6
|
|
||||||
radius: 3
|
|
||||||
color: Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.3)
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width * (activePlayer?.position / Math.max(activePlayer?.length || 1, 1))
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: activeTheme.primary
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: formatTime(activePlayer?.position || 0)
|
|
||||||
font.pixelSize: activeTheme.fontSizeSmall
|
|
||||||
color: activeTheme.surfaceVariantText
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { width: parent.width - 100; height: 1 }
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: formatTime(activePlayer?.length || 0)
|
|
||||||
font.pixelSize: activeTheme.fontSizeSmall
|
|
||||||
color: activeTheme.surfaceVariantText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item { height: activeTheme.spacingL }
|
|
||||||
|
|
||||||
Row {
|
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
|
||||||
spacing: activeTheme.spacingL
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
radius: 24
|
|
||||||
color: prevArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "skip_previous"
|
|
||||||
font.family: activeTheme.iconFont
|
|
||||||
font.pixelSize: activeTheme.iconSize
|
|
||||||
color: activeTheme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: prevArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer?.previous()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 56
|
|
||||||
height: 56
|
|
||||||
radius: 28
|
|
||||||
color: activeTheme.primary
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
|
||||||
font.family: activeTheme.iconFont
|
|
||||||
font.pixelSize: activeTheme.iconSizeLarge
|
|
||||||
color: activeTheme.background
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer?.togglePlaying()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
width: 48
|
|
||||||
height: 48
|
|
||||||
radius: 24
|
|
||||||
color: nextArea.containsMouse ? Qt.rgba(activeTheme.surfaceVariant.r, activeTheme.surfaceVariant.g, activeTheme.surfaceVariant.b, 0.12) : "transparent"
|
|
||||||
|
|
||||||
Text {
|
|
||||||
anchors.centerIn: parent
|
|
||||||
text: "skip_next"
|
|
||||||
font.family: activeTheme.iconFont
|
|
||||||
font.pixelSize: activeTheme.iconSize
|
|
||||||
color: activeTheme.surfaceText
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: nextArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
onClicked: activePlayer?.next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
|
||||||
running: activePlayer?.playbackState === MprisPlaybackState.Playing
|
|
||||||
interval: 1000
|
|
||||||
repeat: true
|
|
||||||
onTriggered: activePlayer?.positionChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(seconds) {
|
|
||||||
const mins = Math.floor(seconds / 60)
|
|
||||||
const secs = Math.floor(seconds % 60)
|
|
||||||
return mins + ":" + (secs < 10 ? "0" : "") + secs
|
|
||||||
}
|
|
||||||
|
|
||||||
function show() {
|
|
||||||
mediaPlayer.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function hide() {
|
|
||||||
mediaPlayer.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggle() {
|
|
||||||
if (mediaPlayer.isVisible) {
|
|
||||||
hide()
|
|
||||||
} else {
|
|
||||||
show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,6 +11,11 @@ Singleton {
|
|||||||
property var audioSinks: []
|
property var audioSinks: []
|
||||||
property string currentAudioSink: ""
|
property string currentAudioSink: ""
|
||||||
|
|
||||||
|
// Microphone properties
|
||||||
|
property int micLevel: 50
|
||||||
|
property var audioSources: []
|
||||||
|
property string currentAudioSource: ""
|
||||||
|
|
||||||
// Real Audio Control
|
// Real Audio Control
|
||||||
Process {
|
Process {
|
||||||
id: volumeChecker
|
id: volumeChecker
|
||||||
@@ -27,6 +32,22 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Microphone level checker
|
||||||
|
Process {
|
||||||
|
id: micLevelChecker
|
||||||
|
command: ["bash", "-c", "pactl get-source-volume @DEFAULT_SOURCE@ | grep -o '[0-9]*%' | head -1 | tr -d '%'"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
if (data.trim()) {
|
||||||
|
root.micLevel = Math.min(100, parseInt(data.trim()) || 50)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: audioSinkLister
|
id: audioSinkLister
|
||||||
command: ["pactl", "list", "sinks"]
|
command: ["pactl", "list", "sinks"]
|
||||||
@@ -35,7 +56,6 @@ Singleton {
|
|||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
console.log("Parsing pactl sink output...")
|
|
||||||
let sinks = []
|
let sinks = []
|
||||||
let lines = text.trim().split('\n')
|
let lines = text.trim().split('\n')
|
||||||
|
|
||||||
@@ -88,7 +108,6 @@ Singleton {
|
|||||||
sink.displayName = displayName
|
sink.displayName = displayName
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Final audio sinks:", JSON.stringify(sinks, null, 2))
|
|
||||||
root.audioSinks = sinks
|
root.audioSinks = sinks
|
||||||
defaultSinkChecker.running = true
|
defaultSinkChecker.running = true
|
||||||
}
|
}
|
||||||
@@ -96,6 +115,61 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Audio source (microphone) lister
|
||||||
|
Process {
|
||||||
|
id: audioSourceLister
|
||||||
|
command: ["pactl", "list", "sources"]
|
||||||
|
running: true
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
if (text.trim()) {
|
||||||
|
let sources = []
|
||||||
|
let lines = text.trim().split('\n')
|
||||||
|
|
||||||
|
let currentSource = null
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
line = line.trim()
|
||||||
|
|
||||||
|
// New source starts
|
||||||
|
if (line.startsWith('Source #')) {
|
||||||
|
if (currentSource && currentSource.name && currentSource.id) {
|
||||||
|
sources.push(currentSource)
|
||||||
|
}
|
||||||
|
currentSource = {
|
||||||
|
id: line.replace('Source #', '').replace(':', ''),
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
active: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Source name
|
||||||
|
else if (line.startsWith('Name: ') && currentSource) {
|
||||||
|
currentSource.name = line.replace('Name: ', '')
|
||||||
|
}
|
||||||
|
// Description (display name)
|
||||||
|
else if (line.startsWith('Description: ') && currentSource) {
|
||||||
|
let desc = line.replace('Description: ', '')
|
||||||
|
currentSource.displayName = desc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the last source
|
||||||
|
if (currentSource && currentSource.name && currentSource.id) {
|
||||||
|
sources.push(currentSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out monitor sources (we want actual input devices)
|
||||||
|
sources = sources.filter(source => !source.name.includes('.monitor'))
|
||||||
|
|
||||||
|
root.audioSources = sources
|
||||||
|
defaultSourceChecker.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: defaultSinkChecker
|
id: defaultSinkChecker
|
||||||
command: ["pactl", "get-default-sink"]
|
command: ["pactl", "get-default-sink"]
|
||||||
@@ -106,7 +180,6 @@ Singleton {
|
|||||||
onRead: (data) => {
|
onRead: (data) => {
|
||||||
if (data.trim()) {
|
if (data.trim()) {
|
||||||
root.currentAudioSink = data.trim()
|
root.currentAudioSink = data.trim()
|
||||||
console.log("Default audio sink:", root.currentAudioSink)
|
|
||||||
|
|
||||||
// Update active status in audioSinks
|
// Update active status in audioSinks
|
||||||
let updatedSinks = []
|
let updatedSinks = []
|
||||||
@@ -124,6 +197,34 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Default source (microphone) checker
|
||||||
|
Process {
|
||||||
|
id: defaultSourceChecker
|
||||||
|
command: ["pactl", "get-default-source"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: SplitParser {
|
||||||
|
splitMarker: "\n"
|
||||||
|
onRead: (data) => {
|
||||||
|
if (data.trim()) {
|
||||||
|
root.currentAudioSource = data.trim()
|
||||||
|
|
||||||
|
// Update active status in audioSources
|
||||||
|
let updatedSources = []
|
||||||
|
for (let source of root.audioSources) {
|
||||||
|
updatedSources.push({
|
||||||
|
id: source.id,
|
||||||
|
name: source.name,
|
||||||
|
displayName: source.displayName,
|
||||||
|
active: source.name === root.currentAudioSource
|
||||||
|
})
|
||||||
|
}
|
||||||
|
root.audioSources = updatedSources
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setVolume(percentage) {
|
function setVolume(percentage) {
|
||||||
let volumeSetProcess = Qt.createQmlObject('
|
let volumeSetProcess = Qt.createQmlObject('
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
@@ -135,6 +236,17 @@ Singleton {
|
|||||||
', root)
|
', root)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setMicLevel(percentage) {
|
||||||
|
let micSetProcess = Qt.createQmlObject('
|
||||||
|
import Quickshell.Io
|
||||||
|
Process {
|
||||||
|
command: ["pactl", "set-source-volume", "@DEFAULT_SOURCE@", "' + percentage + '%"]
|
||||||
|
running: true
|
||||||
|
onExited: micLevelChecker.running = true
|
||||||
|
}
|
||||||
|
', root)
|
||||||
|
}
|
||||||
|
|
||||||
function setAudioSink(sinkName) {
|
function setAudioSink(sinkName) {
|
||||||
console.log("Setting audio sink to:", sinkName)
|
console.log("Setting audio sink to:", sinkName)
|
||||||
|
|
||||||
@@ -160,4 +272,39 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAudioSource(sourceName) {
|
||||||
|
console.log("Setting audio source to:", sourceName)
|
||||||
|
|
||||||
|
sourceSetProcess.command = ["pactl", "set-default-source", sourceName]
|
||||||
|
sourceSetProcess.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dedicated process for setting audio source
|
||||||
|
Process {
|
||||||
|
id: sourceSetProcess
|
||||||
|
running: false
|
||||||
|
|
||||||
|
onExited: (exitCode) => {
|
||||||
|
console.log("Audio source change exit code:", exitCode)
|
||||||
|
if (exitCode === 0) {
|
||||||
|
console.log("Audio source changed successfully")
|
||||||
|
// Refresh current source and list
|
||||||
|
defaultSourceChecker.running = true
|
||||||
|
audioSourceLister.running = true
|
||||||
|
} else {
|
||||||
|
console.error("Failed to change audio source")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer to refresh audio devices regularly (catches new Bluetooth devices)
|
||||||
|
Timer {
|
||||||
|
interval: 4000 // 4s refresh to catch new BT devices
|
||||||
|
running: true; repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
audioSinkLister.running = true
|
||||||
|
audioSourceLister.running = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,14 +17,13 @@ Singleton {
|
|||||||
// Real Bluetooth Management
|
// Real Bluetooth Management
|
||||||
Process {
|
Process {
|
||||||
id: bluetoothStatusChecker
|
id: bluetoothStatusChecker
|
||||||
command: ["bluetoothctl", "show"]
|
command: ["bluetoothctl", "show"] // Use default controller
|
||||||
running: true
|
running: true
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
|
root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller")
|
||||||
root.bluetoothEnabled = text.includes("Powered: yes")
|
root.bluetoothEnabled = text.includes("Powered: yes")
|
||||||
console.log("Bluetooth available:", root.bluetoothAvailable, "enabled:", root.bluetoothEnabled)
|
|
||||||
|
|
||||||
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
if (root.bluetoothEnabled && root.bluetoothAvailable) {
|
||||||
bluetoothDeviceScanner.running = true
|
bluetoothDeviceScanner.running = true
|
||||||
@@ -37,7 +36,7 @@ Singleton {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: bluetoothDeviceScanner
|
id: bluetoothDeviceScanner
|
||||||
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep 'Connected:' | grep -q 'yes' && echo 'true' || echo 'false'); battery=$(echo \"$info\" | grep 'Battery Percentage' | grep -o '([0-9]*)' | tr -d '()'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
|
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([0-9A-F:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]]; then info=$(bluetoothctl info $mac); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); battery=$(echo \"$info\" | grep -m1 'Battery Percentage:' | grep -o '[0-9]\\+'); echo \"$mac|$name|$connected|${battery:-}\"; fi; fi; done"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
@@ -52,7 +51,7 @@ Singleton {
|
|||||||
if (parts.length >= 3) {
|
if (parts.length >= 3) {
|
||||||
let mac = parts[0].trim()
|
let mac = parts[0].trim()
|
||||||
let name = parts[1].trim()
|
let name = parts[1].trim()
|
||||||
let connected = parts[2].trim() === 'true'
|
let connected = parts[2].trim() === 'yes'
|
||||||
let battery = parts[3] ? parseInt(parts[3]) : -1
|
let battery = parts[3] ? parseInt(parts[3]) : -1
|
||||||
|
|
||||||
// Skip if name is still a technical path
|
// Skip if name is still a technical path
|
||||||
@@ -82,7 +81,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
root.bluetoothDevices = devices
|
root.bluetoothDevices = devices
|
||||||
console.log("Found", devices.length, "Bluetooth devices")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,23 +93,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function startDiscovery() {
|
function startDiscovery() {
|
||||||
console.log("Starting Bluetooth discovery...")
|
root.scanning = true
|
||||||
let discoveryProcess = Qt.createQmlObject('
|
// Run comprehensive scan that gets all devices
|
||||||
import Quickshell.Io
|
discoveryScanner.running = true
|
||||||
Process {
|
|
||||||
command: ["bluetoothctl", "scan", "on"]
|
|
||||||
running: true
|
|
||||||
onExited: {
|
|
||||||
root.scanning = true
|
|
||||||
// Scan for 10 seconds then get discovered devices
|
|
||||||
discoveryScanTimer.start()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
', root)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopDiscovery() {
|
function stopDiscovery() {
|
||||||
console.log("Stopping Bluetooth discovery...")
|
|
||||||
let stopDiscoveryProcess = Qt.createQmlObject('
|
let stopDiscoveryProcess = Qt.createQmlObject('
|
||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
Process {
|
Process {
|
||||||
@@ -180,7 +167,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggleBluetoothDevice(mac) {
|
function toggleBluetoothDevice(mac) {
|
||||||
console.log("Toggling Bluetooth device:", mac)
|
|
||||||
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
let device = root.bluetoothDevices.find(d => d.mac === mac)
|
||||||
if (device) {
|
if (device) {
|
||||||
let action = device.connected ? "disconnect" : "connect"
|
let action = device.connected ? "disconnect" : "connect"
|
||||||
@@ -207,26 +193,68 @@ Singleton {
|
|||||||
', root)
|
', root)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timer for discovery scanning
|
// Timer to refresh adapter & device state
|
||||||
Timer {
|
Timer {
|
||||||
id: discoveryScanTimer
|
interval: 3000 // 3s refresh for more responsive updates
|
||||||
interval: 8000 // 8 seconds
|
running: true; repeat: true
|
||||||
repeat: false
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
availableDeviceScanner.running = true
|
bluetoothStatusChecker.running = true
|
||||||
|
if (root.bluetoothEnabled) {
|
||||||
|
bluetoothDeviceScanner.running = true
|
||||||
|
// Also refresh paired devices to get current connection status
|
||||||
|
pairedDeviceChecker.discoveredToMerge = []
|
||||||
|
pairedDeviceChecker.running = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scan for available/discoverable devices
|
property var discoveredDevices: []
|
||||||
|
|
||||||
|
// Handle discovered devices
|
||||||
|
function _handleDiscovered(found) {
|
||||||
|
|
||||||
|
let discoveredDevices = []
|
||||||
|
for (let device of found) {
|
||||||
|
let type = "bluetooth"
|
||||||
|
let nameLower = device.name.toLowerCase()
|
||||||
|
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||||
|
else if (nameLower.includes("mouse")) type = "mouse"
|
||||||
|
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||||
|
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
|
||||||
|
else if (nameLower.includes("watch")) type = "watch"
|
||||||
|
else if (nameLower.includes("speaker")) type = "speaker"
|
||||||
|
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||||
|
|
||||||
|
discoveredDevices.push({
|
||||||
|
mac: device.mac,
|
||||||
|
name: device.name,
|
||||||
|
type: type,
|
||||||
|
paired: false,
|
||||||
|
connected: false,
|
||||||
|
rssi: -70,
|
||||||
|
signalStrength: "fair",
|
||||||
|
canPair: true
|
||||||
|
})
|
||||||
|
|
||||||
|
console.log(" -", device.name, "(", device.mac, ")")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paired devices first, then merge with discovered
|
||||||
|
pairedDeviceChecker.discoveredToMerge = discoveredDevices
|
||||||
|
pairedDeviceChecker.running = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get only currently connected/paired devices that matter
|
||||||
Process {
|
Process {
|
||||||
id: availableDeviceScanner
|
id: availableDeviceScanner
|
||||||
command: ["bash", "-c", "timeout 5 bluetoothctl devices | grep -v 'Device.*/' | while read -r line; do if [[ $line =~ Device\ ([0-9A-F:]+)\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(timeout 3 bluetoothctl info $mac 2>/dev/null); paired=$(echo \"$info\" | grep 'Paired:' | grep -q 'yes' && echo 'true' || echo 'false'); connected=$(echo \"$info\" | grep 'Connected:' | grep -q 'yes' && echo 'true' || echo 'false'); rssi=$(echo \"$info\" | grep 'RSSI:' | awk '{print $2}' | head -n1); echo \"$mac|$name|$paired|$connected|${rssi:-}\"; fi; fi; done"]
|
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); if [[ \"$paired\" == \"yes\" ]] || [[ \"$connected\" == \"yes\" ]]; then echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
stdout: StdioCollector {
|
stdout: StdioCollector {
|
||||||
onStreamFinished: {
|
onStreamFinished: {
|
||||||
|
|
||||||
|
let devices = []
|
||||||
if (text.trim()) {
|
if (text.trim()) {
|
||||||
let devices = []
|
|
||||||
let lines = text.trim().split('\n')
|
let lines = text.trim().split('\n')
|
||||||
|
|
||||||
for (let line of lines) {
|
for (let line of lines) {
|
||||||
@@ -235,16 +263,15 @@ Singleton {
|
|||||||
if (parts.length >= 4) {
|
if (parts.length >= 4) {
|
||||||
let mac = parts[0].trim()
|
let mac = parts[0].trim()
|
||||||
let name = parts[1].trim()
|
let name = parts[1].trim()
|
||||||
let paired = parts[2].trim() === 'true'
|
let paired = parts[2].trim() === 'yes'
|
||||||
let connected = parts[3].trim() === 'true'
|
let connected = parts[3].trim() === 'yes'
|
||||||
let rssi = parts[4] ? parseInt(parts[4]) : 0
|
|
||||||
|
|
||||||
// Skip if name is still a technical path
|
// Skip technical names
|
||||||
if (name.startsWith('/org/bluez') || name.includes('hci0')) {
|
if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine device type from name
|
// Determine device type
|
||||||
let type = "bluetooth"
|
let type = "bluetooth"
|
||||||
let nameLower = name.toLowerCase()
|
let nameLower = name.toLowerCase()
|
||||||
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||||
@@ -255,32 +282,128 @@ Singleton {
|
|||||||
else if (nameLower.includes("speaker")) type = "speaker"
|
else if (nameLower.includes("speaker")) type = "speaker"
|
||||||
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||||
|
|
||||||
// Signal strength assessment
|
|
||||||
let signalStrength = "unknown"
|
|
||||||
if (rssi !== 0) {
|
|
||||||
if (rssi >= -50) signalStrength = "excellent"
|
|
||||||
else if (rssi >= -60) signalStrength = "good"
|
|
||||||
else if (rssi >= -70) signalStrength = "fair"
|
|
||||||
else signalStrength = "weak"
|
|
||||||
}
|
|
||||||
|
|
||||||
devices.push({
|
devices.push({
|
||||||
mac: mac,
|
mac: mac,
|
||||||
name: name,
|
name: name,
|
||||||
type: type,
|
type: type,
|
||||||
paired: paired,
|
paired: paired,
|
||||||
connected: connected,
|
connected: connected,
|
||||||
rssi: rssi,
|
rssi: 0,
|
||||||
signalStrength: signalStrength,
|
signalStrength: "unknown",
|
||||||
canPair: !paired
|
canPair: false // Already paired
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
root.availableDevices = devices
|
|
||||||
console.log("Found", devices.length, "available Bluetooth devices")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
root.availableDevices = devices
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discovery scanner using bluetoothctl --timeout
|
||||||
|
Process {
|
||||||
|
id: discoveryScanner
|
||||||
|
// Discover for 8 s in non-interactive mode, then auto-exit
|
||||||
|
command: ["bluetoothctl",
|
||||||
|
"--timeout", "8",
|
||||||
|
"--monitor", // keeps stdout unbuffered
|
||||||
|
"scan", "on"]
|
||||||
|
running: false
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
/*
|
||||||
|
* bluetoothctl prints lines like:
|
||||||
|
* [NEW] Device 12:34:56:78:9A:BC My-Headphones
|
||||||
|
*/
|
||||||
|
const rx = /^\[NEW\] Device ([0-9A-F:]+)\s+(.+)$/i;
|
||||||
|
const found = text.split('\n')
|
||||||
|
.filter(l => rx.test(l))
|
||||||
|
.map(l => {
|
||||||
|
const [,mac,name] = l.match(rx);
|
||||||
|
return { mac, name };
|
||||||
|
});
|
||||||
|
root._handleDiscovered(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onExited: {
|
||||||
|
root.scanning = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get paired devices and merge with discovered ones
|
||||||
|
Process {
|
||||||
|
id: pairedDeviceChecker
|
||||||
|
command: ["bash", "-c", "bluetoothctl devices | while read -r line; do if [[ $line =~ Device\\ ([A-F0-9:]+)\\ (.+) ]]; then mac=\"${BASH_REMATCH[1]}\"; name=\"${BASH_REMATCH[2]}\"; if [[ ${#name} -gt 3 ]] && [[ ! $name =~ ^/org/bluez ]] && [[ ! $name =~ hci0 ]]; then info=$(bluetoothctl info \"$mac\" 2>/dev/null); paired=$(echo \"$info\" | grep -m1 'Paired:' | awk '{print $2}'); connected=$(echo \"$info\" | grep -m1 'Connected:' | awk '{print $2}'); echo \"$mac|$name|$paired|$connected\"; fi; fi; done"]
|
||||||
|
running: false
|
||||||
|
property var discoveredToMerge: []
|
||||||
|
|
||||||
|
stdout: StdioCollector {
|
||||||
|
onStreamFinished: {
|
||||||
|
// Start with discovered devices (unpaired, available to pair)
|
||||||
|
let allDevices = [...pairedDeviceChecker.discoveredToMerge]
|
||||||
|
let seenMacs = new Set(allDevices.map(d => d.mac))
|
||||||
|
|
||||||
|
// Add only actually paired devices from bluetoothctl
|
||||||
|
if (text.trim()) {
|
||||||
|
let lines = text.trim().split('\n')
|
||||||
|
|
||||||
|
for (let line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
let parts = line.split('|')
|
||||||
|
if (parts.length >= 4) {
|
||||||
|
let mac = parts[0].trim()
|
||||||
|
let name = parts[1].trim()
|
||||||
|
let paired = parts[2].trim() === 'yes'
|
||||||
|
let connected = parts[3].trim() === 'yes'
|
||||||
|
|
||||||
|
// Only include if actually paired
|
||||||
|
if (!paired) continue
|
||||||
|
|
||||||
|
// Check if already in discovered list
|
||||||
|
if (seenMacs.has(mac)) {
|
||||||
|
// Update existing device to show it's paired
|
||||||
|
let existing = allDevices.find(d => d.mac === mac)
|
||||||
|
if (existing) {
|
||||||
|
existing.paired = true
|
||||||
|
existing.connected = connected
|
||||||
|
existing.canPair = false
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add paired device not found during scan
|
||||||
|
let type = "bluetooth"
|
||||||
|
let nameLower = name.toLowerCase()
|
||||||
|
if (nameLower.includes("headphone") || nameLower.includes("airpod") || nameLower.includes("headset") || nameLower.includes("arctis") || nameLower.includes("audio")) type = "headset"
|
||||||
|
else if (nameLower.includes("mouse")) type = "mouse"
|
||||||
|
else if (nameLower.includes("keyboard")) type = "keyboard"
|
||||||
|
else if (nameLower.includes("phone") || nameLower.includes("iphone") || nameLower.includes("samsung") || nameLower.includes("android")) type = "phone"
|
||||||
|
else if (nameLower.includes("watch")) type = "watch"
|
||||||
|
else if (nameLower.includes("speaker")) type = "speaker"
|
||||||
|
else if (nameLower.includes("tv") || nameLower.includes("display")) type = "tv"
|
||||||
|
|
||||||
|
allDevices.push({
|
||||||
|
mac: mac,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
paired: true,
|
||||||
|
connected: connected,
|
||||||
|
rssi: -100,
|
||||||
|
signalStrength: "unknown",
|
||||||
|
canPair: false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
root.availableDevices = allDevices
|
||||||
|
root.scanning = false
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,16 +27,15 @@ Singleton {
|
|||||||
target: modelData
|
target: modelData
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
console.log("MPRIS Player connected:", modelData.identity)
|
|
||||||
if (root.trackedPlayer == null || modelData.isPlaying) {
|
if (root.trackedPlayer == null || modelData.isPlaying) {
|
||||||
root.trackedPlayer = modelData
|
root.trackedPlayer = modelData
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: {
|
||||||
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
|
if (root.trackedPlayer == null || root.trackedPlayer.playbackState !== MprisPlaybackState.Playing) {
|
||||||
for (const player of Mpris.players.values) {
|
for (const player of Mpris.players.values) {
|
||||||
if (player.playbackState.isPlaying) {
|
if (player.playbackState === MprisPlaybackState.Playing) {
|
||||||
root.trackedPlayer = player
|
root.trackedPlayer = player
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -73,7 +72,6 @@ Singleton {
|
|||||||
onActivePlayerChanged: this.updateTrack()
|
onActivePlayerChanged: this.updateTrack()
|
||||||
|
|
||||||
function updateTrack() {
|
function updateTrack() {
|
||||||
console.log(`MPRIS Track Update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtist}`)
|
|
||||||
this.activeTrack = {
|
this.activeTrack = {
|
||||||
uniqueId: this.activePlayer?.uniqueId ?? 0,
|
uniqueId: this.activePlayer?.uniqueId ?? 0,
|
||||||
artUrl: this.activePlayer?.trackArtUrl ?? "",
|
artUrl: this.activePlayer?.trackArtUrl ?? "",
|
||||||
@@ -86,7 +84,7 @@ Singleton {
|
|||||||
this.__reverse = false
|
this.__reverse = false
|
||||||
}
|
}
|
||||||
|
|
||||||
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
|
property bool isPlaying: this.activePlayer && this.activePlayer.playbackState === MprisPlaybackState.Playing
|
||||||
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
|
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
|
||||||
function togglePlaying() {
|
function togglePlaying() {
|
||||||
if (this.canTogglePlaying) this.activePlayer.togglePlaying()
|
if (this.canTogglePlaying) this.activePlayer.togglePlaying()
|
||||||
@@ -128,7 +126,6 @@ Singleton {
|
|||||||
|
|
||||||
function setActivePlayer(player) {
|
function setActivePlayer(player) {
|
||||||
const targetPlayer = player ?? Mpris.players[0]
|
const targetPlayer = player ?? Mpris.players[0]
|
||||||
console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`)
|
|
||||||
|
|
||||||
if (targetPlayer && this.activePlayer) {
|
if (targetPlayer && this.activePlayer) {
|
||||||
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer)
|
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer)
|
||||||
@@ -139,22 +136,4 @@ Singleton {
|
|||||||
this.trackedPlayer = targetPlayer
|
this.trackedPlayer = targetPlayer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debug timer
|
|
||||||
Timer {
|
|
||||||
interval: 3000
|
|
||||||
running: true
|
|
||||||
repeat: true
|
|
||||||
onTriggered: {
|
|
||||||
if (activePlayer) {
|
|
||||||
console.log(` Track: ${activePlayer.trackTitle || 'Unknown'} by ${activePlayer.trackArtist || 'Unknown'}`)
|
|
||||||
console.log(` State: ${activePlayer.playbackState}`)
|
|
||||||
} else if (Mpris.players.length === 0) {
|
|
||||||
console.log(" No MPRIS players detected. Try:")
|
|
||||||
console.log(" - mpv --script-opts=mpris-title='{{media-title}}' file.mp3")
|
|
||||||
console.log(" - firefox/chromium (YouTube, Spotify Web)")
|
|
||||||
console.log(" - vlc file.mp3")
|
|
||||||
console.log(" Check available players: busctl --user list | grep mpris")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,18 @@ PanelWindow {
|
|||||||
|
|
||||||
visible: root.calendarVisible
|
visible: root.calendarVisible
|
||||||
|
|
||||||
|
// Timer to update MPRIS position like the example
|
||||||
|
Timer {
|
||||||
|
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (root.activePlayer) {
|
||||||
|
root.activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
implicitWidth: 320
|
implicitWidth: 320
|
||||||
implicitHeight: 400
|
implicitHeight: 400
|
||||||
|
|
||||||
@@ -162,13 +174,6 @@ PanelWindow {
|
|||||||
height: parent.height
|
height: parent.height
|
||||||
radius: parent.radius
|
radius: parent.radius
|
||||||
color: Theme.primary
|
color: Theme.primary
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation {
|
|
||||||
duration: 200
|
|
||||||
easing.type: Easing.OutQuad
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -180,11 +185,7 @@ PanelWindow {
|
|||||||
const ratio = mouse.x / width
|
const ratio = mouse.x / width
|
||||||
const newPosition = ratio * root.activePlayer.length
|
const newPosition = ratio * root.activePlayer.length
|
||||||
console.log("Seeking to position:", newPosition, "ratio:", ratio, "canSeek:", root.activePlayer.canSeek)
|
console.log("Seeking to position:", newPosition, "ratio:", ratio, "canSeek:", root.activePlayer.canSeek)
|
||||||
if (root.activePlayer.canSeek) {
|
root.activePlayer.setPosition(newPosition)
|
||||||
root.activePlayer.position = newPosition
|
|
||||||
} else {
|
|
||||||
console.log("Player does not support seeking")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,7 +215,16 @@ PanelWindow {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
cursorShape: Qt.PointingHandCursor
|
cursorShape: Qt.PointingHandCursor
|
||||||
onClicked: root.activePlayer?.previous()
|
onClicked: {
|
||||||
|
if (!root.activePlayer) return
|
||||||
|
|
||||||
|
// >8 s → jump to start, otherwise previous track
|
||||||
|
if (root.activePlayer.position > 8000000) {
|
||||||
|
root.activePlayer.setPosition(0)
|
||||||
|
} else {
|
||||||
|
root.activePlayer.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
174
Widgets/CenterCommandCenter/CalendarWidget.qml
Normal file
174
Widgets/CenterCommandCenter/CalendarWidget.qml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import "../../Common"
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: calendarWidget
|
||||||
|
|
||||||
|
property var theme: Theme
|
||||||
|
property date displayDate: new Date()
|
||||||
|
property date selectedDate: new Date()
|
||||||
|
|
||||||
|
spacing: theme.spacingM
|
||||||
|
|
||||||
|
// Month navigation header
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 40
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: prevMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "chevron_left"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize
|
||||||
|
color: theme.primary
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevMonthArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(displayDate)
|
||||||
|
newDate.setMonth(newDate.getMonth() - 1)
|
||||||
|
displayDate = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width - 80
|
||||||
|
height: 40
|
||||||
|
text: Qt.formatDate(displayDate, "MMMM yyyy")
|
||||||
|
font.pixelSize: theme.fontSizeLarge
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 40
|
||||||
|
height: 40
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: nextMonthArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "chevron_right"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize
|
||||||
|
color: theme.primary
|
||||||
|
font.weight: theme.iconFontWeight
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextMonthArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
let newDate = new Date(displayDate)
|
||||||
|
newDate.setMonth(newDate.getMonth() + 1)
|
||||||
|
displayDate = newDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Days of week header
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 32
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width / 7
|
||||||
|
height: 32
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: modelData
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6)
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar grid
|
||||||
|
Grid {
|
||||||
|
width: parent.width
|
||||||
|
height: 200 // Fixed height for calendar
|
||||||
|
columns: 7
|
||||||
|
rows: 6
|
||||||
|
|
||||||
|
property date firstDay: {
|
||||||
|
let date = new Date(displayDate.getFullYear(), displayDate.getMonth(), 1)
|
||||||
|
let dayOfWeek = date.getDay()
|
||||||
|
date.setDate(date.getDate() - dayOfWeek)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: 42
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width / 7
|
||||||
|
height: parent.height / 6
|
||||||
|
|
||||||
|
property date dayDate: {
|
||||||
|
let date = new Date(parent.firstDay)
|
||||||
|
date.setDate(date.getDate() + index)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
|
||||||
|
property bool isCurrentMonth: dayDate.getMonth() === displayDate.getMonth()
|
||||||
|
property bool isToday: dayDate.toDateString() === new Date().toDateString()
|
||||||
|
property bool isSelected: dayDate.toDateString() === selectedDate.toDateString()
|
||||||
|
|
||||||
|
color: isSelected ? theme.primary :
|
||||||
|
isToday ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
|
||||||
|
dayArea.containsMouse ? Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08) : "transparent"
|
||||||
|
|
||||||
|
radius: theme.cornerRadiusSmall
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: dayDate.getDate()
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
color: isSelected ? theme.surface :
|
||||||
|
isToday ? theme.primary :
|
||||||
|
isCurrentMonth ? theme.surfaceText :
|
||||||
|
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.4)
|
||||||
|
font.weight: isToday || isSelected ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: dayArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
selectedDate = dayDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
117
Widgets/CenterCommandCenter/CenterCommandCenter.qml
Normal file
117
Widgets/CenterCommandCenter/CenterCommandCenter.qml
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Widgets
|
||||||
|
import Quickshell.Wayland
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
PanelWindow {
|
||||||
|
id: centerCommandCenter
|
||||||
|
|
||||||
|
property var theme: Theme
|
||||||
|
property bool hasActiveMedia: root.hasActiveMedia
|
||||||
|
property var weather: root.weather
|
||||||
|
property bool useFahrenheit: false
|
||||||
|
|
||||||
|
// Prevent media player from disappearing during track changes
|
||||||
|
property bool showMediaPlayer: hasActiveMedia || hideMediaTimer.running
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: hideMediaTimer
|
||||||
|
interval: 3000 // 3 second grace period
|
||||||
|
running: false
|
||||||
|
repeat: false
|
||||||
|
}
|
||||||
|
|
||||||
|
onHasActiveMediaChanged: {
|
||||||
|
if (hasActiveMedia) {
|
||||||
|
hideMediaTimer.stop()
|
||||||
|
} else {
|
||||||
|
hideMediaTimer.start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
visible: root.calendarVisible
|
||||||
|
|
||||||
|
implicitWidth: 320
|
||||||
|
implicitHeight: 400
|
||||||
|
|
||||||
|
WlrLayershell.layer: WlrLayershell.Overlay
|
||||||
|
WlrLayershell.exclusiveZone: -1
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
anchors {
|
||||||
|
top: true
|
||||||
|
left: true
|
||||||
|
right: true
|
||||||
|
bottom: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 400
|
||||||
|
height: showMediaPlayer ? 540 : (weather?.available ? 480 : 400)
|
||||||
|
x: (parent.width - width) / 2
|
||||||
|
y: theme.barHeight + theme.spacingS
|
||||||
|
color: theme.surfaceContainer
|
||||||
|
radius: theme.cornerRadiusLarge
|
||||||
|
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.12)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
opacity: root.calendarVisible ? 1.0 : 0.0
|
||||||
|
scale: root.calendarVisible ? 1.0 : 0.85
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: theme.mediumDuration
|
||||||
|
easing.type: theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: theme.mediumDuration
|
||||||
|
easing.type: theme.emphasizedEasing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: theme.spacingL
|
||||||
|
spacing: theme.spacingM
|
||||||
|
|
||||||
|
// Media Player (when active)
|
||||||
|
MediaPlayerWidget {
|
||||||
|
visible: showMediaPlayer
|
||||||
|
theme: centerCommandCenter.theme
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather header (when available and no media)
|
||||||
|
WeatherWidget {
|
||||||
|
visible: weather?.available && !showMediaPlayer
|
||||||
|
theme: centerCommandCenter.theme
|
||||||
|
weather: centerCommandCenter.weather
|
||||||
|
useFahrenheit: centerCommandCenter.useFahrenheit
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calendar
|
||||||
|
CalendarWidget {
|
||||||
|
width: parent.width
|
||||||
|
height: showMediaPlayer ? parent.height - 200 : (weather?.available ? parent.height - 120 : parent.height - 40)
|
||||||
|
theme: centerCommandCenter.theme
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
z: -1
|
||||||
|
onClicked: {
|
||||||
|
root.calendarVisible = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
269
Widgets/CenterCommandCenter/MediaPlayerWidget.qml
Normal file
269
Widgets/CenterCommandCenter/MediaPlayerWidget.qml
Normal file
@@ -0,0 +1,269 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
import Quickshell
|
||||||
|
import Quickshell.Services.Mpris
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: mediaPlayerWidget
|
||||||
|
|
||||||
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
||||||
|
property var theme: Theme
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 160 // Reduced height to prevent overflow
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08)
|
||||||
|
border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
// Timer to update MPRIS position
|
||||||
|
property bool justSeeked: false
|
||||||
|
property real seekTargetPosition: 0
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: positionTimer
|
||||||
|
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !justSeeked
|
||||||
|
interval: 1000
|
||||||
|
repeat: true
|
||||||
|
onTriggered: {
|
||||||
|
if (activePlayer) {
|
||||||
|
activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timer to resume position updates after seeking
|
||||||
|
Timer {
|
||||||
|
id: seekCooldownTimer
|
||||||
|
interval: 1000 // Reduced from 2000
|
||||||
|
repeat: false
|
||||||
|
onTriggered: {
|
||||||
|
justSeeked = false
|
||||||
|
// Force position update after seek
|
||||||
|
if (activePlayer) {
|
||||||
|
activePlayer.positionChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: theme.spacingM
|
||||||
|
spacing: theme.spacingM
|
||||||
|
|
||||||
|
// Album art and track info
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
height: 70 // Reduced height
|
||||||
|
spacing: theme.spacingM
|
||||||
|
|
||||||
|
// Album Art
|
||||||
|
Rectangle {
|
||||||
|
width: 70
|
||||||
|
height: 70
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Image {
|
||||||
|
id: albumArt
|
||||||
|
anchors.fill: parent
|
||||||
|
source: activePlayer?.trackArtUrl || ""
|
||||||
|
fillMode: Image.PreserveAspectCrop
|
||||||
|
smooth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
visible: albumArt.status !== Image.Ready
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "album"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 28
|
||||||
|
color: theme.surfaceVariantText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track Info
|
||||||
|
Column {
|
||||||
|
width: parent.width - 70 - theme.spacingM
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackTitle || "Unknown Track"
|
||||||
|
font.pixelSize: theme.fontSizeMedium
|
||||||
|
font.weight: Font.Bold
|
||||||
|
color: theme.surfaceText
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackArtist || "Unknown Artist"
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.8)
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: activePlayer?.trackAlbum || ""
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.6)
|
||||||
|
width: parent.width
|
||||||
|
elide: Text.ElideRight
|
||||||
|
visible: text.length > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple progress bar - click to seek only
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 6
|
||||||
|
radius: 3
|
||||||
|
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: {
|
||||||
|
if (!activePlayer || !activePlayer.length || activePlayer.length === 0) return 0
|
||||||
|
|
||||||
|
// Use seek target position if we just seeked
|
||||||
|
const currentPos = justSeeked ? seekTargetPosition : activePlayer.position
|
||||||
|
return Math.max(0, Math.min(parent.width, parent.width * (currentPos / activePlayer.length)))
|
||||||
|
}
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
|
||||||
|
const ratio = mouse.x / width
|
||||||
|
const targetPosition = Math.floor(ratio * activePlayer.length)
|
||||||
|
const currentPosition = activePlayer.position || 0
|
||||||
|
const seekOffset = targetPosition - currentPosition
|
||||||
|
console.log("Simple seek - offset:", seekOffset, "target:", targetPosition, "current:", currentPosition)
|
||||||
|
|
||||||
|
// Store target position for visual feedback
|
||||||
|
seekTargetPosition = targetPosition
|
||||||
|
justSeeked = true
|
||||||
|
seekCooldownTimer.restart()
|
||||||
|
|
||||||
|
activePlayer.seek(seekOffset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Control buttons - compact to fit
|
||||||
|
Row {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
spacing: theme.spacingL
|
||||||
|
|
||||||
|
// Previous button
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: prevBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_previous"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
if (!activePlayer) return
|
||||||
|
|
||||||
|
// >8 s → jump to start, otherwise previous track
|
||||||
|
if (activePlayer.position > 8000000) {
|
||||||
|
console.log("Jumping to start - current position:", activePlayer.position)
|
||||||
|
|
||||||
|
// Store target position for visual feedback
|
||||||
|
seekTargetPosition = 0
|
||||||
|
justSeeked = true
|
||||||
|
seekCooldownTimer.restart()
|
||||||
|
|
||||||
|
// Seek to the beginning
|
||||||
|
activePlayer.seek(-activePlayer.position)
|
||||||
|
} else {
|
||||||
|
activePlayer.previous()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Play/Pause button
|
||||||
|
Rectangle {
|
||||||
|
width: 36
|
||||||
|
height: 36
|
||||||
|
radius: 18
|
||||||
|
color: theme.primary
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 20
|
||||||
|
color: theme.background
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.togglePlaying()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next button
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: nextBtnArea.containsMouse ? Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.12) : "transparent"
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "skip_next"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: 16
|
||||||
|
color: theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextBtnArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: activePlayer?.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
135
Widgets/CenterCommandCenter/WeatherWidget.qml
Normal file
135
Widgets/CenterCommandCenter/WeatherWidget.qml
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import "../../Common"
|
||||||
|
import "../../Services"
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: weatherWidget
|
||||||
|
|
||||||
|
property var theme: Theme
|
||||||
|
property var weather
|
||||||
|
property bool useFahrenheit: false
|
||||||
|
|
||||||
|
width: parent.width
|
||||||
|
height: 80
|
||||||
|
radius: theme.cornerRadius
|
||||||
|
color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.08)
|
||||||
|
border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2)
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: theme.spacingL
|
||||||
|
|
||||||
|
// Weather icon and temp
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: WeatherService.getWeatherIcon(weather.wCode)
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.iconSize + 4
|
||||||
|
color: theme.primary
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: (useFahrenheit ? weather.tempF : weather.temp) + "°" + (useFahrenheit ? "F" : "C")
|
||||||
|
font.pixelSize: theme.fontSizeLarge
|
||||||
|
color: theme.surfaceText
|
||||||
|
font.weight: Font.Bold
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: useFahrenheit = !useFahrenheit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: weather.city
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.7)
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Weather details grid
|
||||||
|
Grid {
|
||||||
|
columns: 2
|
||||||
|
spacing: theme.spacingS
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
Text {
|
||||||
|
text: "humidity_low"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: weather.humidity + "%"
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
Text {
|
||||||
|
text: "air"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: weather.wind
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
Text {
|
||||||
|
text: "wb_twilight"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: weather.sunrise
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
spacing: theme.spacingXS
|
||||||
|
Text {
|
||||||
|
text: "bedtime"
|
||||||
|
font.family: theme.iconFont
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
Text {
|
||||||
|
text: weather.sunset
|
||||||
|
font.pixelSize: theme.fontSizeSmall
|
||||||
|
color: theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
4
Widgets/CenterCommandCenter/qmldir
Normal file
4
Widgets/CenterCommandCenter/qmldir
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
CenterCommandCenter 1.0 CenterCommandCenter.qml
|
||||||
|
MediaPlayerWidget 1.0 MediaPlayerWidget.qml
|
||||||
|
WeatherWidget 1.0 WeatherWidget.qml
|
||||||
|
CalendarWidget 1.0 CalendarWidget.qml
|
||||||
@@ -828,232 +828,532 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Audio Tab
|
// Audio Tab
|
||||||
ScrollView {
|
Item {
|
||||||
|
id: audioTabContainer
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Theme.spacingM
|
anchors.margins: Theme.spacingM
|
||||||
visible: controlCenterPopup.currentTab === 1
|
visible: controlCenterPopup.currentTab === 1
|
||||||
clip: true
|
|
||||||
|
property int audioSubTab: 0 // 0: Output, 1: Input
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
width: parent.width
|
anchors.fill: parent
|
||||||
spacing: Theme.spacingL
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
// Volume Control
|
// Audio Sub-tabs
|
||||||
Column {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
height: 40
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
Text {
|
Rectangle {
|
||||||
text: "Volume"
|
width: parent.width / 2 - 1
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
height: parent.height
|
||||||
color: Theme.surfaceText
|
radius: Theme.cornerRadius
|
||||||
font.weight: Font.Medium
|
color: audioTabContainer.audioSubTab === 0 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
}
|
|
||||||
|
|
||||||
Row {
|
|
||||||
width: parent.width
|
|
||||||
spacing: Theme.spacingM
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "volume_down"
|
anchors.centerIn: parent
|
||||||
font.family: Theme.iconFont
|
text: "Output"
|
||||||
font.pixelSize: Theme.iconSize
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: audioTabContainer.audioSubTab === 0 ? Theme.primaryText : Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
MouseArea {
|
||||||
id: volumeSliderTrack
|
anchors.fill: parent
|
||||||
width: parent.width - 80
|
hoverEnabled: true
|
||||||
height: 8
|
cursorShape: Qt.PointingHandCursor
|
||||||
radius: 4
|
onClicked: audioTabContainer.audioSubTab = 0
|
||||||
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
id: volumeSliderFill
|
|
||||||
width: parent.width * (root.volumeLevel / 100)
|
|
||||||
height: parent.height
|
|
||||||
radius: parent.radius
|
|
||||||
color: Theme.primary
|
|
||||||
|
|
||||||
Behavior on width {
|
|
||||||
NumberAnimation { duration: 100 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draggable handle
|
|
||||||
Rectangle {
|
|
||||||
id: volumeHandle
|
|
||||||
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, volumeSliderFill.width - width/2))
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation { duration: 150 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MouseArea {
|
|
||||||
id: volumeMouseArea
|
|
||||||
anchors.fill: parent
|
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: (mouse) => {
|
|
||||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
|
||||||
let newVolume = Math.round(ratio * 100)
|
|
||||||
AudioService.setVolume(newVolume)
|
|
||||||
}
|
|
||||||
|
|
||||||
onPositionChanged: (mouse) => {
|
|
||||||
if (pressed) {
|
|
||||||
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
|
||||||
let newVolume = Math.round(ratio * 100)
|
|
||||||
AudioService.setVolume(newVolume)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "volume_up"
|
|
||||||
font.family: Theme.iconFont
|
|
||||||
font.pixelSize: Theme.iconSize
|
|
||||||
color: Theme.surfaceText
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Rectangle {
|
||||||
text: root.volumeLevel + "%"
|
width: parent.width / 2 - 1
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
height: parent.height
|
||||||
color: Theme.surfaceText
|
radius: Theme.cornerRadius
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
color: audioTabContainer.audioSubTab === 1 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: "Input"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: audioTabContainer.audioSubTab === 1 ? Theme.primaryText : Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: audioTabContainer.audioSubTab = 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Output Devices
|
// Output Tab Content
|
||||||
Column {
|
ScrollView {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingM
|
height: parent.height - 48
|
||||||
|
visible: audioTabContainer.audioSubTab === 0
|
||||||
|
clip: true
|
||||||
|
|
||||||
Text {
|
Column {
|
||||||
text: "Output Device"
|
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
|
||||||
color: Theme.surfaceText
|
|
||||||
font.weight: Font.Medium
|
|
||||||
}
|
|
||||||
|
|
||||||
// Current device indicator
|
|
||||||
Rectangle {
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: 35
|
spacing: Theme.spacingL
|
||||||
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: root.currentAudioSink !== ""
|
|
||||||
|
|
||||||
Row {
|
// Volume Control
|
||||||
anchors.left: parent.left
|
Column {
|
||||||
anchors.leftMargin: Theme.spacingM
|
width: parent.width
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
spacing: Theme.spacingM
|
||||||
spacing: Theme.spacingS
|
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "check_circle"
|
text: "Volume"
|
||||||
font.family: Theme.iconFont
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
font.pixelSize: Theme.iconSize - 4
|
color: Theme.surfaceText
|
||||||
color: Theme.primary
|
|
||||||
}
|
|
||||||
|
|
||||||
Text {
|
|
||||||
text: "Current: " + (function() {
|
|
||||||
for (let sink of root.audioSinks) {
|
|
||||||
if (sink.name === root.currentAudioSink) {
|
|
||||||
return sink.displayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return root.currentAudioSink
|
|
||||||
})()
|
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
|
||||||
color: Theme.primary
|
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Real audio devices
|
|
||||||
Repeater {
|
|
||||||
model: root.audioSinks
|
|
||||||
|
|
||||||
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.active ? 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.active ? Theme.primary : "transparent"
|
|
||||||
border.width: 1
|
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
anchors.left: parent.left
|
width: parent.width
|
||||||
anchors.leftMargin: Theme.spacingM
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
spacing: Theme.spacingM
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: {
|
text: "volume_down"
|
||||||
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"
|
|
||||||
}
|
|
||||||
font.family: Theme.iconFont
|
font.family: Theme.iconFont
|
||||||
font.pixelSize: Theme.iconSize
|
font.pixelSize: Theme.iconSize
|
||||||
color: modelData.active ? Theme.primary : Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Rectangle {
|
||||||
spacing: 2
|
id: volumeSliderTrack
|
||||||
|
width: parent.width - 80
|
||||||
|
height: 8
|
||||||
|
radius: 4
|
||||||
|
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3)
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: volumeSliderFill
|
||||||
|
width: parent.width * (root.volumeLevel / 100)
|
||||||
|
height: parent.height
|
||||||
|
radius: parent.radius
|
||||||
|
color: Theme.primary
|
||||||
|
|
||||||
|
Behavior on width {
|
||||||
|
NumberAnimation { duration: 100 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draggable handle
|
||||||
|
Rectangle {
|
||||||
|
id: volumeHandle
|
||||||
|
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, volumeSliderFill.width - width/2))
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
scale: volumeMouseArea.containsMouse || volumeMouseArea.pressed ? 1.2 : 1.0
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: volumeMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||||
|
let newVolume = Math.round(ratio * 100)
|
||||||
|
AudioService.setVolume(newVolume)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: (mouse) => {
|
||||||
|
if (pressed) {
|
||||||
|
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||||
|
let newVolume = Math.round(ratio * 100)
|
||||||
|
AudioService.setVolume(newVolume)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "volume_up"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.volumeLevel + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Output Devices
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Output Device"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current device indicator
|
||||||
|
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: root.currentAudioSink !== ""
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.displayName
|
text: "check_circle"
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.family: Theme.iconFont
|
||||||
color: modelData.active ? Theme.primary : Theme.surfaceText
|
font.pixelSize: Theme.iconSize - 4
|
||||||
font.weight: modelData.active ? Font.Medium : Font.Normal
|
color: Theme.primary
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: modelData.active ? "Selected" : ""
|
text: "Current: " + (function() {
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
for (let sink of root.audioSinks) {
|
||||||
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
if (sink.name === root.currentAudioSink) {
|
||||||
visible: modelData.active
|
return sink.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root.currentAudioSink
|
||||||
|
})()
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
// Real audio devices
|
||||||
id: deviceArea
|
Repeater {
|
||||||
anchors.fill: parent
|
model: root.audioSinks
|
||||||
hoverEnabled: true
|
|
||||||
cursorShape: Qt.PointingHandCursor
|
|
||||||
|
|
||||||
onClicked: {
|
Rectangle {
|
||||||
console.log("Clicked audio device:", JSON.stringify(modelData))
|
width: parent.width
|
||||||
console.log("Device name to set:", modelData.name)
|
height: 50
|
||||||
AudioService.setAudioSink(modelData.name)
|
radius: Theme.cornerRadius
|
||||||
|
color: deviceArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) :
|
||||||
|
(modelData.active ? 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.active ? Theme.primary : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: modelData.active ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.displayName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: modelData.active ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: modelData.active ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.active ? "Selected" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||||
|
visible: modelData.active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: deviceArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
console.log("Clicked audio device:", JSON.stringify(modelData))
|
||||||
|
console.log("Device name to set:", modelData.name)
|
||||||
|
AudioService.setAudioSink(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input Tab Content
|
||||||
|
ScrollView {
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height - 48
|
||||||
|
visible: audioTabContainer.audioSubTab === 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingL
|
||||||
|
|
||||||
|
// Microphone Level Control
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Microphone Level"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "mic"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: micSliderTrack
|
||||||
|
width: parent.width - 80
|
||||||
|
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: 100 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draggable handle
|
||||||
|
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.0
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation { duration: 150 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: micMouseArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: (mouse) => {
|
||||||
|
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||||
|
let newMicLevel = Math.round(ratio * 100)
|
||||||
|
AudioService.setMicLevel(newMicLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
onPositionChanged: (mouse) => {
|
||||||
|
if (pressed) {
|
||||||
|
let ratio = Math.max(0, Math.min(1, mouse.x / width))
|
||||||
|
let newMicLevel = Math.round(ratio * 100)
|
||||||
|
AudioService.setMicLevel(newMicLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "mic"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.micLevel + "%"
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.surfaceText
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Input Devices
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Input Device"
|
||||||
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
|
color: Theme.surfaceText
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current device indicator
|
||||||
|
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: root.currentAudioSource !== ""
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "check_circle"
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize - 4
|
||||||
|
color: Theme.primary
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: "Current: " + (function() {
|
||||||
|
for (let source of root.audioSources) {
|
||||||
|
if (source.name === root.currentAudioSource) {
|
||||||
|
return source.displayName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return root.currentAudioSource
|
||||||
|
})()
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: Theme.primary
|
||||||
|
font.weight: Font.Medium
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real audio input devices
|
||||||
|
Repeater {
|
||||||
|
model: root.audioSources
|
||||||
|
|
||||||
|
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.active ? 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.active ? Theme.primary : "transparent"
|
||||||
|
border.width: 1
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: Theme.spacingM
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingM
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: {
|
||||||
|
if (modelData.name.includes("bluez")) return "headset_mic"
|
||||||
|
else if (modelData.name.includes("usb")) return "headset_mic"
|
||||||
|
else return "mic"
|
||||||
|
}
|
||||||
|
font.family: Theme.iconFont
|
||||||
|
font.pixelSize: Theme.iconSize
|
||||||
|
color: modelData.active ? Theme.primary : Theme.surfaceText
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
spacing: 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.displayName
|
||||||
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
|
color: modelData.active ? Theme.primary : Theme.surfaceText
|
||||||
|
font.weight: modelData.active ? Font.Medium : Font.Normal
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: modelData.active ? "Selected" : ""
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.8)
|
||||||
|
visible: modelData.active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: sourceArea
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
console.log("Clicked audio source:", JSON.stringify(modelData))
|
||||||
|
console.log("Source name to set:", modelData.name)
|
||||||
|
AudioService.setAudioSource(modelData.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import QtQuick
|
|
||||||
import QtQuick.Controls
|
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Widgets
|
|
||||||
import Quickshell.Wayland
|
|
||||||
import Quickshell.Services.SystemTray
|
|
||||||
|
|
||||||
PanelWindow {
|
|
||||||
id: topBar
|
|
||||||
|
|
||||||
property var theme
|
|
||||||
property var root
|
|
||||||
|
|
||||||
anchors {
|
|
||||||
top: true
|
|
||||||
left: true
|
|
||||||
right: true
|
|
||||||
}
|
|
||||||
|
|
||||||
WlrLayershell.topMargin: 8
|
|
||||||
WlrLayershell.bottomMargin: 8
|
|
||||||
WlrLayershell.leftMargin: 16
|
|
||||||
WlrLayershell.rightMargin: 16
|
|
||||||
|
|
||||||
implicitHeight: theme.barHeight - 4
|
|
||||||
color: "transparent"
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.margins: 2
|
|
||||||
anchors.topMargin: 6
|
|
||||||
anchors.bottomMargin: 2
|
|
||||||
anchors.leftMargin: 8
|
|
||||||
anchors.rightMargin: 8
|
|
||||||
radius: theme.cornerRadiusXLarge
|
|
||||||
color: Qt.rgba(theme.surfaceContainer.r, theme.surfaceContainer.g, theme.surfaceContainer.b, 0.75)
|
|
||||||
|
|
||||||
// Material 3 elevation shadow
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: DropShadow {
|
|
||||||
horizontalOffset: 0
|
|
||||||
verticalOffset: 4
|
|
||||||
radius: 16
|
|
||||||
samples: 33
|
|
||||||
color: Qt.rgba(0, 0, 0, 0.15)
|
|
||||||
transparentBorder: true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle border for definition
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: "transparent"
|
|
||||||
border.color: Qt.rgba(theme.outline.r, theme.outline.g, theme.outline.b, 0.08)
|
|
||||||
border.width: 1
|
|
||||||
radius: parent.radius
|
|
||||||
}
|
|
||||||
|
|
||||||
// Subtle surface tint overlay with animation
|
|
||||||
Rectangle {
|
|
||||||
anchors.fill: parent
|
|
||||||
color: Qt.rgba(theme.surfaceTint.r, theme.surfaceTint.g, theme.surfaceTint.b, 0.04)
|
|
||||||
radius: parent.radius
|
|
||||||
|
|
||||||
SequentialAnimation on opacity {
|
|
||||||
running: true
|
|
||||||
loops: Animation.Infinite
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.08
|
|
||||||
duration: theme.extraLongDuration
|
|
||||||
easing.type: theme.standardEasing
|
|
||||||
}
|
|
||||||
NumberAnimation {
|
|
||||||
to: 0.02
|
|
||||||
duration: theme.extraLongDuration
|
|
||||||
easing.type: theme.standardEasing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Item {
|
|
||||||
anchors.fill: parent
|
|
||||||
anchors.leftMargin: theme.spacingL
|
|
||||||
anchors.rightMargin: theme.spacingL
|
|
||||||
anchors.topMargin: theme.spacingXS
|
|
||||||
anchors.bottomMargin: theme.spacingXS
|
|
||||||
|
|
||||||
// Left section - Apps and Workspace Switcher
|
|
||||||
Row {
|
|
||||||
id: leftSection
|
|
||||||
height: parent.height
|
|
||||||
spacing: theme.spacingL
|
|
||||||
anchors.left: parent.left
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
AppLauncherButton {
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
}
|
|
||||||
|
|
||||||
WorkspaceSwitcher {
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Center section - Clock/Media Player
|
|
||||||
ClockWidget {
|
|
||||||
id: clockWidget
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
anchors.centerIn: parent
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right section - System controls
|
|
||||||
Row {
|
|
||||||
id: rightSection
|
|
||||||
height: parent.height
|
|
||||||
spacing: theme.spacingXS
|
|
||||||
anchors.right: parent.right
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
|
||||||
|
|
||||||
ClipboardButton {
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
}
|
|
||||||
|
|
||||||
ColorPickerButton {
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
}
|
|
||||||
|
|
||||||
NotificationButton {
|
|
||||||
theme: topBar.theme
|
|
||||||
root: topBar.root
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
TopBar 1.0 TopBar.qml
|
TopBar 1.0 TopBar.qml
|
||||||
TopBarSimple 1.0 TopBarSimple.qml
|
|
||||||
AppLauncherButton 1.0 AppLauncherButton.qml
|
AppLauncherButton 1.0 AppLauncherButton.qml
|
||||||
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
|
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
|
||||||
ClockWidget 1.0 ClockWidget.qml
|
ClockWidget 1.0 ClockWidget.qml
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import Quickshell.Services.Notifications
|
|||||||
import Quickshell.Services.Mpris
|
import Quickshell.Services.Mpris
|
||||||
import "Services"
|
import "Services"
|
||||||
import "Widgets"
|
import "Widgets"
|
||||||
|
import "Widgets/CenterCommandCenter"
|
||||||
import "Common"
|
import "Common"
|
||||||
import "Common/Utilities.js" as Utils
|
import "Common/Utilities.js" as Utils
|
||||||
|
|
||||||
@@ -65,6 +66,11 @@ ShellRoot {
|
|||||||
property var audioSinks: AudioService.audioSinks
|
property var audioSinks: AudioService.audioSinks
|
||||||
property string currentAudioSink: AudioService.currentAudioSink
|
property string currentAudioSink: AudioService.currentAudioSink
|
||||||
|
|
||||||
|
// Microphone properties from AudioService
|
||||||
|
property int micLevel: AudioService.micLevel
|
||||||
|
property var audioSources: AudioService.audioSources
|
||||||
|
property string currentAudioSource: AudioService.currentAudioSource
|
||||||
|
|
||||||
// Bluetooth properties from BluetoothService
|
// Bluetooth properties from BluetoothService
|
||||||
property var bluetoothDevices: BluetoothService.bluetoothDevices
|
property var bluetoothDevices: BluetoothService.bluetoothDevices
|
||||||
|
|
||||||
@@ -280,7 +286,7 @@ ShellRoot {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Global popup windows
|
// Global popup windows
|
||||||
CalendarPopup {}
|
CenterCommandCenter {}
|
||||||
TrayMenuPopup {}
|
TrayMenuPopup {}
|
||||||
NotificationPopup {}
|
NotificationPopup {}
|
||||||
NotificationHistoryPopup {}
|
NotificationHistoryPopup {}
|
||||||
|
|||||||
Reference in New Issue
Block a user