mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-01-28 23:42:51 -05:00
Media fixes
This commit is contained in:
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`
|
|
||||||
Reference in New Issue
Block a user