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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
// 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: {
|
||||
if (text.trim()) {
|
||||
|
||||
let devices = []
|
||||
if (text.trim()) {
|
||||
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,33 +282,129 @@ 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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,10 +828,74 @@ PanelWindow {
|
||||
}
|
||||
|
||||
// Audio Tab
|
||||
ScrollView {
|
||||
Item {
|
||||
id: audioTabContainer
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingM
|
||||
visible: controlCenterPopup.currentTab === 1
|
||||
|
||||
property int audioSubTab: 0 // 0: Output, 1: Input
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: Theme.spacingM
|
||||
|
||||
// Audio Sub-tabs
|
||||
Row {
|
||||
width: parent.width
|
||||
height: 40
|
||||
spacing: 2
|
||||
|
||||
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 {
|
||||
anchors.centerIn: parent
|
||||
text: "Output"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: audioTabContainer.audioSubTab === 0 ? Theme.primaryText : Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: audioTabContainer.audioSubTab = 0
|
||||
}
|
||||
}
|
||||
|
||||
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 Tab Content
|
||||
ScrollView {
|
||||
width: parent.width
|
||||
height: parent.height - 48
|
||||
visible: audioTabContainer.audioSubTab === 0
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
@@ -1062,6 +1126,242 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bluetooth Tab
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
@@ -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
|
||||
TopBarSimple 1.0 TopBarSimple.qml
|
||||
AppLauncherButton 1.0 AppLauncherButton.qml
|
||||
WorkspaceSwitcher 1.0 WorkspaceSwitcher.qml
|
||||
ClockWidget 1.0 ClockWidget.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 {}
|
||||
|
||||
Reference in New Issue
Block a user