From 2f37fd78be2b98b5f2da769780f3492092e3fc93 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:47:16 +0000 Subject: [PATCH 1/2] feat: Next-gen Aero glass UI overhaul with 7 new QML components - MeshGradientBackground: animated fluid gradient with album art colors - ParticleField: floating bubbles/light dust particle system - GlassPanel: frosted glass panel with blur, tint, and edge lighting - GlassButton: skeuomorphic glass/acrylic button with metallic sheen - GlowSlider: luminous slider with glowing water-drop handle - AlbumArtDisplay: holographic 3D album art with rotation and color extraction - ImmersiveLyrics: depth-based lyrics with focus/blur effects - Rewritten main.qml with parallax, spring physics, breathing UI - Added Qt5Compat.GraphicalEffects for blur/glow/shadow effects Co-authored-by: MollycodeX <114975977+MollycodeX@users.noreply.github.com> Agent-Logs-Url: https://github.com/MollycodeX/MSCPLAYER/sessions/d36ba381-7696-4ad0-8b15-d84c80ed2541 --- CMakeLists.txt | 2 +- src/AlbumArtDisplay.qml | 189 +++++++ src/CMakeLists.txt | 10 +- src/GlassButton.qml | 107 ++++ src/GlassPanel.qml | 111 ++++ src/GlowSlider.qml | 120 ++++ src/ImmersiveLyrics.qml | 105 ++++ src/MeshGradientBackground.qml | 72 +++ src/ParticleField.qml | 71 +++ src/main.qml | 990 ++++++++++++++++++++------------- 10 files changed, 1377 insertions(+), 400 deletions(-) create mode 100644 src/AlbumArtDisplay.qml create mode 100644 src/GlassButton.qml create mode 100644 src/GlassPanel.qml create mode 100644 src/GlowSlider.qml create mode 100644 src/ImmersiveLyrics.qml create mode 100644 src/MeshGradientBackground.qml create mode 100644 src/ParticleField.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 8fce450..dd047c5 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_AUTOMOC ON) # --------------------------------------------------------------------------- # Qt6 GUI / QML / i18n dependencies # --------------------------------------------------------------------------- -find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick QuickDialogs2 Network) +find_package(Qt6 REQUIRED COMPONENTS Core Gui Qml Quick QuickDialogs2 Network Core5Compat ShaderTools) # --------------------------------------------------------------------------- # 添加主源码与头文件路径 diff --git a/src/AlbumArtDisplay.qml b/src/AlbumArtDisplay.qml new file mode 100644 index 0000000..d53711a --- /dev/null +++ b/src/AlbumArtDisplay.qml @@ -0,0 +1,189 @@ +import QtQuick +import Qt5Compat.GraphicalEffects + +// AlbumArtDisplay – holographic 3D album art with rotation and reflection. +Item { + id: root + + property string source: "" + property bool isPlaying: false + property real parallaxX: 0 // -1..1, from mouse + property real parallaxY: 0 // -1..1, from mouse + + // Emitted once the image loads so parent can extract dominant colors + signal imageReady() + // Expose dominant colors extracted from the cover + property color dominantColor1: "#1a0533" + property color dominantColor2: "#0a1628" + property color dominantColor3: "#0f2027" + property color dominantColor4: "#1a0a2e" + + implicitWidth: 200 + implicitHeight: 200 + + // 3D perspective wrapper + Item { + id: perspectiveContainer + anchors.centerIn: parent + width: root.width + height: root.height + + transform: [ + Rotation { + origin.x: perspectiveContainer.width / 2 + origin.y: perspectiveContainer.height / 2 + axis { x: 1; y: 0; z: 0 } + angle: root.parallaxY * 6 // tilt up to ±6° + Behavior on angle { + SpringAnimation { spring: 2; damping: 0.4 } + } + }, + Rotation { + origin.x: perspectiveContainer.width / 2 + origin.y: perspectiveContainer.height / 2 + axis { x: 0; y: 1; z: 0 } + angle: -root.parallaxX * 6 + Behavior on angle { + SpringAnimation { spring: 2; damping: 0.4 } + } + } + ] + + // Shadow beneath the album art + RectangularGlow { + anchors.centerIn: artClip + width: artClip.width + 4 + height: artClip.height + 4 + glowRadius: 20 + spread: 0.1 + color: Qt.rgba(root.dominantColor1.r, + root.dominantColor1.g, + root.dominantColor1.b, 0.4) + cornerRadius: artClip.radius + glowRadius + } + + // Round-clipped album art + Rectangle { + id: artClip + anchors.centerIn: parent + width: parent.width * 0.85 + height: parent.height * 0.85 + radius: 16 + color: Qt.rgba(1, 1, 1, 0.05) + clip: true + + Image { + id: coverImage + anchors.fill: parent + source: root.source + fillMode: Image.PreserveAspectCrop + asynchronous: true + visible: root.source !== "" + + onStatusChanged: { + if (status === Image.Ready) { + root._extractColors() + root.imageReady() + } + } + } + + // Placeholder when no cover + Item { + anchors.fill: parent + visible: root.source === "" || coverImage.status !== Image.Ready + + Rectangle { + anchors.fill: parent + color: Qt.rgba(1, 1, 1, 0.04) + } + + Text { + anchors.centerIn: parent + text: "♪" + font.pixelSize: 48 + color: Qt.rgba(1, 1, 1, 0.2) + } + } + + // Glass reflection overlay + Rectangle { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(1, 1, 1, 0.12) } + GradientStop { position: 0.35; color: Qt.rgba(1, 1, 1, 0.02) } + GradientStop { position: 0.65; color: "transparent" } + GradientStop { position: 1.0; color: Qt.rgba(0, 0, 0, 0.1) } + } + } + + // Edge highlight + Rectangle { + anchors.fill: parent + color: "transparent" + radius: parent.radius + border.width: 1 + border.color: Qt.rgba(1, 1, 1, 0.15) + } + } + + // Slow rotation when playing + RotationAnimation on rotation { + from: 0 + to: 360 + duration: 30000 // 30s per revolution + loops: Animation.Infinite + running: root.isPlaying && root.source !== "" + paused: !root.isPlaying + } + } + + // --- Color extraction via hidden Canvas --- + Canvas { + id: colorExtractor + width: 8 + height: 8 + visible: false + + onPaint: { + var ctx = getContext("2d") + ctx.drawImage(root.source, 0, 0, 8, 8) + } + + onPainted: { + var ctx = getContext("2d") + try { + var data = ctx.getImageData(0, 0, 8, 8).data + // Sample corners and center for dominant colors + var samples = [ + _sampleAt(data, 8, 1, 1), // top-left + _sampleAt(data, 8, 6, 1), // top-right + _sampleAt(data, 8, 1, 6), // bottom-left + _sampleAt(data, 8, 4, 4) // center + ] + root.dominantColor1 = _toColor(samples[0]) + root.dominantColor2 = _toColor(samples[1]) + root.dominantColor3 = _toColor(samples[2]) + root.dominantColor4 = _toColor(samples[3]) + } catch(e) { + // Keep defaults on error + } + } + } + + function _extractColors() { + if (root.source !== "") { + colorExtractor.loadImage(root.source) + colorExtractor.requestPaint() + } + } + + function _sampleAt(data, stride, x, y) { + var idx = (y * stride + x) * 4 + return { r: data[idx] / 255, g: data[idx+1] / 255, b: data[idx+2] / 255 } + } + + function _toColor(s) { + return Qt.rgba(s.r, s.g, s.b, 1.0) + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 910b9f0..ecab075 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -118,7 +118,15 @@ qt_add_executable(vibe_coding_player qt_add_qml_module(vibe_coding_player URI VibePlayer VERSION 1.0 - QML_FILES main.qml + QML_FILES + main.qml + GlassPanel.qml + MeshGradientBackground.qml + ParticleField.qml + GlowSlider.qml + GlassButton.qml + AlbumArtDisplay.qml + ImmersiveLyrics.qml RESOURCE_PREFIX / ) diff --git a/src/GlassButton.qml b/src/GlassButton.qml new file mode 100644 index 0000000..fd57b3d --- /dev/null +++ b/src/GlassButton.qml @@ -0,0 +1,107 @@ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects + +// GlassButton – a skeuomorphic glass/acrylic button with metallic sheen. +Button { + id: root + + property color baseColor: Qt.rgba(1, 1, 1, 0.08) + property color hoverColor: Qt.rgba(1, 1, 1, 0.14) + property color pressColor: Qt.rgba(1, 1, 1, 0.06) + property color borderColor: Qt.rgba(1, 1, 1, 0.15) + property color textColor: "#e0e0e0" + property color accentGlow: "#5599ff" + property real cornerRadius: 12 + property string iconText: "" // emoji / unicode icon + property bool isAccent: false // whether this is a primary/accent button + + leftPadding: 16 + rightPadding: 16 + topPadding: 8 + bottomPadding: 8 + + contentItem: Row { + spacing: root.iconText !== "" && root.text !== "" ? 6 : 0 + + Text { + visible: root.iconText !== "" + text: root.iconText + font.pixelSize: 16 + anchors.verticalCenter: parent.verticalCenter + color: root.isAccent ? root.accentGlow : root.textColor + } + + Text { + visible: root.text !== "" + text: root.text + font.pixelSize: 13 + font.family: "sans-serif" + font.weight: Font.Medium + color: root.textColor + anchors.verticalCenter: parent.verticalCenter + } + } + + background: Rectangle { + id: bg + implicitWidth: 60 + implicitHeight: 36 + radius: root.cornerRadius + color: root.pressed ? root.pressColor + : (root.hovered ? root.hoverColor : root.baseColor) + + border.width: 1 + border.color: root.hovered ? Qt.rgba(1, 1, 1, 0.3) : root.borderColor + + Behavior on color { + ColorAnimation { duration: 150; easing.type: Easing.OutCubic } + } + Behavior on border.color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + + // Top edge highlight (metallic sheen) + Rectangle { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 1 + height: 1 + radius: root.cornerRadius + gradient: Gradient { + orientation: Gradient.Horizontal + GradientStop { position: 0.0; color: "transparent" } + GradientStop { position: 0.3; color: Qt.rgba(1, 1, 1, root.hovered ? 0.25 : 0.1) } + GradientStop { position: 0.7; color: Qt.rgba(1, 1, 1, root.hovered ? 0.25 : 0.1) } + GradientStop { position: 1.0; color: "transparent" } + } + } + + // Press shadow (inset feel) + Rectangle { + anchors.fill: parent + radius: root.cornerRadius + color: "transparent" + opacity: root.pressed ? 0.15 : 0 + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.4) + Behavior on opacity { NumberAnimation { duration: 100 } } + } + + // Accent glow for primary buttons + layer.enabled: root.isAccent && root.hovered + layer.effect: Glow { + radius: 8 + samples: 17 + color: root.accentGlow + spread: 0.1 + } + } + + // Jelly press scale + scale: root.pressed ? 0.95 : 1.0 + Behavior on scale { + SpringAnimation { spring: 5; damping: 0.3; mass: 0.3 } + } +} diff --git a/src/GlassPanel.qml b/src/GlassPanel.qml new file mode 100644 index 0000000..eebd415 --- /dev/null +++ b/src/GlassPanel.qml @@ -0,0 +1,111 @@ +import QtQuick +import Qt5Compat.GraphicalEffects + +// GlassPanel – frosted glass container with blur, tint, and edge lighting. +// Requires a `backgroundSource` Item reference to blur behind the panel. +Item { + id: root + + property Item backgroundSource: null + property real blurRadius: 40 + property color tintColor: Qt.rgba(1, 1, 1, 0.06) + property real borderOpacity: 0.12 + property real cornerRadius: 16 + property bool hovered: false + + // Allow children to be placed inside the panel + default property alias contentData: contentContainer.data + + // --- Frosted glass background --- + Item { + id: blurLayer + anchors.fill: parent + visible: root.backgroundSource !== null + + // Round-rect clip mask (rendered off-screen) + Rectangle { + id: clipMask + anchors.fill: parent + radius: root.cornerRadius + visible: false + } + + // Captured + blurred background + Item { + id: blurContent + anchors.fill: parent + layer.enabled: true + layer.effect: OpacityMask { maskSource: clipMask } + + ShaderEffectSource { + id: bgCapture + anchors.fill: parent + sourceItem: root.backgroundSource + sourceRect: { + if (!root.backgroundSource) return Qt.rect(0, 0, 0, 0) + var p = root.mapToItem(root.backgroundSource, 0, 0) + return Qt.rect(p.x, p.y, root.width, root.height) + } + live: true + visible: false + } + + FastBlur { + anchors.fill: parent + source: bgCapture + radius: root.blurRadius + } + + // Tint overlay + Rectangle { + anchors.fill: parent + color: root.tintColor + } + } + } + + // Fallback when no background source: solid semi-transparent fill + Rectangle { + anchors.fill: parent + visible: root.backgroundSource === null + color: root.tintColor + radius: root.cornerRadius + } + + // --- Edge highlight border --- + Rectangle { + anchors.fill: parent + color: "transparent" + radius: root.cornerRadius + border.width: 1 + border.color: Qt.rgba(1, 1, 1, root.hovered ? root.borderOpacity * 2.5 + : root.borderOpacity) + Behavior on border.color { + ColorAnimation { duration: 300; easing.type: Easing.OutCubic } + } + } + + // --- Subtle inner glow on hover --- + Rectangle { + anchors.fill: parent + radius: root.cornerRadius + color: "transparent" + border.width: 0 + opacity: root.hovered ? 0.06 : 0 + Behavior on opacity { NumberAnimation { duration: 300 } } + + layer.enabled: root.hovered + layer.effect: Glow { + radius: 12 + samples: 25 + color: Qt.rgba(1, 1, 1, 0.3) + spread: 0.1 + } + } + + // Content container + Item { + id: contentContainer + anchors.fill: parent + } +} diff --git a/src/GlowSlider.qml b/src/GlowSlider.qml new file mode 100644 index 0000000..19aadee --- /dev/null +++ b/src/GlowSlider.qml @@ -0,0 +1,120 @@ +import QtQuick +import QtQuick.Controls +import Qt5Compat.GraphicalEffects + +// GlowSlider – a luminous progress/volume slider with a glowing handle. +Slider { + id: root + + property color glowColor: "#5599ff" + property color trackColor: Qt.rgba(1, 1, 1, 0.1) + property real trackHeight: 4 + property real handleSize: 16 + property bool springy: true + + background: Item { + x: root.leftPadding + y: root.topPadding + root.availableHeight / 2 - root.trackHeight / 2 + width: root.availableWidth + height: root.trackHeight + + // Track background + Rectangle { + anchors.fill: parent + radius: parent.height / 2 + color: root.trackColor + } + + // Filled portion (glowing flow line) + Rectangle { + width: root.visualPosition * parent.width + height: parent.height + radius: parent.height / 2 + color: root.glowColor + + // Glow effect on the filled track + layer.enabled: true + layer.effect: Glow { + radius: 8 + samples: 17 + color: root.glowColor + spread: 0.3 + } + } + } + + handle: Item { + x: root.leftPadding + root.visualPosition * root.availableWidth - root.handleSize / 2 + y: root.topPadding + root.availableHeight / 2 - root.handleSize / 2 + width: root.handleSize + height: root.handleSize + + // Spring animation on x position + Behavior on x { + enabled: root.springy && !root.pressed + SpringAnimation { + spring: 3 + damping: 0.25 + mass: 0.5 + } + } + + // Outer glow + Rectangle { + id: handleGlow + anchors.centerIn: parent + width: root.handleSize * (root.pressed ? 2.2 : 1.6) + height: width + radius: width / 2 + color: "transparent" + + Behavior on width { + SpringAnimation { spring: 4; damping: 0.3 } + } + + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: Qt.rgba(root.glowColor.r, + root.glowColor.g, + root.glowColor.b, + root.pressed ? 0.5 : 0.3) } + GradientStop { position: 1.0; color: "transparent" } + } + } + } + + // Handle body (water-drop style) + Rectangle { + anchors.centerIn: parent + width: root.handleSize + height: root.handleSize + radius: root.handleSize / 2 + color: root.glowColor + + // Inner highlight + Rectangle { + anchors.centerIn: parent + width: parent.width * 0.5 + height: parent.height * 0.5 + radius: width / 2 + color: Qt.lighter(root.glowColor, 1.6) + opacity: 0.6 + } + + // Glow + layer.enabled: true + layer.effect: Glow { + radius: 6 + samples: 13 + color: root.glowColor + spread: 0.2 + } + + scale: root.pressed ? 1.2 : (root.hovered ? 1.1 : 1.0) + Behavior on scale { + SpringAnimation { spring: 5; damping: 0.3 } + } + } + } +} diff --git a/src/ImmersiveLyrics.qml b/src/ImmersiveLyrics.qml new file mode 100644 index 0000000..29d4eef --- /dev/null +++ b/src/ImmersiveLyrics.qml @@ -0,0 +1,105 @@ +import QtQuick +import Qt5Compat.GraphicalEffects + +// ImmersiveLyrics – depth-based lyrics display where the current line +// is in focus and surrounding lines blur into the background. +Item { + id: root + + property string lyrics: "" + property color textColor: "#e0e0e0" + property color glowColor: "#5599ff" + property bool expanded: true + + clip: true + + // Split lyrics into lines for depth rendering + property var _lines: lyrics.split("\n").filter(function(l) { return l.trim() !== "" }) + property int _currentLine: 0 + + // Auto-scroll timer (simple approximation – scrolls through lines) + Timer { + id: scrollTimer + interval: 4000 + repeat: true + running: root.visible && root.expanded && root._lines.length > 1 + onTriggered: { + root._currentLine = (root._currentLine + 1) % root._lines.length + } + } + + onLyricsChanged: _currentLine = 0 + + ListView { + id: lyricsView + anchors.fill: parent + model: root._lines + clip: true + spacing: 6 + preferredHighlightBegin: height * 0.35 + preferredHighlightEnd: height * 0.65 + highlightRangeMode: ListView.ApplyRange + currentIndex: root._currentLine + + Behavior on contentY { + SpringAnimation { spring: 2; damping: 0.5 } + } + + delegate: Item { + width: lyricsView.width + height: lyricText.implicitHeight + 8 + + property bool isCurrent: index === lyricsView.currentIndex + property int dist: Math.abs(index - lyricsView.currentIndex) + + Text { + id: lyricText + anchors.left: parent.left + anchors.right: parent.right + anchors.leftMargin: 12 + anchors.rightMargin: 12 + text: modelData + wrapMode: Text.Wrap + font.pixelSize: isCurrent ? 15 : 13 + font.weight: isCurrent ? Font.DemiBold : Font.Normal + font.family: "sans-serif" + color: root.textColor + + opacity: { + if (isCurrent) return 1.0 + if (dist === 1) return 0.55 + if (dist === 2) return 0.3 + return 0.15 + } + + Behavior on opacity { + NumberAnimation { duration: 400; easing.type: Easing.OutCubic } + } + Behavior on font.pixelSize { + NumberAnimation { duration: 300; easing.type: Easing.OutCubic } + } + + // Subtle glow on current line + layer.enabled: isCurrent + layer.effect: Glow { + radius: 4 + samples: 9 + color: Qt.rgba(root.glowColor.r, root.glowColor.g, + root.glowColor.b, 0.3) + spread: 0.1 + } + } + + // Scale effect for depth + transform: Scale { + origin.x: parent.width / 2 + origin.y: parent.height / 2 + xScale: isCurrent ? 1.0 : (1.0 - dist * 0.02) + yScale: xScale + Behavior on xScale { + NumberAnimation { duration: 300; easing.type: Easing.OutCubic } + } + } + } + } +} diff --git a/src/MeshGradientBackground.qml b/src/MeshGradientBackground.qml new file mode 100644 index 0000000..113c49c --- /dev/null +++ b/src/MeshGradientBackground.qml @@ -0,0 +1,72 @@ +import QtQuick + +// MeshGradientBackground – animated fluid gradient with color blobs. +// Feed it colors extracted from album art for a living background. +Canvas { + id: root + + property color color1: "#1a0533" + property color color2: "#0a1628" + property color color3: "#0f2027" + property color color4: "#1a0a2e" + property real animSpeed: 0.0004 + + // Internal blob state + property real _t: 0 + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var w = width, h = height + + // Dark base fill + ctx.fillStyle = Qt.rgba(0.02, 0.02, 0.06, 1.0) + ctx.fillRect(0, 0, w, h) + + // Draw soft radial blobs at animated positions + var blobs = [ + { cx: w * (0.25 + 0.2 * Math.sin(_t * 0.7)), + cy: h * (0.3 + 0.15 * Math.cos(_t * 0.5)), + r: Math.max(w, h) * 0.55, color: root.color1 }, + { cx: w * (0.75 + 0.15 * Math.cos(_t * 0.6)), + cy: h * (0.2 + 0.2 * Math.sin(_t * 0.8)), + r: Math.max(w, h) * 0.45, color: root.color2 }, + { cx: w * (0.6 + 0.2 * Math.sin(_t * 0.9)), + cy: h * (0.75 + 0.15 * Math.cos(_t * 0.4)), + r: Math.max(w, h) * 0.5, color: root.color3 }, + { cx: w * (0.2 + 0.15 * Math.cos(_t * 1.1)), + cy: h * (0.8 + 0.1 * Math.sin(_t * 0.6)), + r: Math.max(w, h) * 0.4, color: root.color4 } + ] + + for (var i = 0; i < blobs.length; i++) { + var b = blobs[i] + var grad = ctx.createRadialGradient(b.cx, b.cy, 0, b.cx, b.cy, b.r) + // Extract RGBA from the Qt color and apply alpha + var c = b.color + grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, 0.45)) + grad.addColorStop(0.4, Qt.rgba(c.r, c.g, c.b, 0.2)) + grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0)) + ctx.fillStyle = grad + ctx.fillRect(0, 0, w, h) + } + } + + Timer { + running: root.visible + interval: 50 + repeat: true + onTriggered: { + root._t += root.animSpeed * interval + root.requestPaint() + } + } + + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + onColor1Changed: requestPaint() + onColor2Changed: requestPaint() + onColor3Changed: requestPaint() + onColor4Changed: requestPaint() +} diff --git a/src/ParticleField.qml b/src/ParticleField.qml new file mode 100644 index 0000000..333575d --- /dev/null +++ b/src/ParticleField.qml @@ -0,0 +1,71 @@ +import QtQuick + +// ParticleField – delicate floating bubbles / light dust rising slowly. +Canvas { + id: root + + property int particleCount: 35 + property color particleColor: "#ffffff" + property real maxOpacity: 0.25 + + // Internal particle data (populated once) + property var _particles: [] + + Component.onCompleted: _initParticles() + + function _initParticles() { + var arr = [] + for (var i = 0; i < particleCount; i++) { + arr.push({ + x: Math.random() * width, + y: Math.random() * height, + r: 1 + Math.random() * 2.5, // radius 1–3.5 + speed: 0.15 + Math.random() * 0.45, // rise speed + drift: (Math.random() - 0.5) * 0.3, // horizontal wander + alpha: 0.05 + Math.random() * (maxOpacity - 0.05), + phase: Math.random() * Math.PI * 2 + }) + } + _particles = arr + } + + onPaint: { + var ctx = getContext("2d") + ctx.reset() + + var w = width, h = height + var c = particleColor + var t = Date.now() * 0.001 + + for (var i = 0; i < _particles.length; i++) { + var p = _particles[i] + + // Move upward, wrap around + p.y -= p.speed + p.x += p.drift + Math.sin(t + p.phase) * 0.15 + if (p.y < -10) { p.y = h + 10; p.x = Math.random() * w } + if (p.x < -10) p.x = w + 10 + if (p.x > w + 10) p.x = -10 + + // Draw soft circle + var grad = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, p.r * 2) + grad.addColorStop(0, Qt.rgba(c.r, c.g, c.b, p.alpha)) + grad.addColorStop(0.5, Qt.rgba(c.r, c.g, c.b, p.alpha * 0.4)) + grad.addColorStop(1, Qt.rgba(c.r, c.g, c.b, 0)) + ctx.fillStyle = grad + ctx.beginPath() + ctx.arc(p.x, p.y, p.r * 2, 0, Math.PI * 2) + ctx.fill() + } + } + + Timer { + running: root.visible + interval: 50 + repeat: true + onTriggered: root.requestPaint() + } + + onWidthChanged: { _initParticles(); requestPaint() } + onHeightChanged: { _initParticles(); requestPaint() } +} diff --git a/src/main.qml b/src/main.qml index 6ed405f..795ba36 100644 --- a/src/main.qml +++ b/src/main.qml @@ -3,42 +3,60 @@ import QtQuick.Controls import QtQuick.Layouts import QtQuick.Window import QtQuick.Dialogs +import Qt5Compat.GraphicalEffects Window { id: root - width: 480 - height: 700 - minimumWidth: 360 - minimumHeight: 520 + width: 520 + height: 800 + minimumWidth: 400 + minimumHeight: 600 visible: true title: qsTr("Vibe Player") - color: theme.background + color: "#050510" - // Theme toggle: false = light, true = dark - property bool darkTheme: false + // Theme toggle: false = dark (default for Aero), true = light + property bool darkTheme: true - // Centralized theme palette + // Parallax mouse tracking (-1..1 range) + property real parallaxX: 0 + property real parallaxY: 0 + + // ----------------------------------------------------------------------- + // Next-Gen Aero theme palette + // ----------------------------------------------------------------------- QtObject { id: theme - readonly property color background: root.darkTheme ? "#1e1e1e" : "#ffffff" - readonly property color text: root.darkTheme ? "#e0e0e0" : "#000000" - readonly property color secondaryText: root.darkTheme ? "#aaaaaa" : "#555555" - readonly property color mutedText: root.darkTheme ? "#888888" : "#666666" - readonly property color placeholderText: root.darkTheme ? "#666666" : "#999999" - readonly property color accent: root.darkTheme ? "#5599ff" : "#3366cc" - readonly property color accentBg: root.darkTheme ? "#2a3a5c" : "#e0e8ff" - readonly property color hoverBg: root.darkTheme ? "#2c2c2c" : "#f0f0f0" - readonly property color surfaceBg: root.darkTheme ? "#2a2a2a" : "#f8f8f8" - readonly property color border: root.darkTheme ? "#444444" : "#cccccc" - readonly property color success: root.darkTheme ? "#66bb6a" : "#4CAF50" - readonly property color info: root.darkTheme ? "#42a5f5" : "#1976D2" - readonly property color metadataEvenRow: root.darkTheme ? "#252525" : "#ffffff" - readonly property color metadataHoverRow: root.darkTheme ? "#33404d" : "#e8f0fe" - readonly property color buttonBg: root.darkTheme ? "#3a3a3a" : "#e8e8e8" - readonly property color buttonHoverBg: root.darkTheme ? "#4a4a4a" : "#d0d0d0" + // Base colors + readonly property color background: root.darkTheme ? "#050510" : "#dde1ea" + readonly property color text: root.darkTheme ? "#e8eaef" : "#1a1a2e" + readonly property color secondaryText: root.darkTheme ? Qt.rgba(1,1,1,0.55) : Qt.rgba(0,0,0,0.5) + readonly property color mutedText: root.darkTheme ? Qt.rgba(1,1,1,0.3) : Qt.rgba(0,0,0,0.35) + readonly property color placeholderText: root.darkTheme ? Qt.rgba(1,1,1,0.2) : Qt.rgba(0,0,0,0.25) + // Accent derived from album art (or default blue) + readonly property color accent: albumArtView.dominantColor1 !== "#1a0533" + ? albumArtView.dominantColor2 + : (root.darkTheme ? "#5599ff" : "#3366cc") + readonly property color accentBg: Qt.rgba(theme.accent.r, theme.accent.g, + theme.accent.b, root.darkTheme ? 0.15 : 0.2) + readonly property color hoverBg: root.darkTheme ? Qt.rgba(1,1,1,0.06) : Qt.rgba(0,0,0,0.05) + readonly property color surfaceBg: root.darkTheme ? Qt.rgba(1,1,1,0.04) : Qt.rgba(1,1,1,0.5) + readonly property color border: root.darkTheme ? Qt.rgba(1,1,1,0.1) : Qt.rgba(0,0,0,0.1) + readonly property color success: "#66bb6a" + readonly property color info: "#42a5f5" + readonly property color metadataEvenRow: root.darkTheme ? Qt.rgba(1,1,1,0.03) : Qt.rgba(1,1,1,0.7) + readonly property color metadataHoverRow: root.darkTheme ? Qt.rgba(1,1,1,0.08) : Qt.rgba(0.9,0.95,1,0.8) + readonly property color buttonBg: root.darkTheme ? Qt.rgba(1,1,1,0.08) : Qt.rgba(1,1,1,0.5) + readonly property color buttonHoverBg: root.darkTheme ? Qt.rgba(1,1,1,0.14) : Qt.rgba(1,1,1,0.7) + + // Glass panel styling + readonly property color glassTint: root.darkTheme ? Qt.rgba(0.05,0.05,0.12,0.55) : Qt.rgba(1,1,1,0.55) + readonly property real glassBorder: root.darkTheme ? 0.12 : 0.2 + readonly property real glassRadius: 20 + readonly property real glassBlur: 40 } - // Propagate theme colors to all controls via the palette + // Propagate palette palette.button: theme.buttonBg palette.buttonText: theme.text palette.window: theme.background @@ -56,7 +74,7 @@ Window { return m + ":" + (s < 10 ? "0" : "") + s } - // Timer to poll playback position while playing + // Position polling timer Timer { id: positionTimer interval: 250 @@ -65,7 +83,9 @@ Window { onTriggered: playerController.updatePosition() } - // File dialog for adding individual audio files + // ----------------------------------------------------------------------- + // File / Folder dialogs (unchanged functionality) + // ----------------------------------------------------------------------- FileDialog { id: fileDialog title: qsTr("Select Audio Files") @@ -76,8 +96,6 @@ Window { playerController.addTrackUrl(selectedFiles[i]) } } - - // Folder dialog for adding an entire folder of audio files FolderDialog { id: folderDialog title: qsTr("Select Music Folder") @@ -85,7 +103,7 @@ Window { } // ----------------------------------------------------------------------- - // Metadata selection dialog – lazy-loaded when first needed + // Metadata selection dialog (glass-themed) // ----------------------------------------------------------------------- Loader { id: metadataSelectionDialogLoader @@ -107,6 +125,7 @@ Window { text: qsTr("Multiple results found. Please select the correct one:") wrapMode: Text.Wrap Layout.fillWidth: true + color: theme.text } ListView { @@ -119,33 +138,34 @@ Window { delegate: Rectangle { width: resultsList.width height: 64 - color: resultMouseArea.containsMouse ? theme.metadataHoverRow : (index % 2 === 0 ? theme.metadataEvenRow : theme.surfaceBg) - radius: 4 + color: resultMouseArea.containsMouse + ? theme.metadataHoverRow + : (index % 2 === 0 ? theme.metadataEvenRow : theme.surfaceBg) + radius: 8 RowLayout { anchors.fill: parent anchors.margins: 6 spacing: 8 - // Album cover art thumbnail Image { Layout.preferredWidth: 48 Layout.preferredHeight: 48 source: modelData.coverArtUrl fillMode: Image.PreserveAspectFit asynchronous: true - // Placeholder when no cover art or loading failed Rectangle { anchors.fill: parent visible: parent.status !== Image.Ready color: theme.surfaceBg border.color: theme.border border.width: 1 - radius: 4 + radius: 6 Label { anchors.centerIn: parent - text: "🎵" - font.pointSize: 16 + text: "♪" + font.pixelSize: 20 + color: theme.mutedText } } } @@ -153,18 +173,18 @@ Window { ColumnLayout { Layout.fillWidth: true spacing: 2 - Label { text: modelData.title || qsTr("(unknown title)") font.bold: true elide: Text.ElideRight Layout.fillWidth: true + color: theme.text } Label { text: (modelData.artist || qsTr("Unknown Artist")) + (modelData.album ? " — " + modelData.album : "") - font.pointSize: 9 - color: theme.mutedText + font.pixelSize: 11 + color: theme.secondaryText elide: Text.ElideRight Layout.fillWidth: true } @@ -184,14 +204,11 @@ Window { } } } - function show() { - active = true - item.open() - } + function show() { active = true; item.open() } } // ----------------------------------------------------------------------- - // Write metadata confirmation dialog – lazy-loaded when first needed + // Write metadata dialog // ----------------------------------------------------------------------- Loader { id: writeMetadataDialogLoader @@ -207,39 +224,18 @@ Window { ColumnLayout { anchors.fill: parent spacing: 8 + Label { text: qsTr("Write the following metadata into the audio file?"); wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.text } + Label { text: qsTr("Title: ") + playerController.trackTitle; wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.text } + Label { text: qsTr("Artist: ") + playerController.trackArtist; wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.text } + Label { text: qsTr("Album: ") + playerController.trackAlbum; wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.text } - Label { - text: qsTr("Write the following metadata into the audio file?") - wrapMode: Text.Wrap - Layout.fillWidth: true - } - Label { - text: qsTr("Title: ") + playerController.trackTitle - wrapMode: Text.Wrap - Layout.fillWidth: true - } - Label { - text: qsTr("Artist: ") + playerController.trackArtist - wrapMode: Text.Wrap - Layout.fillWidth: true - } - Label { - text: qsTr("Album: ") + playerController.trackAlbum - wrapMode: Text.Wrap - Layout.fillWidth: true - } - - // Album cover art preview in dialog Image { Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 100 - Layout.preferredHeight: 100 + Layout.preferredWidth: 100; Layout.preferredHeight: 100 visible: playerController.albumArtUrl !== "" source: playerController.albumArtUrl fillMode: Image.PreserveAspectFit } - - // Checkbox to embed album art CheckBox { id: embedAlbumArtCheckBox text: qsTr("Embed album cover art") @@ -248,17 +244,13 @@ Window { onCheckedChanged: playerController.embedAlbumArt = checked } } - onAccepted: playerController.writeMetadataToFile() } - function show() { - active = true - item.open() - } + function show() { active = true; item.open() } } // ----------------------------------------------------------------------- - // TagLib-not-available information dialog – lazy-loaded when first needed + // TagLib-missing dialog // ----------------------------------------------------------------------- Loader { id: taglibMissingDialogLoader @@ -270,31 +262,18 @@ Window { anchors.centerIn: parent width: Math.min(root.width - 40, 360) standardButtons: Dialog.Ok - ColumnLayout { - anchors.fill: parent - spacing: 8 - - Label { - text: qsTr("Metadata writing is not available in this build because TagLib was not found at compile time.") - wrapMode: Text.Wrap - Layout.fillWidth: true - } - Label { - text: qsTr("To enable this feature, install TagLib (e.g. libtag1-dev on Ubuntu) and rebuild the application.") - wrapMode: Text.Wrap - Layout.fillWidth: true - color: theme.mutedText - } + anchors.fill: parent; spacing: 8 + Label { text: qsTr("Metadata writing is not available in this build because TagLib was not found at compile time."); wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.text } + Label { text: qsTr("To enable this feature, install TagLib (e.g. libtag1-dev on Ubuntu) and rebuild the application."); wrapMode: Text.Wrap; Layout.fillWidth: true; color: theme.mutedText } } } - function show() { - active = true - item.open() - } + function show() { active = true; item.open() } } - // Listen for metadataResults changes to auto-show selection dialog. + // ----------------------------------------------------------------------- + // Metadata / write-result connections + // ----------------------------------------------------------------------- Connections { target: playerController function onMetadataResultsChanged() { @@ -309,342 +288,557 @@ Window { writeResultTimer.restart() } } + Timer { id: writeResultTimer; interval: 3000; onTriggered: writeResultLabel.visible = false } - // Timer to hide the write-result label after a few seconds - Timer { - id: writeResultTimer - interval: 3000 - onTriggered: writeResultLabel.visible = false + // ======================================================================= + // LAYER 0 – BACKGROUND (gradient + particles + blurred album art) + // ======================================================================= + Item { + id: backgroundLayer + anchors.fill: parent + + // Animated mesh gradient + MeshGradientBackground { + id: meshBg + anchors.fill: parent + color1: albumArtView.dominantColor1 + color2: albumArtView.dominantColor2 + color3: albumArtView.dominantColor3 + color4: albumArtView.dominantColor4 + } + + // Blurred album art environment reflection + Image { + id: bgAlbumArt + anchors.fill: parent + source: playerController.albumArtUrl + fillMode: Image.PreserveAspectCrop + visible: false + } + FastBlur { + id: bgAlbumBlur + anchors.fill: parent + source: bgAlbumArt + radius: 80 + opacity: playerController.albumArtUrl !== "" ? 0.18 : 0 + Behavior on opacity { NumberAnimation { duration: 800 } } + } + + // Floating particles + ParticleField { + anchors.fill: parent + particleColor: theme.accent + particleCount: 30 + maxOpacity: 0.18 + } } - ColumnLayout { + // ======================================================================= + // PARALLAX MOUSE TRACKER + // ======================================================================= + MouseArea { + id: parallaxTracker anchors.fill: parent - anchors.margins: 16 - spacing: 12 - - // Theme and language toggles - RowLayout { - Layout.fillWidth: true - Item { Layout.fillWidth: true } - Button { - text: translationManager.language === "zh" ? "English" : "中文" - flat: true - font.pointSize: 9 - Accessible.name: translationManager.language === "zh" - ? qsTr("Switch to English") : qsTr("Switch to Chinese") - onClicked: translationManager.setLanguage( - translationManager.language === "zh" ? "en" : "zh") - } - Button { - text: root.darkTheme ? qsTr("☀ Light") : qsTr("🌙 Dark") - flat: true - font.pointSize: 9 - Accessible.name: root.darkTheme ? qsTr("Switch to light theme") : qsTr("Switch to dark theme") - onClicked: root.darkTheme = !root.darkTheme - } + hoverEnabled: true + acceptedButtons: Qt.NoButton + propagateComposedEvents: true + onPositionChanged: function(mouse) { + root.parallaxX = (mouse.x / root.width - 0.5) * 2 + root.parallaxY = (mouse.y / root.height - 0.5) * 2 } + } - // Current track display with metadata - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - // Album cover art display - Image { - id: albumArtImage - Layout.alignment: Qt.AlignHCenter - Layout.preferredWidth: 160 - Layout.preferredHeight: 160 - visible: playerController.albumArtUrl !== "" - source: playerController.albumArtUrl - fillMode: Image.PreserveAspectFit - - Rectangle { - anchors.fill: parent - color: "transparent" - border.color: theme.border - border.width: 1 - radius: 4 - } - } + // ======================================================================= + // LAYER 1 – MAIN CONTENT (inside glass panel with parallax offset) + // ======================================================================= + Item { + id: contentWrapper + anchors.fill: parent + anchors.margins: 12 + + // Subtle parallax shift + transform: Translate { + x: root.parallaxX * 3 + y: root.parallaxY * 3 + Behavior on x { SpringAnimation { spring: 1.5; damping: 0.5 } } + Behavior on y { SpringAnimation { spring: 1.5; damping: 0.5 } } + } - Text { - Layout.fillWidth: true - text: { - if (playerController.trackTitle !== "") - return playerController.trackTitle - if (playerController.currentTrack !== "") - return qsTr("Now Playing: ") + playerController.currentTrack - return qsTr("No Track Loaded") + GlassPanel { + id: mainPanel + anchors.fill: parent + backgroundSource: backgroundLayer + blurRadius: theme.glassBlur + tintColor: theme.glassTint + borderOpacity: theme.glassBorder + cornerRadius: theme.glassRadius + + // Breathing animation tied to playback + scale: playerController.isPlaying ? breathAnim.value : 1.0 + transformOrigin: Item.Center + QtObject { + id: breathAnim + property real value: 1.0 + NumberAnimation on value { + from: 1.0; to: 1.003; duration: 1200 + easing.type: Easing.InOutSine + loops: Animation.Infinite + running: playerController.isPlaying } - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideMiddle - font.pointSize: 12 - font.bold: true - color: theme.text } - Text { - Layout.fillWidth: true - visible: playerController.trackArtist !== "" - text: playerController.trackArtist - + (playerController.trackAlbum !== "" - ? " — " + playerController.trackAlbum : "") - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideMiddle - font.pointSize: 10 - color: theme.secondaryText - } + Flickable { + id: mainFlickable + anchors.fill: parent + anchors.margins: 16 + contentHeight: mainColumn.implicitHeight + clip: true + flickableDirection: Flickable.VerticalFlick + boundsBehavior: Flickable.DragOverBounds + + // Spring physics for overscroll + rebound: Transition { + NumberAnimation { + properties: "x,y" + duration: 600 + easing.type: Easing.OutElastic + easing.amplitude: 1.0 + easing.period: 0.4 + } + } - // Metadata action buttons - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: 8 - visible: playerController.trackTitle !== "" + ColumnLayout { + id: mainColumn + width: mainFlickable.width + spacing: 16 - Button { - text: qsTr("Choose Result...") - font.pointSize: 9 - visible: playerController.metadataResults.length > 1 - onClicked: metadataSelectionDialogLoader.show() - } + // ------------------------------------------------------- + // HEADER – language + theme toggles + // ------------------------------------------------------- + RowLayout { + Layout.fillWidth: true + Item { Layout.fillWidth: true } + + GlassButton { + text: translationManager.language === "zh" ? "EN" : "中" + textColor: theme.text + borderColor: theme.border + baseColor: theme.buttonBg + hoverColor: theme.buttonHoverBg + cornerRadius: 10 + Accessible.name: translationManager.language === "zh" + ? qsTr("Switch to English") : qsTr("Switch to Chinese") + onClicked: translationManager.setLanguage( + translationManager.language === "zh" ? "en" : "zh") + } - Button { - text: qsTr("Save to File") - font.pointSize: 9 - onClicked: { - if (playerController.metadataWriteSupported) - writeMetadataDialogLoader.show() - else - taglibMissingDialogLoader.show() + GlassButton { + iconText: root.darkTheme ? "☀" : "🌙" + text: "" + textColor: theme.text + borderColor: theme.border + baseColor: theme.buttonBg + hoverColor: theme.buttonHoverBg + cornerRadius: 10 + Accessible.name: root.darkTheme ? qsTr("Switch to light theme") : qsTr("Switch to dark theme") + onClicked: root.darkTheme = !root.darkTheme + } } - } - } - // Fingerprint availability indicator - Label { - Layout.alignment: Qt.AlignHCenter - visible: playerController.fingerprintAvailable && playerController.currentTrack !== "" - text: "🎵 " + qsTr("Audio fingerprint identification active") - font.pointSize: 8 - color: theme.success - } + // ------------------------------------------------------- + // ALBUM ART – holographic 3D display + // ------------------------------------------------------- + AlbumArtDisplay { + id: albumArtView + Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: Math.min(mainFlickable.width * 0.55, 220) + Layout.preferredHeight: Layout.preferredWidth + source: playerController.albumArtUrl + isPlaying: playerController.isPlaying + parallaxX: root.parallaxX + parallaxY: root.parallaxY + } - // Write result notification - Label { - id: writeResultLabel - Layout.alignment: Qt.AlignHCenter - visible: false - font.pointSize: 9 - color: theme.info - } - } + // ------------------------------------------------------- + // TRACK INFO + // ------------------------------------------------------- + ColumnLayout { + Layout.fillWidth: true + spacing: 4 + + Text { + Layout.fillWidth: true + text: { + if (playerController.trackTitle !== "") + return playerController.trackTitle + if (playerController.currentTrack !== "") + return qsTr("Now Playing: ") + playerController.currentTrack + return qsTr("No Track Loaded") + } + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideMiddle + font.pixelSize: 17 + font.weight: Font.DemiBold + font.family: "sans-serif" + color: theme.text + + // Subtle glow on track title + layer.enabled: playerController.trackTitle !== "" + layer.effect: Glow { + radius: 4; samples: 9; spread: 0.05 + color: Qt.rgba(theme.accent.r, theme.accent.g, + theme.accent.b, 0.25) + } + } - // Progress bar - ColumnLayout { - Layout.fillWidth: true - spacing: 2 - - Slider { - id: progressSlider - Layout.fillWidth: true - from: 0 - to: playerController.duration > 0 ? playerController.duration : 1 - value: playerController.position - enabled: playerController.duration > 0 - onMoved: playerController.seek(value) - } + Text { + Layout.fillWidth: true + visible: playerController.trackArtist !== "" + text: playerController.trackArtist + + (playerController.trackAlbum !== "" + ? " — " + playerController.trackAlbum : "") + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideMiddle + font.pixelSize: 13 + font.family: "sans-serif" + color: theme.secondaryText + } - RowLayout { - Layout.fillWidth: true - Label { - text: formatTime(playerController.position) - font.pointSize: 9 - color: theme.mutedText - } - Item { Layout.fillWidth: true } - Label { - text: formatTime(playerController.duration) - font.pointSize: 9 - color: theme.mutedText - } - } - } + // Metadata action buttons + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + visible: playerController.trackTitle !== "" + + GlassButton { + text: qsTr("Choose Result...") + textColor: theme.text + borderColor: theme.border + baseColor: theme.buttonBg + hoverColor: theme.buttonHoverBg + visible: playerController.metadataResults.length > 1 + onClicked: metadataSelectionDialogLoader.show() + } + GlassButton { + text: qsTr("Save to File") + textColor: theme.text + borderColor: theme.border + baseColor: theme.buttonBg + hoverColor: theme.buttonHoverBg + onClicked: { + if (playerController.metadataWriteSupported) + writeMetadataDialogLoader.show() + else + taglibMissingDialogLoader.show() + } + } + } - // Transport controls - Row { - Layout.alignment: Qt.AlignHCenter - spacing: 8 + // Fingerprint indicator + Text { + Layout.alignment: Qt.AlignHCenter + visible: playerController.fingerprintAvailable && playerController.currentTrack !== "" + text: "♪ " + qsTr("Audio fingerprint identification active") + font.pixelSize: 10 + font.family: "sans-serif" + color: theme.success + } - Button { - text: qsTr("Previous") - onClicked: playerController.previous() - } - Button { - text: playerController.isPlaying ? qsTr("Pause") : qsTr("Play") - onClicked: { - if (playerController.isPlaying) - playerController.pause() - else - playerController.play() - } - } - Button { - text: qsTr("Stop") - onClicked: playerController.stop() - } - Button { - text: qsTr("Next") - onClicked: playerController.next() - } - } + // Write result notification + Text { + id: writeResultLabel + Layout.alignment: Qt.AlignHCenter + visible: false + font.pixelSize: 11 + font.family: "sans-serif" + color: theme.info + } + } - // Volume control - Row { - Layout.alignment: Qt.AlignHCenter - spacing: 8 - - Label { text: qsTr("Volume") } - Slider { - id: volumeSlider - from: 0.0 - to: 1.0 - value: playerController.volume - onMoved: playerController.volume = value - implicitWidth: 200 - } - Label { text: Math.round(volumeSlider.value * 100) + "%" } - } + // ------------------------------------------------------- + // PROGRESS BAR – glowing flow line + // ------------------------------------------------------- + ColumnLayout { + Layout.fillWidth: true + spacing: 2 + + GlowSlider { + id: progressSlider + Layout.fillWidth: true + from: 0 + to: playerController.duration > 0 ? playerController.duration : 1 + value: playerController.position + enabled: playerController.duration > 0 + glowColor: theme.accent + trackColor: root.darkTheme ? Qt.rgba(1,1,1,0.08) : Qt.rgba(0,0,0,0.08) + onMoved: playerController.seek(value) + } - // Add track buttons (file picker) - Row { - Layout.alignment: Qt.AlignHCenter - spacing: 8 + RowLayout { + Layout.fillWidth: true + Text { + text: formatTime(playerController.position) + font.pixelSize: 10; font.family: "sans-serif" + color: theme.mutedText + } + Item { Layout.fillWidth: true } + Text { + text: formatTime(playerController.duration) + font.pixelSize: 10; font.family: "sans-serif" + color: theme.mutedText + } + } + } - Button { - text: qsTr("Add Files...") - onClicked: fileDialog.open() - } - Button { - text: qsTr("Add Folder...") - onClicked: folderDialog.open() - } - } + // ------------------------------------------------------- + // TRANSPORT CONTROLS – glass buttons with icons + // ------------------------------------------------------- + Row { + Layout.alignment: Qt.AlignHCenter + spacing: 10 + + GlassButton { + iconText: "⏮"; text: "" + textColor: theme.text; borderColor: theme.border + baseColor: theme.buttonBg; hoverColor: theme.buttonHoverBg + cornerRadius: 20 + Accessible.name: qsTr("Previous") + onClicked: playerController.previous() + } + GlassButton { + iconText: playerController.isPlaying ? "⏸" : "▶" + text: "" + isAccent: true + accentGlow: theme.accent + textColor: theme.text; borderColor: theme.border + baseColor: Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.15) + hoverColor: Qt.rgba(theme.accent.r, theme.accent.g, theme.accent.b, 0.25) + cornerRadius: 24 + leftPadding: 18; rightPadding: 18; topPadding: 12; bottomPadding: 12 + Accessible.name: playerController.isPlaying ? qsTr("Pause") : qsTr("Play") + onClicked: { + if (playerController.isPlaying) playerController.pause() + else playerController.play() + } + } + GlassButton { + iconText: "⏹"; text: "" + textColor: theme.text; borderColor: theme.border + baseColor: theme.buttonBg; hoverColor: theme.buttonHoverBg + cornerRadius: 20 + Accessible.name: qsTr("Stop") + onClicked: playerController.stop() + } + GlassButton { + iconText: "⏭"; text: "" + textColor: theme.text; borderColor: theme.border + baseColor: theme.buttonBg; hoverColor: theme.buttonHoverBg + cornerRadius: 20 + Accessible.name: qsTr("Next") + onClicked: playerController.next() + } + } - // Lyrics panel (collapsible) - ColumnLayout { - Layout.fillWidth: true - spacing: 4 + // ------------------------------------------------------- + // VOLUME – glowing slider with label + // ------------------------------------------------------- + RowLayout { + Layout.alignment: Qt.AlignHCenter + spacing: 10 + + // Volume icon with brightness based on level + Text { + text: volumeSlider.value > 0.5 ? "🔊" + : (volumeSlider.value > 0 ? "🔉" : "🔇") + font.pixelSize: 16 + opacity: 0.4 + volumeSlider.value * 0.6 + Behavior on opacity { NumberAnimation { duration: 200 } } + } - RowLayout { - Layout.fillWidth: true - Label { - text: qsTr("Lyrics") - font.bold: true - } - Item { Layout.fillWidth: true } - Button { - id: lyricsToggle - text: lyricsPane.visible ? "▲" : "▼" - flat: true - font.pointSize: 9 - implicitWidth: 32 - Accessible.name: lyricsPane.visible ? qsTr("Collapse lyrics") : qsTr("Expand lyrics") - onClicked: lyricsPane.visible = !lyricsPane.visible - } - } + GlowSlider { + id: volumeSlider + from: 0.0; to: 1.0 + value: playerController.volume + glowColor: theme.accent + trackColor: root.darkTheme ? Qt.rgba(1,1,1,0.08) : Qt.rgba(0,0,0,0.08) + implicitWidth: 160 + onMoved: playerController.volume = value + } - ScrollView { - id: lyricsPane - Layout.fillWidth: true - Layout.preferredHeight: 120 - visible: playerController.lyrics !== "" - clip: true + Text { + text: Math.round(volumeSlider.value * 100) + "%" + font.pixelSize: 11; font.family: "sans-serif" + color: theme.mutedText + Layout.preferredWidth: 32 + } + } - TextArea { - readOnly: true - wrapMode: TextEdit.Wrap - text: playerController.lyrics - font.pointSize: 10 - color: theme.text - background: Rectangle { - color: theme.surfaceBg - radius: 4 + // ------------------------------------------------------- + // ADD FILES / FOLDER + // ------------------------------------------------------- + Row { + Layout.alignment: Qt.AlignHCenter + spacing: 8 + + GlassButton { + iconText: "+"; text: qsTr("Files") + textColor: theme.text; borderColor: theme.border + baseColor: theme.buttonBg; hoverColor: theme.buttonHoverBg + onClicked: fileDialog.open() + } + GlassButton { + iconText: "📂"; text: qsTr("Folder") + textColor: theme.text; borderColor: theme.border + baseColor: theme.buttonBg; hoverColor: theme.buttonHoverBg + onClicked: folderDialog.open() + } } - } - } - Label { - visible: playerController.lyrics === "" && playerController.currentTrack !== "" - text: qsTr("(no lyrics available)") - color: theme.placeholderText - font.pointSize: 9 - } - } + // ------------------------------------------------------- + // LYRICS PANEL – immersive depth lyrics + // ------------------------------------------------------- + ColumnLayout { + Layout.fillWidth: true + spacing: 4 - // Playlist header - Label { - text: qsTr("Playlist") + " (" + playerController.trackCount + ")" - font.bold: true - } + RowLayout { + Layout.fillWidth: true + Text { + text: qsTr("Lyrics") + font.pixelSize: 13; font.weight: Font.DemiBold + font.family: "sans-serif" + color: theme.text + } + Item { Layout.fillWidth: true } + GlassButton { + iconText: lyricsPanel.visible ? "▲" : "▼" + text: "" + textColor: theme.mutedText + borderColor: "transparent" + baseColor: "transparent" + hoverColor: theme.hoverBg + cornerRadius: 8 + leftPadding: 8; rightPadding: 8; topPadding: 4; bottomPadding: 4 + Accessible.name: lyricsPanel.visible ? qsTr("Collapse lyrics") : qsTr("Expand lyrics") + onClicked: lyricsPanel.visible = !lyricsPanel.visible + } + } - // Playlist view - ListView { - id: playlistView - Layout.fillWidth: true - Layout.fillHeight: true - clip: true - cacheBuffer: 120 - model: playerController.trackList - - delegate: Rectangle { - width: playlistView.width - height: 36 - color: index === playerController.currentIndex ? theme.accentBg : (mouseArea.containsMouse ? theme.hoverBg : "transparent") - radius: 4 - - RowLayout { - anchors.fill: parent - anchors.leftMargin: 8 - anchors.rightMargin: 8 - spacing: 8 - - Label { - text: (index + 1) + "." - Layout.preferredWidth: 30 - color: index === playerController.currentIndex ? theme.accent : theme.mutedText + ImmersiveLyrics { + id: lyricsPanel + Layout.fillWidth: true + Layout.preferredHeight: 140 + visible: playerController.lyrics !== "" + lyrics: playerController.lyrics + textColor: theme.text + glowColor: theme.accent + } + + Text { + visible: playerController.lyrics === "" && playerController.currentTrack !== "" + text: qsTr("(no lyrics available)") + color: theme.placeholderText + font.pixelSize: 11; font.family: "sans-serif" + } } - Label { - text: modelData + + // ------------------------------------------------------- + // PLAYLIST + // ------------------------------------------------------- + ColumnLayout { Layout.fillWidth: true - elide: Text.ElideMiddle - font.bold: index === playerController.currentIndex - } - Button { - text: qsTr("Remove") - flat: true - font.pointSize: 9 - onClicked: playerController.removeTrack(index) - } - } + spacing: 6 - MouseArea { - id: mouseArea - anchors.fill: parent - hoverEnabled: true - // Ignore clicks on the Remove button area - acceptedButtons: Qt.LeftButton - onDoubleClicked: playerController.selectTrack(index) - z: -1 - } - } + Text { + text: qsTr("Playlist") + " (" + playerController.trackCount + ")" + font.pixelSize: 13; font.weight: Font.DemiBold + font.family: "sans-serif" + color: theme.text + } + + // Playlist items (use Repeater inside Column since we're in a Flickable) + Column { + Layout.fillWidth: true + spacing: 2 - // Empty-playlist placeholder - Label { - anchors.centerIn: parent - visible: playerController.trackCount === 0 - text: qsTr("(playlist is empty)") - color: theme.placeholderText + Repeater { + model: playerController.trackList + + Rectangle { + width: parent.width + height: 38 + radius: 8 + color: index === playerController.currentIndex + ? theme.accentBg + : (plMouseArea.containsMouse ? theme.hoverBg : "transparent") + border.width: index === playerController.currentIndex ? 1 : 0 + border.color: Qt.rgba(theme.accent.r, theme.accent.g, + theme.accent.b, 0.3) + + Behavior on color { + ColorAnimation { duration: 200; easing.type: Easing.OutCubic } + } + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 10; anchors.rightMargin: 10 + spacing: 8 + + Text { + text: (index + 1) + "." + Layout.preferredWidth: 28 + font.pixelSize: 11; font.family: "sans-serif" + color: index === playerController.currentIndex + ? theme.accent : theme.mutedText + } + Text { + text: modelData + Layout.fillWidth: true + elide: Text.ElideMiddle + font.pixelSize: 12; font.family: "sans-serif" + font.weight: index === playerController.currentIndex + ? Font.DemiBold : Font.Normal + color: theme.text + } + GlassButton { + iconText: "✕"; text: "" + textColor: theme.mutedText + borderColor: "transparent" + baseColor: "transparent" + hoverColor: Qt.rgba(1, 0.3, 0.3, 0.15) + cornerRadius: 6 + leftPadding: 6; rightPadding: 6; topPadding: 2; bottomPadding: 2 + Accessible.name: qsTr("Remove") + onClicked: playerController.removeTrack(index) + } + } + + MouseArea { + id: plMouseArea + anchors.fill: parent + hoverEnabled: true + acceptedButtons: Qt.LeftButton + onDoubleClicked: playerController.selectTrack(index) + z: -1 + } + } + } + + // Empty playlist placeholder + Item { + width: parent.width + height: 60 + visible: playerController.trackCount === 0 + Text { + anchors.centerIn: parent + text: qsTr("(playlist is empty)") + color: theme.placeholderText + font.pixelSize: 12; font.family: "sans-serif" + } + } + } + } + } } } } From 72bbc14109c8299cd1b895518af2bbc3f1b8300c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:48:44 +0000 Subject: [PATCH 2/2] fix: address code review feedback - accent color logic, breathing amplitude, comment Co-authored-by: MollycodeX <114975977+MollycodeX@users.noreply.github.com> Agent-Logs-Url: https://github.com/MollycodeX/MSCPLAYER/sessions/d36ba381-7696-4ad0-8b15-d84c80ed2541 --- src/AlbumArtDisplay.qml | 2 +- src/main.qml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AlbumArtDisplay.qml b/src/AlbumArtDisplay.qml index d53711a..c5c8ab8 100644 --- a/src/AlbumArtDisplay.qml +++ b/src/AlbumArtDisplay.qml @@ -131,7 +131,7 @@ Item { RotationAnimation on rotation { from: 0 to: 360 - duration: 30000 // 30s per revolution + duration: 30000 // 30 seconds per revolution loops: Animation.Infinite running: root.isPlaying && root.source !== "" paused: !root.isPlaying diff --git a/src/main.qml b/src/main.qml index 795ba36..d55738b 100644 --- a/src/main.qml +++ b/src/main.qml @@ -34,7 +34,7 @@ Window { readonly property color mutedText: root.darkTheme ? Qt.rgba(1,1,1,0.3) : Qt.rgba(0,0,0,0.35) readonly property color placeholderText: root.darkTheme ? Qt.rgba(1,1,1,0.2) : Qt.rgba(0,0,0,0.25) // Accent derived from album art (or default blue) - readonly property color accent: albumArtView.dominantColor1 !== "#1a0533" + readonly property color accent: albumArtView.dominantColor2 !== "#0a1628" ? albumArtView.dominantColor2 : (root.darkTheme ? "#5599ff" : "#3366cc") readonly property color accentBg: Qt.rgba(theme.accent.r, theme.accent.g, @@ -380,7 +380,7 @@ Window { id: breathAnim property real value: 1.0 NumberAnimation on value { - from: 1.0; to: 1.003; duration: 1200 + from: 1.0; to: 1.008; duration: 1200 easing.type: Easing.InOutSine loops: Animation.Infinite running: playerController.isPlaying