From d169f5d4a3e3ca6c2499ad407383ece38b0b3ce9 Mon Sep 17 00:00:00 2001 From: bbedward Date: Fri, 11 Jul 2025 13:50:10 -0400 Subject: [PATCH] iBluetooth repair, input device support, some media player improvements --- MediaPlayer.qml | 420 ----------- Services/AudioService.qml | 153 +++- Services/BluetoothService.qml | 221 ++++-- Services/MprisController.qml | 27 +- ...ndarPopup.qml => CalendarPopup.qml.backup} | 36 +- .../CenterCommandCenter/CalendarWidget.qml | 174 +++++ .../CenterCommandCenter.qml | 117 ++++ .../CenterCommandCenter/MediaPlayerWidget.qml | 269 +++++++ Widgets/CenterCommandCenter/WeatherWidget.qml | 135 ++++ Widgets/CenterCommandCenter/qmldir | 4 + Widgets/ControlCenterPopup.qml | 658 +++++++++++++----- Widgets/TopBarSimple.qml | 139 ---- Widgets/qmldir | 1 - shell.qml | 8 +- 14 files changed, 1533 insertions(+), 829 deletions(-) delete mode 100644 MediaPlayer.qml rename Widgets/{CalendarPopup.qml => CalendarPopup.qml.backup} (96%) create mode 100644 Widgets/CenterCommandCenter/CalendarWidget.qml create mode 100644 Widgets/CenterCommandCenter/CenterCommandCenter.qml create mode 100644 Widgets/CenterCommandCenter/MediaPlayerWidget.qml create mode 100644 Widgets/CenterCommandCenter/WeatherWidget.qml create mode 100644 Widgets/CenterCommandCenter/qmldir delete mode 100644 Widgets/TopBarSimple.qml diff --git a/MediaPlayer.qml b/MediaPlayer.qml deleted file mode 100644 index 49a0cdc6..00000000 --- a/MediaPlayer.qml +++ /dev/null @@ -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() - } - } -} \ No newline at end of file diff --git a/Services/AudioService.qml b/Services/AudioService.qml index d5882f31..ee697495 100644 --- a/Services/AudioService.qml +++ b/Services/AudioService.qml @@ -11,6 +11,11 @@ Singleton { property var audioSinks: [] property string currentAudioSink: "" + // Microphone properties + property int micLevel: 50 + property var audioSources: [] + property string currentAudioSource: "" + // Real Audio Control Process { 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 { id: audioSinkLister command: ["pactl", "list", "sinks"] @@ -35,7 +56,6 @@ Singleton { stdout: StdioCollector { onStreamFinished: { if (text.trim()) { - console.log("Parsing pactl sink output...") let sinks = [] let lines = text.trim().split('\n') @@ -88,7 +108,6 @@ Singleton { sink.displayName = displayName } - console.log("Final audio sinks:", JSON.stringify(sinks, null, 2)) root.audioSinks = sinks 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 { id: defaultSinkChecker command: ["pactl", "get-default-sink"] @@ -106,7 +180,6 @@ Singleton { onRead: (data) => { if (data.trim()) { root.currentAudioSink = data.trim() - console.log("Default audio sink:", root.currentAudioSink) // Update active status in audioSinks 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) { let volumeSetProcess = Qt.createQmlObject(' import Quickshell.Io @@ -135,6 +236,17 @@ Singleton { ', 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) { 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 + } + } } \ No newline at end of file diff --git a/Services/BluetoothService.qml b/Services/BluetoothService.qml index 94513135..bf0b0d3b 100644 --- a/Services/BluetoothService.qml +++ b/Services/BluetoothService.qml @@ -17,14 +17,13 @@ Singleton { // Real Bluetooth Management Process { id: bluetoothStatusChecker - command: ["bluetoothctl", "show"] + command: ["bluetoothctl", "show"] // Use default controller running: true stdout: StdioCollector { onStreamFinished: { root.bluetoothAvailable = text.trim() !== "" && !text.includes("No default controller") root.bluetoothEnabled = text.includes("Powered: yes") - console.log("Bluetooth available:", root.bluetoothAvailable, "enabled:", root.bluetoothEnabled) if (root.bluetoothEnabled && root.bluetoothAvailable) { bluetoothDeviceScanner.running = true @@ -37,7 +36,7 @@ Singleton { Process { 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 stdout: StdioCollector { @@ -52,7 +51,7 @@ Singleton { if (parts.length >= 3) { let mac = parts[0].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 // Skip if name is still a technical path @@ -82,7 +81,6 @@ Singleton { } root.bluetoothDevices = devices - console.log("Found", devices.length, "Bluetooth devices") } } } @@ -95,23 +93,12 @@ Singleton { } function startDiscovery() { - console.log("Starting Bluetooth discovery...") - let discoveryProcess = Qt.createQmlObject(' - import Quickshell.Io - Process { - command: ["bluetoothctl", "scan", "on"] - running: true - onExited: { - root.scanning = true - // Scan for 10 seconds then get discovered devices - discoveryScanTimer.start() - } - } - ', root) + root.scanning = true + // Run comprehensive scan that gets all devices + discoveryScanner.running = true } function stopDiscovery() { - console.log("Stopping Bluetooth discovery...") let stopDiscoveryProcess = Qt.createQmlObject(' import Quickshell.Io Process { @@ -180,7 +167,6 @@ Singleton { } function toggleBluetoothDevice(mac) { - console.log("Toggling Bluetooth device:", mac) let device = root.bluetoothDevices.find(d => d.mac === mac) if (device) { let action = device.connected ? "disconnect" : "connect" @@ -207,26 +193,68 @@ Singleton { ', root) } - // Timer for discovery scanning + // Timer to refresh adapter & device state Timer { - id: discoveryScanTimer - interval: 8000 // 8 seconds - repeat: false + interval: 3000 // 3s refresh for more responsive updates + running: true; repeat: true 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 { 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 stdout: StdioCollector { onStreamFinished: { + + let devices = [] if (text.trim()) { - let devices = [] let lines = text.trim().split('\n') for (let line of lines) { @@ -235,16 +263,15 @@ Singleton { if (parts.length >= 4) { let mac = parts[0].trim() let name = parts[1].trim() - let paired = parts[2].trim() === 'true' - let connected = parts[3].trim() === 'true' - let rssi = parts[4] ? parseInt(parts[4]) : 0 + let paired = parts[2].trim() === 'yes' + let connected = parts[3].trim() === 'yes' - // Skip if name is still a technical path - if (name.startsWith('/org/bluez') || name.includes('hci0')) { + // Skip technical names + if (name.startsWith('/org/bluez') || name.includes('hci0') || name.length < 3) { continue } - // Determine device type from name + // Determine device type 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" @@ -255,32 +282,128 @@ Singleton { else if (nameLower.includes("speaker")) type = "speaker" 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({ mac: mac, name: name, type: type, paired: paired, connected: connected, - rssi: rssi, - signalStrength: signalStrength, - canPair: !paired + rssi: 0, + signalStrength: "unknown", + 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 + } } } diff --git a/Services/MprisController.qml b/Services/MprisController.qml index 966e0564..9e9c6c73 100644 --- a/Services/MprisController.qml +++ b/Services/MprisController.qml @@ -27,16 +27,15 @@ Singleton { target: modelData Component.onCompleted: { - console.log("MPRIS Player connected:", modelData.identity) if (root.trackedPlayer == null || modelData.isPlaying) { root.trackedPlayer = modelData } } 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) { - if (player.playbackState.isPlaying) { + if (player.playbackState === MprisPlaybackState.Playing) { root.trackedPlayer = player break } @@ -73,7 +72,6 @@ Singleton { onActivePlayerChanged: this.updateTrack() function updateTrack() { - console.log(`MPRIS Track Update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtist}`) this.activeTrack = { uniqueId: this.activePlayer?.uniqueId ?? 0, artUrl: this.activePlayer?.trackArtUrl ?? "", @@ -86,7 +84,7 @@ Singleton { 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 function togglePlaying() { if (this.canTogglePlaying) this.activePlayer.togglePlaying() @@ -128,7 +126,6 @@ Singleton { function setActivePlayer(player) { const targetPlayer = player ?? Mpris.players[0] - console.log(`[Mpris] Active player ${targetPlayer} << ${activePlayer}`) if (targetPlayer && this.activePlayer) { this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer) @@ -139,22 +136,4 @@ Singleton { 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") - } - } - } } \ No newline at end of file diff --git a/Widgets/CalendarPopup.qml b/Widgets/CalendarPopup.qml.backup similarity index 96% rename from Widgets/CalendarPopup.qml rename to Widgets/CalendarPopup.qml.backup index 0adbcde9..609343e6 100644 --- a/Widgets/CalendarPopup.qml +++ b/Widgets/CalendarPopup.qml.backup @@ -13,6 +13,18 @@ PanelWindow { 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 implicitHeight: 400 @@ -162,13 +174,6 @@ PanelWindow { height: parent.height radius: parent.radius color: Theme.primary - - Behavior on width { - NumberAnimation { - duration: 200 - easing.type: Easing.OutQuad - } - } } MouseArea { @@ -180,11 +185,7 @@ PanelWindow { const ratio = mouse.x / width const newPosition = ratio * root.activePlayer.length console.log("Seeking to position:", newPosition, "ratio:", ratio, "canSeek:", root.activePlayer.canSeek) - if (root.activePlayer.canSeek) { - root.activePlayer.position = newPosition - } else { - console.log("Player does not support seeking") - } + root.activePlayer.setPosition(newPosition) } } } @@ -214,7 +215,16 @@ PanelWindow { anchors.fill: parent hoverEnabled: true 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() + } + } } } diff --git a/Widgets/CenterCommandCenter/CalendarWidget.qml b/Widgets/CenterCommandCenter/CalendarWidget.qml new file mode 100644 index 00000000..642a8eda --- /dev/null +++ b/Widgets/CenterCommandCenter/CalendarWidget.qml @@ -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 + } + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/CenterCommandCenter/CenterCommandCenter.qml b/Widgets/CenterCommandCenter/CenterCommandCenter.qml new file mode 100644 index 00000000..0ebc7a94 --- /dev/null +++ b/Widgets/CenterCommandCenter/CenterCommandCenter.qml @@ -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 + } + } +} \ No newline at end of file diff --git a/Widgets/CenterCommandCenter/MediaPlayerWidget.qml b/Widgets/CenterCommandCenter/MediaPlayerWidget.qml new file mode 100644 index 00000000..d6ccdbd0 --- /dev/null +++ b/Widgets/CenterCommandCenter/MediaPlayerWidget.qml @@ -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() + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/CenterCommandCenter/WeatherWidget.qml b/Widgets/CenterCommandCenter/WeatherWidget.qml new file mode 100644 index 00000000..dfaa44fb --- /dev/null +++ b/Widgets/CenterCommandCenter/WeatherWidget.qml @@ -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 + } + } + } + } +} \ No newline at end of file diff --git a/Widgets/CenterCommandCenter/qmldir b/Widgets/CenterCommandCenter/qmldir new file mode 100644 index 00000000..3b33776f --- /dev/null +++ b/Widgets/CenterCommandCenter/qmldir @@ -0,0 +1,4 @@ +CenterCommandCenter 1.0 CenterCommandCenter.qml +MediaPlayerWidget 1.0 MediaPlayerWidget.qml +WeatherWidget 1.0 WeatherWidget.qml +CalendarWidget 1.0 CalendarWidget.qml \ No newline at end of file diff --git a/Widgets/ControlCenterPopup.qml b/Widgets/ControlCenterPopup.qml index 0aa8256a..5d288afe 100644 --- a/Widgets/ControlCenterPopup.qml +++ b/Widgets/ControlCenterPopup.qml @@ -828,232 +828,532 @@ PanelWindow { } // Audio Tab - ScrollView { + Item { + id: audioTabContainer anchors.fill: parent anchors.margins: Theme.spacingM visible: controlCenterPopup.currentTab === 1 - clip: true + + property int audioSubTab: 0 // 0: Output, 1: Input Column { - width: parent.width - spacing: Theme.spacingL + anchors.fill: parent + spacing: Theme.spacingM - // Volume Control - Column { + // Audio Sub-tabs + Row { width: parent.width - spacing: Theme.spacingM + height: 40 + spacing: 2 - Text { - text: "Volume" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - Row { - width: parent.width - spacing: Theme.spacingM + Rectangle { + width: parent.width / 2 - 1 + height: parent.height + radius: Theme.cornerRadius + color: audioTabContainer.audioSubTab === 0 ? Theme.primary : Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) Text { - text: "volume_down" - font.family: Theme.iconFont - font.pixelSize: Theme.iconSize - color: Theme.surfaceText - anchors.verticalCenter: parent.verticalCenter + anchors.centerIn: parent + text: "Output" + font.pixelSize: Theme.fontSizeMedium + color: audioTabContainer.audioSubTab === 0 ? Theme.primaryText : Theme.surfaceText + font.weight: Font.Medium } - Rectangle { - 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 - - 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 + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: audioTabContainer.audioSubTab = 0 } } - Text { - text: root.volumeLevel + "%" - font.pixelSize: Theme.fontSizeMedium - color: Theme.surfaceText - anchors.horizontalCenter: parent.horizontalCenter + Rectangle { + width: parent.width / 2 - 1 + height: parent.height + radius: Theme.cornerRadius + 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 - Column { + // Output Tab Content + ScrollView { width: parent.width - spacing: Theme.spacingM + height: parent.height - 48 + visible: audioTabContainer.audioSubTab === 0 + clip: true - Text { - text: "Output Device" - font.pixelSize: Theme.fontSizeLarge - color: Theme.surfaceText - font.weight: Font.Medium - } - - // Current device indicator - Rectangle { + Column { 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 !== "" + spacing: Theme.spacingL - Row { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter - spacing: Theme.spacingS + // Volume Control + Column { + width: parent.width + spacing: Theme.spacingM Text { - text: "check_circle" - font.family: Theme.iconFont - font.pixelSize: Theme.iconSize - 4 - 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 + text: "Volume" + font.pixelSize: Theme.fontSizeLarge + color: Theme.surfaceText 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 { - anchors.left: parent.left - anchors.leftMargin: Theme.spacingM - anchors.verticalCenter: parent.verticalCenter + width: parent.width 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" - } + text: "volume_down" font.family: Theme.iconFont font.pixelSize: Theme.iconSize - color: modelData.active ? Theme.primary : Theme.surfaceText + color: Theme.surfaceText anchors.verticalCenter: parent.verticalCenter } - Column { - spacing: 2 + Rectangle { + 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 + 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: modelData.displayName - font.pixelSize: Theme.fontSizeMedium - color: modelData.active ? Theme.primary : Theme.surfaceText - font.weight: modelData.active ? Font.Medium : Font.Normal + text: "check_circle" + font.family: Theme.iconFont + font.pixelSize: Theme.iconSize - 4 + color: Theme.primary } 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 + 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 } } } - MouseArea { - id: deviceArea - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + // Real audio devices + Repeater { + model: root.audioSinks - onClicked: { - console.log("Clicked audio device:", JSON.stringify(modelData)) - console.log("Device name to set:", modelData.name) - AudioService.setAudioSink(modelData.name) + 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 { + 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) + } + } } } } diff --git a/Widgets/TopBarSimple.qml b/Widgets/TopBarSimple.qml deleted file mode 100644 index 49566905..00000000 --- a/Widgets/TopBarSimple.qml +++ /dev/null @@ -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 - } - } - } -} \ No newline at end of file diff --git a/Widgets/qmldir b/Widgets/qmldir index 810944cf..df52ed61 100644 --- a/Widgets/qmldir +++ b/Widgets/qmldir @@ -1,5 +1,4 @@ TopBar 1.0 TopBar.qml -TopBarSimple 1.0 TopBarSimple.qml AppLauncherButton 1.0 AppLauncherButton.qml WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml ClockWidget 1.0 ClockWidget.qml diff --git a/shell.qml b/shell.qml index 22395491..381204b2 100644 --- a/shell.qml +++ b/shell.qml @@ -12,6 +12,7 @@ import Quickshell.Services.Notifications import Quickshell.Services.Mpris import "Services" import "Widgets" +import "Widgets/CenterCommandCenter" import "Common" import "Common/Utilities.js" as Utils @@ -65,6 +66,11 @@ ShellRoot { property var audioSinks: AudioService.audioSinks 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 property var bluetoothDevices: BluetoothService.bluetoothDevices @@ -280,7 +286,7 @@ ShellRoot { } // Global popup windows - CalendarPopup {} + CenterCommandCenter {} TrayMenuPopup {} NotificationPopup {} NotificationHistoryPopup {}