1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-29 07:52:50 -05:00

Media fixes

This commit is contained in:
bbedward
2025-07-11 15:45:18 -04:00
parent d169f5d4a3
commit 688c4b85fb
5 changed files with 338 additions and 106 deletions

View File

@@ -19,33 +19,135 @@ Rectangle {
border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2) border.color: Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.2)
border.width: 1 border.width: 1
// Timer to update MPRIS position // Constants and helpers - all microseconds
property bool justSeeked: false readonly property real oneSecondUs: 1000000.0
property real seekTargetPosition: 0
function asSec(us) { return us / oneSecondUs }
function ratio() { return trackLenUs > 0 ? uiPosUs / trackLenUs : 0 }
function normalizeLength(lenRaw) {
// If length < 86 400 it's almost certainly seconds (24 h upper bound).
// Convert to µs; otherwise return as-is.
return (lenRaw > 0 && lenRaw < 86400) ? lenRaw * oneSecondUs : lenRaw;
}
// Call seek() in safe 5-second chunks so every player obeys.
function chunkedSeek(offsetUs) {
if (Math.abs(offsetUs) < 5 * oneSecondUs) { // ≤5 s? single shot.
activePlayer.seek(offsetUs);
return;
}
const step = 5 * oneSecondUs; // 5 s
let remaining = offsetUs;
let safety = 0; // avoid infinite loops
while (Math.abs(remaining) > step && safety < 40) { // max 200 s
activePlayer.seek(Math.sign(remaining) * step);
remaining -= Math.sign(remaining) * step;
safety++;
}
if (remaining !== 0) activePlayer.seek(remaining);
}
// Returns a guaranteed-valid object-path for the current track.
function trackPath() {
const md = activePlayer.metadata || {};
// Spec: "/org/mpris/MediaPlayer2/Track/NNN"
if (typeof md["mpris:trackid"] === "string" &&
md["mpris:trackid"].length > 1 && md["mpris:trackid"].startsWith("/"))
return md["mpris:trackid"];
// Nothing reliable? Fall back to the *current* playlist entry object if exposed
if (activePlayer.currentTrackPath) return activePlayer.currentTrackPath;
// Absolute last resort—return null so caller knows SetPosition will fail
return null;
}
// Position tracking - all microseconds
property real uiPosUs: 0
property real backendPosUs: 0
property real trackLenUs: 0
property double backendStamp: Date.now() // wall-clock of last update in ms
// Optimistic timer
Timer { Timer {
id: positionTimer id: tickTimer
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !justSeeked interval: 50 // 20 fps feels smooth, cheap
interval: 1000
repeat: true repeat: true
running: activePlayer?.playbackState === MprisPlaybackState.Playing
onTriggered: { onTriggered: {
if (activePlayer) { if (trackLenUs <= 0) return;
activePlayer.positionChanged() const projected = backendPosUs + (Date.now() - backendStamp) * 1000.0;
} uiPosUs = Math.min(projected, trackLenUs); // never exceed track end
} }
} }
// Timer to resume position updates after seeking // --- 500-ms poll to keep external moves in sync -------------------
Timer { // Timer {
id: seekCooldownTimer // id: pollTimer
interval: 1000 // Reduced from 2000 // interval: 500 // ms
repeat: false // repeat: true
onTriggered: { // running: true // always on; cost is negligible
justSeeked = false // onTriggered: {
// Force position update after seek // if (!activePlayer || trackLenUs <= 0) return;
if (activePlayer) {
activePlayer.positionChanged() // const polledUs = activePlayer.position; // property read
} // // Compare in percent to avoid false positives
// if (Math.abs((polledUs - backendPosUs) / trackLenUs) > 0.01) { // >1 % jump
// backendPosUs = polledUs;
// backendStamp = Date.now();
// uiPosUs = polledUs; // snap instantly
// }
// }
// }
// Initialize when player changes
onActivePlayerChanged: {
if (activePlayer) {
backendPosUs = activePlayer.position || 0
trackLenUs = normalizeLength(activePlayer.length || 0)
backendStamp = Date.now()
uiPosUs = backendPosUs
console.log(`player change len ${asSec(trackLenUs)} s, pos ${asSec(uiPosUs)} s`)
} else {
backendPosUs = 0
trackLenUs = 0
backendStamp = Date.now()
uiPosUs = 0
}
}
// Backend events
Connections {
target: activePlayer
function onPositionChanged() {
const posUs = activePlayer.position
backendPosUs = posUs
backendStamp = Date.now()
uiPosUs = posUs // snap immediately on tick
}
function onSeeked(pos) {
backendPosUs = pos
backendStamp = Date.now()
uiPosUs = backendPosUs
}
function onPostTrackChanged() {
backendPosUs = activePlayer?.position || 0
trackLenUs = normalizeLength(activePlayer?.length || 0)
backendStamp = Date.now()
uiPosUs = backendPosUs
}
function onTrackTitleChanged() {
backendPosUs = activePlayer?.position || 0
trackLenUs = normalizeLength(activePlayer?.length || 0)
backendStamp = Date.now()
uiPosUs = backendPosUs
} }
} }
@@ -129,7 +231,7 @@ Rectangle {
} }
} }
// Simple progress bar - click to seek only // Progress bar
Rectangle { Rectangle {
width: parent.width width: parent.width
height: 6 height: 6
@@ -137,16 +239,12 @@ Rectangle {
color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3) color: Qt.rgba(theme.surfaceVariant.r, theme.surfaceVariant.g, theme.surfaceVariant.b, 0.3)
Rectangle { Rectangle {
width: { id: progressFill
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 height: parent.height
radius: parent.radius radius: parent.radius
color: theme.primary color: theme.primary
width: parent.width * ratio()
} }
MouseArea { MouseArea {
@@ -154,20 +252,20 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: (mouse) => { onClicked: (mouse) => {
if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) { if (!activePlayer || !activePlayer.canSeek || trackLenUs <= 0) return
const ratio = mouse.x / width
const targetPosition = Math.floor(ratio * activePlayer.length) const targetUs = (mouse.x / width) * trackLenUs
const currentPosition = activePlayer.position || 0 const offset = targetUs - backendPosUs
const seekOffset = targetPosition - currentPosition
console.log("Simple seek - offset:", seekOffset, "target:", targetPosition, "current:", currentPosition) if (typeof activePlayer.setPosition === "function") {
activePlayer.setPosition(trackPath() || "/", Math.round(targetUs))
// Store target position for visual feedback console.log(`SetPosition ${asSec(targetUs)} s`)
seekTargetPosition = targetPosition } else {
justSeeked = true chunkedSeek(offset) // <-- use helper
seekCooldownTimer.restart() console.log(`chunkedSeek ${asSec(offset/oneSecondUs)} s`)
activePlayer.seek(seekOffset)
} }
uiPosUs = backendPosUs = targetUs
} }
} }
} }
@@ -201,16 +299,15 @@ Rectangle {
if (!activePlayer) return if (!activePlayer) return
// >8 s → jump to start, otherwise previous track // >8 s → jump to start, otherwise previous track
if (activePlayer.position > 8000000) { if (uiPosUs > 8 * oneSecondUs && activePlayer.canSeek) {
console.log("Jumping to start - current position:", activePlayer.position) if (typeof activePlayer.setPosition === "function") {
activePlayer.setPosition(trackPath() || "/", 0)
// Store target position for visual feedback console.log("Back → SetPosition 0 µs")
seekTargetPosition = 0 } else {
justSeeked = true chunkedSeek(-backendPosUs) // <-- use helper
seekCooldownTimer.restart() console.log("Back → chunkedSeek to 0")
}
// Seek to the beginning uiPosUs = backendPosUs = 0
activePlayer.seek(-activePlayer.position)
} else { } else {
activePlayer.previous() activePlayer.previous()
} }

View File

@@ -13,7 +13,7 @@ Rectangle {
radius: theme.cornerRadius radius: theme.cornerRadius
color: clockMouseArea.containsMouse && root.hasActiveMedia ? color: clockMouseArea.containsMouse && root.hasActiveMedia ?
Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) : Qt.rgba(theme.primary.r, theme.primary.g, theme.primary.b, 0.12) :
Qt.rgba(theme.surfaceText.r, theme.surfaceText.g, theme.surfaceText.b, 0.08) Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
Behavior on color { Behavior on color {
ColorAnimation { ColorAnimation {
@@ -57,12 +57,41 @@ Rectangle {
visible: parent.children[0].status !== Image.Ready visible: parent.children[0].status !== Image.Ready
color: "transparent" color: "transparent"
Text { // Animated equalizer bars
Row {
anchors.centerIn: parent anchors.centerIn: parent
text: "music_note" spacing: 2
font.family: theme.iconFont
font.pixelSize: theme.iconSize Repeater {
color: theme.surfaceVariantText model: 5
Rectangle {
property real targetHeight: root.activePlayer?.playbackState === MprisPlaybackState.Playing ?
4 + Math.random() * 12 : 4
width: 3
height: targetHeight
radius: 1.5
color: theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: 100 + index * 50
easing.type: Easing.OutQuad
}
}
Timer {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing
interval: 150 + index * 30
repeat: true
onTriggered: {
parent.targetHeight = 4 + Math.random() * 12
}
}
}
}
} }
} }
} }

View File

@@ -29,6 +29,104 @@ PanelWindow {
implicitHeight: Theme.barHeight - 4 implicitHeight: Theme.barHeight - 4
color: "transparent" color: "transparent"
// Audio visualization data
property list<real> audioLevels: [0, 0, 0, 0]
// Real-time audio visualization using cava (with fallback)
property bool cavaAvailable: false
Process {
id: cavaCheck
command: ["which", "cava"]
running: true
onExited: (exitCode) => {
topBar.cavaAvailable = exitCode === 0
if (topBar.cavaAvailable) {
console.log("cava found - creating config and enabling real audio visualization")
configWriter.running = true
} else {
console.log("cava not found - using fallback animation")
fallbackTimer.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
// Create temporary config file for cava
Process {
id: configWriter
running: topBar.cavaAvailable
command: [
"sh", "-c",
`cat > /tmp/quickshell_cava_config << 'EOF'
[general]
mode = normal
framerate = 30
autosens = 0
sensitivity = 50
bars = 4
[output]
method = raw
raw_target = /dev/stdout
data_format = ascii
channels = mono
mono_option = average
[smoothing]
noise_reduction = 20
EOF`
]
onExited: {
// Start cava after config is written
if (topBar.cavaAvailable) {
cavaProcess.running = Qt.binding(() => root.hasActiveMedia && root.activePlayer?.playbackState === MprisPlaybackState.Playing)
}
}
}
Process {
id: cavaProcess
running: false
command: ["cava", "-p", "/tmp/quickshell_cava_config"]
stdout: SplitParser {
splitMarker: "\n"
onRead: data => {
if (data.trim()) {
// Parse semicolon-separated values from cava
let points = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p))
if (points.length >= 4) {
topBar.audioLevels = [points[0], points[1], points[2], points[3]]
}
}
}
}
onRunningChanged: {
if (!running) {
topBar.audioLevels = [0, 0, 0, 0]
}
}
}
// Fallback animation when cava is not available
Timer {
id: fallbackTimer
running: false
interval: 100
repeat: true
onTriggered: {
// Generate smooth random values for fallback (0-100 range)
topBar.audioLevels = [
Math.random() * 40 + 10, // 10-50
Math.random() * 60 + 20, // 20-80
Math.random() * 50 + 15, // 15-65
Math.random() * 35 + 20 // 20-55
]
}
}
// Floating panel container with margins // Floating panel container with margins
Item { Item {
@@ -362,20 +460,46 @@ PanelWindow {
visible: root.hasActiveMedia || root.weather.available visible: root.hasActiveMedia || root.weather.available
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
// Music icon when media is playing // Animated equalizer when media is playing
Text { Item {
text: "music_note" width: 20
font.family: Theme.iconFont height: Theme.iconSize
font.pixelSize: Theme.iconSize - 2
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.hasActiveMedia visible: root.hasActiveMedia
SequentialAnimation on scale { Row {
running: root.activePlayer?.playbackState === MprisPlaybackState.Playing anchors.centerIn: parent
loops: Animation.Infinite spacing: 2
NumberAnimation { to: 1.1; duration: 500 }
NumberAnimation { to: 1.0; duration: 500 } Repeater {
model: 4
Rectangle {
width: 3
height: {
if (root.activePlayer?.playbackState === MprisPlaybackState.Playing && topBar.audioLevels.length > index) {
// Scale and compress audio data for better visual range
const rawLevel = topBar.audioLevels[index] || 0
// Use square root to compress high values and expand low values
const scaledLevel = Math.sqrt(Math.min(Math.max(rawLevel, 0), 100) / 100) * 100
const maxHeight = Theme.iconSize - 2
const minHeight = 3
return minHeight + (scaledLevel / 100) * (maxHeight - minHeight)
}
return 3 // Minimum height when not playing
}
radius: 1.5
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
Behavior on height {
NumberAnimation {
duration: 80 // Slightly slower for smoother movement
easing.type: Easing.OutQuad
}
}
}
}
} }
} }

View File

@@ -12,7 +12,7 @@ Rectangle {
width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2) width: Math.max(120, workspaceRow.implicitWidth + theme.spacingL * 2)
height: 32 height: 32
radius: theme.cornerRadiusLarge radius: theme.cornerRadiusLarge
color: Qt.rgba(theme.surfaceContainerHigh.r, theme.surfaceContainerHigh.g, theme.surfaceContainerHigh.b, 0.8) color: Qt.rgba(theme.secondary.r, theme.secondary.g, theme.secondary.b, 0.08)
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
property int currentWorkspace: 1 property int currentWorkspace: 1

View File

@@ -8,6 +8,7 @@ Install the required tools:
```bash ```bash
# Required for Material-You palette generation # Required for Material-You palette generation
# Or paru -S matugen-bin on arch
cargo install matugen cargo install matugen
# Required for JSON processing (usually pre-installed) # Required for JSON processing (usually pre-installed)
@@ -16,7 +17,6 @@ sudo pacman -S jq # Arch Linux
# Background setters (choose one) # Background setters (choose one)
sudo pacman -S swaybg # Simple and reliable sudo pacman -S swaybg # Simple and reliable
# or: cargo install swww # Smoother transitions
``` ```
## Setup ## Setup
@@ -24,50 +24,32 @@ sudo pacman -S swaybg # Simple and reliable
1. **Initial wallpaper setup:** 1. **Initial wallpaper setup:**
```bash ```bash
# Set your initial wallpaper # Set your initial wallpaper
./scripts/set-wallpaper.sh /path/to/your/wallpaper.jpg sudo cp ./set-wallpaper.sh /usr/local/bin
sudo chmod +x /usr/local/bin/set-wallpaper.sh
set-wallpaper.sh /path/to/your/wallpaper.jpg
``` ```
2. **Enable Niri color integration (optional):** 2. **Enable Niri color integration (optional):**
Add this line to your `~/.config/niri/config.kdl`: Niri doesn't have a good way to just set colors, you have to edit your main `~/.config/niri/config.kdl`
```kdl
!include "generated_colors.kdl" The script generates suggestions in `~/quickshell/generated_niri_colors.kdl` you can manually configure in Niri.
```
3. **Enable Auto theme:** 3. **Enable Auto theme:**
Open Control Center → Theme Picker → Click the gradient "Auto" button Open Control Center → Theme Picker → Click the gradient "Auto" button
## Usage 4. **Configure swaybg systemd unit**
### Change wallpaper and auto-update theme: ```
```bash [Unit]
./scripts/set-wallpaper.sh /new/wallpaper.jpg PartOf=graphical-session.target
After=graphical-session.target
Requisite=graphical-session.target
[Service]
ExecStart=/usr/bin/swaybg -m fill -i "%h/quickshell/current_wallpaper"
Restart=on-failure
``` ```
### Manual theme switching:
- Use the Control Center theme picker
- Preferences persist across restarts
- Auto theme requires wallpaper symlink to exist
## How it works
1. **Color extraction:** `Colors.qml` uses Quickshell's ColorQuantizer to extract dominant colors from the wallpaper symlink
2. **Persistence:** `Prefs.qml` stores your theme choice using PersistentProperties
3. **Dynamic switching:** `Theme.qml` switches between static themes and wallpaper colors
4. **Auto-reload:** Quickshell's file watching automatically reloads when the wallpaper symlink changes
## Troubleshooting
### "Dynamic theme requires wallpaper setup!" error
Run the setup command:
```bash ```bash
./scripts/set-wallpaper.sh /path/to/your/wallpaper.jpg systemctl enable --user --now swaybg
``` ```
### Colors don't update when changing wallpaper
- Make sure you're using the script, not manually changing files
- The symlink at `~/quickshell/current_wallpaper` must exist
### Niri colors don't change
- Ensure `!include "generated_colors.kdl"` is in your config.kdl
- Check that matugen and jq are installed
- Look for `~/.config/niri/generated_colors.kdl`