mirror of
https://github.com/danbulant/dots-hyprland
synced 2026-05-24 12:22:09 +00:00
media controls: visualizer
This commit is contained in:
parent
4473129599
commit
a3818322a6
4 changed files with 114 additions and 7 deletions
|
|
@ -26,6 +26,7 @@ Scope {
|
||||||
property real contentPadding: 13
|
property real contentPadding: 13
|
||||||
property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
|
property real popupRounding: Appearance.rounding.screenRounding - Appearance.sizes.elevationMargin + 1
|
||||||
property real artRounding: Appearance.rounding.verysmall
|
property real artRounding: Appearance.rounding.verysmall
|
||||||
|
property list<real> visualizerPoints: []
|
||||||
|
|
||||||
property bool hasPlasmaIntegration: false
|
property bool hasPlasmaIntegration: false
|
||||||
function isRealPlayer(player) {
|
function isRealPlayer(player) {
|
||||||
|
|
@ -68,13 +69,32 @@ Scope {
|
||||||
return filtered;
|
return filtered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Process {
|
||||||
|
id: cavaProc
|
||||||
|
running: mediaControlsLoader.active
|
||||||
|
onRunningChanged: {
|
||||||
|
if (!cavaProc.running) {
|
||||||
|
root.visualizerPoints = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command: ["cava", "-p", `${FileUtils.trimFileProtocol(Directories.config)}/quickshell/scripts/cava/raw_output_config.txt`]
|
||||||
|
stdout: SplitParser {
|
||||||
|
onRead: data => {
|
||||||
|
// Parse `;`-separated values into the visualizerPoints array
|
||||||
|
let allPoints = data.split(";").map(p => parseFloat(p.trim())).filter(p => !isNaN(p));
|
||||||
|
let points = allPoints.slice(Math.floor(allPoints.length / 2), allPoints.length);
|
||||||
|
root.visualizerPoints = points;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Loader {
|
Loader {
|
||||||
id: mediaControlsLoader
|
id: mediaControlsLoader
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
sourceComponent: PanelWindow {
|
sourceComponent: PanelWindow {
|
||||||
id: mediaControlsRoot
|
id: mediaControlsRoot
|
||||||
visible: mediaControlsLoader.active
|
visible: true
|
||||||
|
|
||||||
exclusiveZone: 0
|
exclusiveZone: 0
|
||||||
implicitWidth: (
|
implicitWidth: (
|
||||||
|
|
@ -112,6 +132,7 @@ Scope {
|
||||||
delegate: PlayerControl {
|
delegate: PlayerControl {
|
||||||
required property MprisPlayer modelData
|
required property MprisPlayer modelData
|
||||||
player: modelData
|
player: modelData
|
||||||
|
visualizerPoints: root.visualizerPoints
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,8 @@ Item { // Player instance
|
||||||
property string artFilePath: `${artDownloadLocation}/${artFileName}`
|
property string artFilePath: `${artDownloadLocation}/${artFileName}`
|
||||||
property color artDominantColor: colorQuantizer?.colors[0] || Appearance.m3colors.m3secondaryContainer
|
property color artDominantColor: colorQuantizer?.colors[0] || Appearance.m3colors.m3secondaryContainer
|
||||||
property bool downloaded: false
|
property bool downloaded: false
|
||||||
|
property list<real> visualizerPoints: []
|
||||||
|
property real maxVisualizerValue: 1000 // Max value in the data points
|
||||||
|
|
||||||
implicitWidth: widgetWidth
|
implicitWidth: widgetWidth
|
||||||
implicitHeight: widgetHeight
|
implicitHeight: widgetHeight
|
||||||
|
|
@ -150,6 +152,69 @@ Item { // Player instance
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Canvas { // Visualizer
|
||||||
|
id: visualizerCanvas
|
||||||
|
anchors.fill: parent
|
||||||
|
onPaint: {
|
||||||
|
var ctx = getContext("2d");
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
var points = playerController.visualizerPoints;
|
||||||
|
var maxVal = playerController.maxVisualizerValue || 1;
|
||||||
|
var h = height;
|
||||||
|
var w = width;
|
||||||
|
var n = points.length;
|
||||||
|
if (n < 2) return;
|
||||||
|
|
||||||
|
// Smoothing: simple moving average (optional)
|
||||||
|
var smoothPoints = [];
|
||||||
|
var smoothWindow = 3; // adjust for more/less smoothing
|
||||||
|
for (var i = 0; i < n; ++i) {
|
||||||
|
var sum = 0, count = 0;
|
||||||
|
for (var j = -smoothWindow; j <= smoothWindow; ++j) {
|
||||||
|
var idx = Math.max(0, Math.min(n - 1, i + j));
|
||||||
|
sum += points[idx];
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
smoothPoints.push(sum / count);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, h);
|
||||||
|
for (var i = 0; i < n; ++i) {
|
||||||
|
var x = i * w / (n - 1);
|
||||||
|
var y = h - (smoothPoints[i] / maxVal) * h;
|
||||||
|
ctx.lineTo(x, y);
|
||||||
|
}
|
||||||
|
ctx.lineTo(w, h);
|
||||||
|
ctx.closePath();
|
||||||
|
|
||||||
|
ctx.fillStyle = Qt.rgba(
|
||||||
|
blendedColors.colPrimary.r,
|
||||||
|
blendedColors.colPrimary.g,
|
||||||
|
blendedColors.colPrimary.b,
|
||||||
|
0.25
|
||||||
|
);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
Connections {
|
||||||
|
target: playerController
|
||||||
|
function onVisualizerPointsChanged() {
|
||||||
|
visualizerCanvas.requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: MultiEffect { // Blur a tiny bit to obscure away the points
|
||||||
|
source: visualizerCanvas
|
||||||
|
saturation: 0.2
|
||||||
|
blurEnabled: true
|
||||||
|
blurMax: 6
|
||||||
|
blur: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: root.contentPadding
|
anchors.margins: root.contentPadding
|
||||||
|
|
@ -160,7 +225,7 @@ Item { // Player instance
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
implicitWidth: height
|
implicitWidth: height
|
||||||
radius: root.artRounding
|
radius: root.artRounding
|
||||||
color: blendedColors.colLayer1
|
color: ColorUtils.transparentize(blendedColors.colLayer1, 0.5)
|
||||||
|
|
||||||
layer.enabled: true
|
layer.enabled: true
|
||||||
layer.effect: OpacityMask {
|
layer.effect: OpacityMask {
|
||||||
|
|
@ -235,12 +300,18 @@ Item { // Player instance
|
||||||
iconName: "skip_previous"
|
iconName: "skip_previous"
|
||||||
onClicked: playerController.player?.previous()
|
onClicked: playerController.player?.previous()
|
||||||
}
|
}
|
||||||
StyledProgressBar {
|
Item {
|
||||||
id: slider
|
id: progressBarContainer
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
highlightColor: blendedColors.colPrimary
|
implicitHeight: progressBar.implicitHeight
|
||||||
trackColor: blendedColors.colSecondaryContainer
|
|
||||||
value: playerController.player?.position / playerController.player?.length
|
StyledProgressBar {
|
||||||
|
id: progressBar
|
||||||
|
anchors.fill: parent
|
||||||
|
highlightColor: blendedColors.colPrimary
|
||||||
|
trackColor: blendedColors.colSecondaryContainer
|
||||||
|
value: playerController.player?.position / playerController.player?.length
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TrackChangeButton {
|
TrackChangeButton {
|
||||||
iconName: "skip_next"
|
iconName: "skip_next"
|
||||||
|
|
|
||||||
14
.config/quickshell/scripts/cava/raw_output_config.txt
Normal file
14
.config/quickshell/scripts/cava/raw_output_config.txt
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
[general]
|
||||||
|
mode = waves
|
||||||
|
framerate = 60
|
||||||
|
autosens = 1
|
||||||
|
bars = 60
|
||||||
|
|
||||||
|
[output]
|
||||||
|
method = raw
|
||||||
|
raw_target = /dev/stdout
|
||||||
|
data_format = ascii
|
||||||
|
|
||||||
|
[smoothing]
|
||||||
|
noise_reduction = 20
|
||||||
|
|
||||||
|
|
@ -5,6 +5,7 @@ pkgdesc='Illogical Impulse Audio Dependencies'
|
||||||
arch=(any)
|
arch=(any)
|
||||||
license=(None)
|
license=(None)
|
||||||
depends=(
|
depends=(
|
||||||
|
cava
|
||||||
pavucontrol-qt
|
pavucontrol-qt
|
||||||
wireplumber
|
wireplumber
|
||||||
libdbusmenu-gtk3
|
libdbusmenu-gtk3
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue