diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-23 20:19:34 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-23 20:19:34 +0200 |
| commit | 0d021266ec91cf5660cfbe2678edbec8a9c62b4e (patch) | |
| tree | 319d6832758ddcd87f3f91a2a95a0cf59cef8c3e /desktop/companion/Companion.qml | |
| parent | f90381dbd12cdd862a3848874cbbf6d7ef15ae67 (diff) | |
| download | quickshell-0d021266ec91cf5660cfbe2678edbec8a9c62b4e.tar.gz quickshell-0d021266ec91cf5660cfbe2678edbec8a9c62b4e.zip | |
companion: DJ on track change, groove while bored with music
Reads the sound module's Player, which already filters the MPRIS
duplicates. The track key is artist+title while playing, so a pause and
resume of the same track does not DJ again; a shell start with music on
DJs once, which is harmless. Frame loops are a small table keyed by the
frame Mood names, so dj and headbang need no special case in Mood.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'desktop/companion/Companion.qml')
| -rw-r--r-- | desktop/companion/Companion.qml | 54 |
1 files changed, 50 insertions, 4 deletions
diff --git a/desktop/companion/Companion.qml b/desktop/companion/Companion.qml index f8d50e9..aa0505d 100644 --- a/desktop/companion/Companion.qml +++ b/desktop/companion/Companion.qml @@ -16,6 +16,7 @@ import Quickshell.Bluetooth import Quickshell.Hyprland import QtQuick import ".." +import "../modules/sound" import "../NotifyConfig.js" as NC import "Mood.js" as Mood import "Lines.js" as Lines @@ -107,6 +108,8 @@ PanelWindow { property double wakeAt: 0 property double admireAt: 0 property double singAt: 0 + property double djAt: 0 + property string groove: "" property double lastLineAt: 0 property double lastMonologueAt: 0 property bool monologue: false @@ -146,7 +149,9 @@ PanelWindow { root.wakeAt = t; root.say("wake"); } else if (root.idle === 1 && root.wasIdle === 0) { - if (Mood.monologue("bored", root.lastMonologueAt, t, Math.random())) root.startMonologue(); + root.pickGroove(); + if (Player.playing) root.say("groove"); + else if (Mood.monologue("bored", root.lastMonologueAt, t, Math.random())) root.startMonologue(); else root.say("bored"); } root.wasIdle = root.idle; @@ -230,6 +235,28 @@ PanelWindow { root.say("headphones"); } + // What is playing, from the sound module's player (it already drops the + // playerctld and browser-integration duplicates). A new artist+title + // while playing is a track start or change: she DJs for Mood.T.djHold + // and says the artist's line if she knows them. Paused reads "", and + // resuming the same track matches lastTrack, so it does not DJ again. + readonly property string track: Player.playing ? Player.artist + "\n" + Player.title : "" + property string lastTrack: "" + onTrackChanged: { + if (root.track === "" || root.track === root.lastTrack) return; + root.lastTrack = root.track; + root.djAt = Date.now(); + root.now = root.djAt; + root.say("artist:" + Player.artist); + } + // Music starting while she is already bored needs a groove too. + readonly property bool music: Player.playing + onMusicChanged: if (root.music && root.idle === 1) root.pickGroove() + function pickGroove() { + const g = ["whistle", "headbang", "airguitar"]; + root.groove = g[Math.floor(Math.random() * g.length)]; + } + // A listed wallpaper on an empty workspace: she turns to it and blushes. // Checked after the events above (switching to an empty workspace, // closing its last window) and whenever she stops being away: shown, @@ -285,7 +312,8 @@ PanelWindow { shovedAt: root.shovedAt, recording: root.asst.recording, busy: root.asst.busy, repliedAt: root.repliedAt, clicks: root.clicks, admireAt: root.admireAt, mockAt: root.mockAt, typingAt: root.typingAt, wakeAt: root.wakeAt, - singAt: root.singAt, monologue: root.monologue, idle: root.idle, + singAt: root.singAt, djAt: root.djAt, music: root.music, groove: root.groove, + monologue: root.monologue, idle: root.idle, }) readonly property string outfit: Mood.outfit(root.destructAt, root.now, Prefs.companionSkirtless * 1000) // Anything above the monologue cuts it short. @@ -302,7 +330,7 @@ PanelWindow { function say(kind, force) { const t = Date.now(); if (root.hidden || (!force && !Mood.canSpeak(root.lastLineAt, t))) return false; - const line = Lines.pick(kind, Math.random(), Prefs.companionApps); + const line = Lines.pick(kind, Math.random(), Prefs.companionApps, Prefs.companionArtists); if (line === "") return false; root.lastLineAt = t; bubble.show(line, 3000); @@ -409,6 +437,24 @@ PanelWindow { onTriggered: root.waveIndex = (root.waveIndex + 1) % root.waveCycle.length } + // Frames Mood names that are really a loop: the frame names its cycle + // and how fast it turns. Everything else shows as named. + readonly property var cycles: ({ + dj: { frames: ["dj", "dj-scratch"], ms: 450 }, + headbang: { frames: ["headbang-up", "headbang-down"], ms: 220 }, + }) + property int cycleIndex: 0 + readonly property var cycle: root.cycles[root.mood.frame] ?? null + onCycleChanged: root.cycleIndex = 0 + Timer { + interval: root.cycle?.ms ?? 1000 + repeat: true + running: root.cycle !== null && !root.away + onTriggered: root.cycleIndex = (root.cycleIndex + 1) % root.cycle.frames.length + } + readonly property string frame: root.leaving ? root.waveCycle[root.waveIndex] + : root.cycle ? root.cycle.frames[root.cycleIndex % root.cycle.frames.length] : root.mood.frame + // qs -p <desktop> ipc call companion toggle -> SUPER+\ IpcHandler { target: "companion" @@ -444,7 +490,7 @@ PanelWindow { id: sprite x: seat.x y: seat.y - source: `file://${root.art}/${root.outfit}/${root.leaving ? root.waveCycle[root.waveIndex] : root.mood.frame}.png` + source: `file://${root.art}/${root.outfit}/${root.frame}.png` fallback: `file://${root.art}/${root.outfit}/base.png` out: root.away trembling: root.mood.state === "admire" && root.mood.frame === "blush3" |
