mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
qs/media: replace FBOs with ClippingRectangle and rewrite seekbar as a
shader
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.Common
|
||||
import qs.Services
|
||||
@@ -591,24 +591,11 @@ DankModal {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Rectangle {
|
||||
id: gridProgressMask
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: gridButtonRect.isHolding
|
||||
layer.enabled: gridButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: gridProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
@@ -729,24 +716,11 @@ DankModal {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Rectangle {
|
||||
id: listProgressMask
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: listButtonRect.isHolding
|
||||
layer.enabled: listButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: listProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -240,8 +240,10 @@ DankPopout {
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
if (root.shouldBeVisible)
|
||||
mainContainer.forceActiveFocus();
|
||||
if (!root.shouldBeVisible)
|
||||
return;
|
||||
mainContainer.forceActiveFocus();
|
||||
tabBar.snapIndicator();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,11 @@ Item {
|
||||
property string section: ""
|
||||
property int barPosition: SettingsData.Position.Top
|
||||
|
||||
readonly property color accent: MediaAccentService.accent
|
||||
readonly property color onAccent: MediaAccentService.onAccent
|
||||
readonly property color accentHover: MediaAccentService.accentHover
|
||||
readonly property color accentPressed: MediaAccentService.accentPressed
|
||||
|
||||
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
|
||||
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
|
||||
@@ -298,8 +303,16 @@ Item {
|
||||
Component.onCompleted: syncArt()
|
||||
|
||||
function syncArt() {
|
||||
if (curArt === "" || layerA.art == curArt || layerB.art == curArt)
|
||||
if (curArt === "")
|
||||
return;
|
||||
const frontArt = _showA ? layerA.art : layerB.art;
|
||||
const backArt = _showA ? layerB.art : layerA.art;
|
||||
if (frontArt == curArt)
|
||||
return;
|
||||
if (backArt == curArt) {
|
||||
_showA = !_showA;
|
||||
return;
|
||||
}
|
||||
if (_showA)
|
||||
layerB.art = curArt;
|
||||
else
|
||||
@@ -564,13 +577,13 @@ Item {
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.centerIn: parent
|
||||
color: shuffleArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
color: shuffleArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "shuffle"
|
||||
size: 20
|
||||
color: activePlayer && activePlayer.shuffle ? Theme.primary : Theme.surfaceText
|
||||
color: activePlayer && activePlayer.shuffle ? root.accent : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -626,13 +639,13 @@ Item {
|
||||
height: 50
|
||||
radius: 25
|
||||
anchors.centerIn: parent
|
||||
color: Theme.primary
|
||||
color: root.accent
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 28
|
||||
color: Theme.background
|
||||
color: root.onAccent
|
||||
weight: 500
|
||||
}
|
||||
|
||||
@@ -696,7 +709,7 @@ Item {
|
||||
height: 40
|
||||
radius: 20
|
||||
anchors.centerIn: parent
|
||||
color: repeatArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
|
||||
color: repeatArea.containsMouse ? root.accentHover : Theme.withAlpha(root.accent, 0)
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
@@ -713,7 +726,7 @@ Item {
|
||||
}
|
||||
}
|
||||
size: 20
|
||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? Theme.primary : Theme.surfaceText
|
||||
color: activePlayer && activePlayer.loopState !== MprisLoopState.None ? root.accent : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -752,7 +765,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 185
|
||||
color: playerSelectorArea.containsMouse || playersExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
color: playerSelectorArea.containsMouse || playersExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
z: 100
|
||||
@@ -819,7 +832,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 130
|
||||
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
border.color: volumeAvailable ? Theme.outlineStrong : Theme.outlineMedium
|
||||
border.width: 1
|
||||
z: 101
|
||||
@@ -831,7 +844,7 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
name: getVolumeIcon()
|
||||
size: 18
|
||||
color: volumeAvailable && currentVolume > 0 ? Theme.primary : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
||||
color: volumeAvailable && currentVolume > 0 ? root.accent : Theme.withAlpha(Theme.surfaceText, volumeAvailable ? 1.0 : 0.5)
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -882,7 +895,7 @@ Item {
|
||||
radius: 20
|
||||
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
|
||||
y: 240
|
||||
color: audioDevicesArea.containsMouse || devicesExpanded ? Theme.primaryPressed : Theme.withAlpha(Theme.primaryPressed, 0)
|
||||
color: audioDevicesArea.containsMouse || devicesExpanded ? root.accentPressed : Theme.withAlpha(root.accentPressed, 0)
|
||||
border.color: Theme.outlineStrong
|
||||
border.width: 1
|
||||
z: 100
|
||||
|
||||
@@ -154,13 +154,13 @@ Card {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: 16
|
||||
color: Theme.primary
|
||||
color: MediaAccentService.accent
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: activePlayer?.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
||||
size: 16
|
||||
color: Theme.background
|
||||
color: MediaAccentService.onAccent
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -567,24 +567,11 @@ Rectangle {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Rectangle {
|
||||
id: gridProgressMask
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: gridButtonRect.isHolding
|
||||
layer.enabled: gridButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: gridProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
@@ -700,24 +687,11 @@ Rectangle {
|
||||
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: isSelected ? 2 : 0
|
||||
|
||||
Rectangle {
|
||||
id: listProgressMask
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: parent.radius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
Item {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
visible: listButtonRect.isHolding
|
||||
layer.enabled: listButtonRect.isHolding
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: listProgressMask
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.left: parent.left
|
||||
|
||||
@@ -4,6 +4,7 @@ import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
import Quickshell.Services.Mpris
|
||||
import Quickshell.Widgets
|
||||
|
||||
DankOSD {
|
||||
id: root
|
||||
@@ -185,10 +186,11 @@ DankOSD {
|
||||
visible: false
|
||||
}
|
||||
|
||||
Item {
|
||||
id: blurredBg
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
visible: false
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
opacity: 0.7
|
||||
|
||||
MultiEffect {
|
||||
anchors.centerIn: parent
|
||||
@@ -203,24 +205,6 @@ DankOSD {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: bgMask
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
source: blurredBg
|
||||
maskEnabled: true
|
||||
maskSource: bgMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1.0
|
||||
opacity: 0.7
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
@@ -520,28 +520,27 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
Image {
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: {
|
||||
var wp = Theme.wallpaperPath;
|
||||
if (!wp || wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
|
||||
sourceSize.width: 120
|
||||
sourceSize.height: 120
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: autoWallpaperMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = Theme.wallpaperPath;
|
||||
if (!wp || wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: Theme.wallpaperPath && !Theme.wallpaperPath.startsWith("#")
|
||||
sourceSize.width: 120
|
||||
sourceSize.height: 120
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -553,16 +552,6 @@ Item {
|
||||
visible: Theme.wallpaperPath && Theme.wallpaperPath.startsWith("#")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: autoWallpaperMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: (ToastService.wallpaperErrorStatus === "error" || ToastService.wallpaperErrorStatus === "matugen_missing") ? "error" : "palette"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Modals.FileBrowser
|
||||
import qs.Services
|
||||
@@ -75,28 +75,27 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
Image {
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: {
|
||||
var wp = root.currentWallpaper;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: wallpaperMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = root.currentWallpaper;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: root.currentWallpaper !== "" && !root.currentWallpaper.startsWith("#")
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,16 +107,6 @@ Item {
|
||||
visible: root.currentWallpaper !== "" && root.currentWallpaper.startsWith("#")
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: wallpaperMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "image"
|
||||
@@ -421,31 +410,30 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
Image {
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathLight;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var lightWallpaper = SessionData.wallpaperPathLight;
|
||||
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: lightMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathLight;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var lightWallpaper = SessionData.wallpaperPathLight;
|
||||
return lightWallpaper !== "" && !lightWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,16 +451,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: lightMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "light_mode"
|
||||
@@ -611,31 +589,30 @@ Item {
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.surfaceVariant
|
||||
|
||||
Image {
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathDark;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var darkWallpaper = SessionData.wallpaperPathDark;
|
||||
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: darkMask
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
var wp = SessionData.wallpaperPathDark;
|
||||
if (wp === "" || wp.startsWith("#"))
|
||||
return "";
|
||||
if (wp.startsWith("file://"))
|
||||
wp = wp.substring(7);
|
||||
return "file://" + wp.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
visible: {
|
||||
var darkWallpaper = SessionData.wallpaperPathDark;
|
||||
return darkWallpaper !== "" && !darkWallpaper.startsWith("#");
|
||||
}
|
||||
sourceSize.width: 160
|
||||
sourceSize.height: 160
|
||||
asynchronous: true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -653,16 +630,6 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: darkMask
|
||||
anchors.fill: parent
|
||||
anchors.margins: 1
|
||||
radius: Theme.cornerRadius - 1
|
||||
color: "black"
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "dark_mode"
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
@@ -58,23 +58,6 @@ Item {
|
||||
visible: intersectsViewport
|
||||
opacity: (monitorObj?.id ?? -1) == widgetMonitorId ? 1 : 0.4
|
||||
|
||||
Rectangle {
|
||||
id: maskRect
|
||||
width: root.width
|
||||
height: root.height
|
||||
radius: Theme.cornerRadius
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
maskEnabled: true
|
||||
maskSource: maskRect
|
||||
maskSpreadAtMin: 1
|
||||
maskThresholdMin: 0.5
|
||||
}
|
||||
|
||||
Behavior on x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
@@ -104,51 +87,55 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
|
||||
live: true
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Rectangle {
|
||||
ScreencopyView {
|
||||
id: windowPreview
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) :
|
||||
hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) :
|
||||
Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||
border.width: 1
|
||||
}
|
||||
captureSource: root.overviewOpen ? root.toplevel?.wayland : null
|
||||
live: true
|
||||
|
||||
ColumnLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.fontSizeSmall * 0.5
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pressed ? Theme.withAlpha(Theme.surfaceContainerHigh, 0.5) : hovered ? Theme.withAlpha(Theme.surfaceVariant, 0.3) : Theme.withAlpha(Theme.surfaceContainer, 0.1)
|
||||
border.color: Theme.withAlpha(Theme.outline, 0.3)
|
||||
border.width: 1
|
||||
}
|
||||
|
||||
Image {
|
||||
id: windowIcon
|
||||
property var iconSize: {
|
||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1)
|
||||
}
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(iconSize, iconSize)
|
||||
ColumnLayout {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
spacing: Theme.fontSizeSmall * 0.5
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
Image {
|
||||
id: windowIcon
|
||||
property var iconSize: {
|
||||
return Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) / (root.monitorData?.scale ?? 1);
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
source: root.iconPath
|
||||
width: iconSize
|
||||
height: iconSize
|
||||
sourceSize: Qt.size(iconSize, iconSize)
|
||||
|
||||
Behavior on width {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
// Accent color extracted from the current track's album art via ColorQuantizer,
|
||||
// falling back to Theme.primary when no usable accent is available.
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property bool hasAccent: _accent !== null
|
||||
readonly property color accent: _accent !== null ? _accent : Theme.primary
|
||||
|
||||
readonly property color onAccent: {
|
||||
const c = accent;
|
||||
const lum = 0.2126 * c.r + 0.7152 * c.g + 0.0722 * c.b;
|
||||
return lum > 0.6 ? Qt.rgba(0, 0, 0, 1) : Qt.rgba(1, 1, 1, 1);
|
||||
}
|
||||
|
||||
readonly property color accentHover: Theme.withAlpha(accent, 0.12)
|
||||
readonly property color accentPressed: Theme.withAlpha(accent, Theme.transparentBlurLayers ? 0.24 : 0.16)
|
||||
|
||||
// Plain-named alias: underscore-prefixed props with onChanged handlers crash config load.
|
||||
readonly property string artUrl: TrackArtService.resolvedArtUrl
|
||||
onArtUrlChanged: {
|
||||
if (artUrl === "")
|
||||
_accent = null;
|
||||
}
|
||||
|
||||
property var _accent: null
|
||||
|
||||
ColorQuantizer {
|
||||
id: quantizer
|
||||
source: root.artUrl
|
||||
depth: 4
|
||||
rescaleSize: 64
|
||||
onColorsChanged: root._accent = root._pickAccent(colors)
|
||||
}
|
||||
|
||||
function _pickAccent(colors) {
|
||||
if (!colors || colors.length === 0)
|
||||
return null;
|
||||
|
||||
let best = null;
|
||||
let bestScore = -1;
|
||||
for (let i = 0; i < colors.length; i++) {
|
||||
const c = colors[i];
|
||||
const s = c.hsvSaturation;
|
||||
const v = c.hsvValue;
|
||||
if (v < 0.22 || v > 0.96 || s < 0.22)
|
||||
continue;
|
||||
const score = s * (1 - Math.abs(v - 0.68));
|
||||
if (score > bestScore) {
|
||||
bestScore = score;
|
||||
best = c;
|
||||
}
|
||||
}
|
||||
|
||||
if (!best)
|
||||
return null;
|
||||
return _normalize(best);
|
||||
}
|
||||
|
||||
function _normalize(c) {
|
||||
const hue = c.hsvHue < 0 ? 0 : c.hsvHue;
|
||||
const s = Math.min(1, c.hsvSaturation * 1.05);
|
||||
const v = Math.max(c.hsvValue, 0.62);
|
||||
return Qt.hsva(hue, s, v, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
#version 450
|
||||
|
||||
layout(location = 0) in vec2 qt_TexCoord0;
|
||||
layout(location = 0) out vec4 fragColor;
|
||||
|
||||
layout(std140, binding = 0) uniform buf {
|
||||
mat4 qt_Matrix;
|
||||
float qt_Opacity;
|
||||
float widthPx;
|
||||
float heightPx;
|
||||
float value;
|
||||
float actualValue;
|
||||
float phase;
|
||||
float ampPx;
|
||||
float wavelengthPx;
|
||||
float lineWidthPx;
|
||||
float showActual;
|
||||
vec4 fillColor;
|
||||
vec4 trackColor;
|
||||
vec4 playheadColor;
|
||||
vec4 actualColor;
|
||||
} ubuf;
|
||||
|
||||
const float TAU = 6.28318530718;
|
||||
const float AA = 0.75; // pixel-space antialias band
|
||||
|
||||
// Signed distance to a rounded box centered at the origin.
|
||||
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
|
||||
vec2 q = abs(p) - halfSize + vec2(r);
|
||||
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
|
||||
}
|
||||
|
||||
// Composite a straight-alpha color over a premultiplied accumulator.
|
||||
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
|
||||
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
|
||||
}
|
||||
|
||||
void main() {
|
||||
float w = ubuf.widthPx;
|
||||
float h = ubuf.heightPx;
|
||||
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
|
||||
|
||||
float mid = h * 0.5;
|
||||
float halfW = ubuf.lineWidthPx * 0.5;
|
||||
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
|
||||
|
||||
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
|
||||
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
|
||||
bool seeking = ubuf.showActual > 0.5;
|
||||
|
||||
float loX = min(playX, actualX);
|
||||
float hiX = max(playX, actualX);
|
||||
float fillEnd = seeking ? loX : playX; // filled progress ends here
|
||||
float actStart = seeking ? loX : playX; // seek-preview segment
|
||||
float actEnd = seeking ? hiX : playX;
|
||||
float trackStart = seeking ? hiX : playX; // unplayed remainder
|
||||
|
||||
// Perpendicular distance to the animated sine stroke.
|
||||
float ang = k * px.x + ubuf.phase;
|
||||
float wy = mid + ubuf.ampPx * sin(ang);
|
||||
float slope = ubuf.ampPx * k * cos(ang);
|
||||
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
|
||||
float aaW = AA;
|
||||
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
|
||||
|
||||
// Straight remainder line.
|
||||
float dLine = abs(px.y - mid);
|
||||
float aaL = AA;
|
||||
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
|
||||
|
||||
vec4 col = vec4(0.0);
|
||||
|
||||
// 1. Track (unplayed remainder), to the right of the progress head.
|
||||
{
|
||||
float m = lineStroke * step(trackStart, px.x);
|
||||
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
|
||||
}
|
||||
|
||||
// 2. Seek-preview segment (only while seeking).
|
||||
if (seeking) {
|
||||
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
|
||||
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||
}
|
||||
|
||||
// 3. Filled progress wave.
|
||||
{
|
||||
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
|
||||
// Rounded start cap.
|
||||
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
|
||||
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
|
||||
m = max(m, capM * step(halfW - 1.0, px.x));
|
||||
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
|
||||
}
|
||||
|
||||
// 4. Actual-position marker (only while seeking).
|
||||
if (seeking) {
|
||||
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
|
||||
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
|
||||
float aa = AA;
|
||||
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
|
||||
}
|
||||
|
||||
// 5. Playhead pill (on top).
|
||||
{
|
||||
float phW = 3.5;
|
||||
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
|
||||
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
|
||||
float aa = AA;
|
||||
float m = 1.0 - smoothstep(-aa, aa, d);
|
||||
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
|
||||
}
|
||||
|
||||
fragColor = col * ubuf.qt_Opacity;
|
||||
}
|
||||
Binary file not shown.
@@ -152,7 +152,8 @@ Item {
|
||||
property real energy: 0
|
||||
property vector4d bandsA: Qt.vector4d(0, 0, 0, 0)
|
||||
property vector2d bandsB: Qt.vector2d(0, 0)
|
||||
property vector4d fillColor: Qt.vector4d(Theme.primary.r, Theme.primary.g, Theme.primary.b, Theme.primary.a)
|
||||
readonly property color accentColor: MediaAccentService.accent
|
||||
property vector4d fillColor: Qt.vector4d(accentColor.r, accentColor.g, accentColor.b, accentColor.a)
|
||||
|
||||
Behavior on activation {
|
||||
NumberAnimation {
|
||||
@@ -173,7 +174,7 @@ Item {
|
||||
|
||||
imageSource: artUrl || lastValidArtUrl || ""
|
||||
fallbackIcon: "album"
|
||||
border.color: Theme.primary
|
||||
border.color: MediaAccentService.accent
|
||||
border.width: 2
|
||||
|
||||
onImageSourceChanged: {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import QtQuick.Effects
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
@@ -65,67 +65,44 @@ Rectangle {
|
||||
border.color: "transparent"
|
||||
border.width: 0
|
||||
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: false
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
radius: Math.min(width, height) / 2
|
||||
color: "transparent"
|
||||
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: false
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
MultiEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
source: root.activeImage
|
||||
maskEnabled: true
|
||||
maskSource: circularMask
|
||||
visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
||||
Item {
|
||||
id: circularMask
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - 4
|
||||
height: parent.height - 4
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
|
||||
AnimatedImage {
|
||||
id: probe
|
||||
anchors.fill: parent
|
||||
radius: width / 2
|
||||
color: "black"
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
|
||||
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
|
||||
}
|
||||
|
||||
// Takes over once the probe settles on a non-animated image, then latches.
|
||||
Image {
|
||||
id: staticImage
|
||||
anchors.fill: parent
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
smooth: true
|
||||
mipmap: true
|
||||
cache: root.cacheImages
|
||||
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
|
||||
sourceSize.width: Math.max(width * 2, 128)
|
||||
sourceSize.height: Math.max(height * 2, 128)
|
||||
source: {
|
||||
if (!root.shouldProbe)
|
||||
return root.imageSource;
|
||||
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
|
||||
return root.imageSource;
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -142,6 +142,8 @@ Item {
|
||||
value: root.value
|
||||
actualValue: root.playerValue
|
||||
showActualPlaybackState: root.isSeeking
|
||||
fillColor: MediaAccentService.accent
|
||||
playheadColor: MediaAccentService.accent
|
||||
actualProgressColor: Theme.onSurface_38
|
||||
isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
||||
|
||||
@@ -179,8 +181,8 @@ Item {
|
||||
Item {
|
||||
property real lineWidth: 3
|
||||
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
|
||||
property color fillColor: Theme.primary
|
||||
property color playheadColor: Theme.primary
|
||||
property color fillColor: MediaAccentService.accent
|
||||
property color playheadColor: MediaAccentService.accent
|
||||
property color actualProgressColor: Theme.onSurface_38
|
||||
readonly property real midY: height / 2
|
||||
|
||||
|
||||
@@ -256,6 +256,11 @@ FocusScope {
|
||||
}
|
||||
}
|
||||
|
||||
function snapIndicator() {
|
||||
indicator.initialSetupComplete = false;
|
||||
updateIndicator();
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
Qt.callLater(updateIndicator);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
|
||||
// Wave progress indicator: track, animated fill, seek preview and playhead are
|
||||
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -19,19 +20,6 @@ Item {
|
||||
property color playheadColor: Theme.primary
|
||||
property color actualProgressColor: Theme.onSurface_38
|
||||
|
||||
property real dpr: (root.window ? root.window.devicePixelRatio : 1)
|
||||
function snap(v) {
|
||||
return Math.round(v * dpr) / dpr;
|
||||
}
|
||||
|
||||
readonly property real playX: snap(root.width * root.value)
|
||||
readonly property real actualX: snap(root.width * root.actualValue)
|
||||
readonly property real midY: snap(height / 2)
|
||||
readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
|
||||
readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
|
||||
readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
|
||||
readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
|
||||
|
||||
Behavior on currentAmp {
|
||||
NumberAnimation {
|
||||
duration: 300
|
||||
@@ -39,236 +27,34 @@ Item {
|
||||
}
|
||||
}
|
||||
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
|
||||
Component.onCompleted: currentAmp = isPlaying ? amp : 0
|
||||
|
||||
Shape {
|
||||
id: flatTrack
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
layer.enabled: true
|
||||
blending: true
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.trackColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathMove {
|
||||
id: flatStart
|
||||
x: Math.min(root.width, snap(root.playX + playhead.width / 2))
|
||||
y: root.midY
|
||||
}
|
||||
PathLine {
|
||||
id: flatEnd
|
||||
x: root.width
|
||||
y: root.midY
|
||||
}
|
||||
}
|
||||
readonly property real widthPx: width
|
||||
readonly property real heightPx: height
|
||||
readonly property real value: root.value
|
||||
readonly property real actualValue: root.actualValue
|
||||
readonly property real phase: root.phase
|
||||
readonly property real ampPx: root.currentAmp
|
||||
readonly property real wavelengthPx: root.wavelength
|
||||
readonly property real lineWidthPx: root.lineWidth
|
||||
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
|
||||
readonly property color fillColor: root.fillColor
|
||||
readonly property color trackColor: root.trackColor
|
||||
readonly property color playheadColor: root.playheadColor
|
||||
readonly property color actualColor: root.actualProgressColor
|
||||
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
|
||||
}
|
||||
|
||||
Item {
|
||||
id: waveClip
|
||||
anchors.fill: parent
|
||||
clip: true
|
||||
|
||||
readonly property real startX: snap(root.lineWidth / 2)
|
||||
readonly property real aaBias: (0.25 / root.dpr)
|
||||
readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
|
||||
readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
|
||||
readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
|
||||
|
||||
Rectangle {
|
||||
id: mask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: 0
|
||||
width: waveClip.endX
|
||||
color: "transparent"
|
||||
clip: true
|
||||
|
||||
Shape {
|
||||
id: waveShape
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: parent.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
id: wavePath
|
||||
strokeColor: root.fillColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
id: waveSvg
|
||||
path: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMask
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
x: waveClip.gapStartX
|
||||
width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
|
||||
color: "transparent"
|
||||
clip: true
|
||||
visible: (root.previewBehind || root.previewAhead) && width > 0
|
||||
|
||||
Shape {
|
||||
anchors.top: parent.top
|
||||
anchors.bottom: parent.bottom
|
||||
width: root.width + 4 * root.wavelength
|
||||
antialiasing: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
x: waveOffsetX
|
||||
|
||||
ShapePath {
|
||||
strokeColor: root.actualProgressColor
|
||||
strokeWidth: snap(root.lineWidth)
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
fillColor: "transparent"
|
||||
PathSvg {
|
||||
path: waveSvg.path
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: startCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.startX - width / 2
|
||||
y: waveY(waveClip.startX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: endCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.fillColor
|
||||
x: waveClip.endX - width / 2
|
||||
y: waveY(waveClip.endX) - height / 2
|
||||
visible: waveClip.endX > waveClip.startX
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualEndCap
|
||||
width: snap(root.lineWidth)
|
||||
height: snap(root.lineWidth)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: waveClip.gapEndX - width / 2
|
||||
y: waveY(waveClip.gapEndX) - height / 2
|
||||
visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
|
||||
z: 2
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: actualMarker
|
||||
width: 2
|
||||
height: Math.max(root.lineWidth + 4, 10)
|
||||
radius: width / 2
|
||||
color: root.actualProgressColor
|
||||
x: root.actualX - width / 2
|
||||
y: root.midY - height / 2
|
||||
visible: root.showActualPlaybackState
|
||||
z: 2
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: playhead
|
||||
width: 3.5
|
||||
height: Math.max(root.lineWidth + 12, 16)
|
||||
radius: width / 2
|
||||
color: root.playheadColor
|
||||
x: root.playX - width / 2
|
||||
y: root.midY - height / 2
|
||||
z: 3
|
||||
}
|
||||
|
||||
property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
|
||||
function wrapMod(a, m) {
|
||||
let r = a % m;
|
||||
return r < 0 ? r + m : r;
|
||||
}
|
||||
function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
|
||||
return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
|
||||
}
|
||||
|
||||
readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
|
||||
|
||||
FrameAnimation {
|
||||
running: root.visible && (root.isPlaying || root.currentAmp > 0)
|
||||
onTriggered: {
|
||||
if (root.isPlaying)
|
||||
root.phase += 0.03 * frameTime * 60;
|
||||
startCap.y = waveY(waveClip.startX) - startCap.height / 2;
|
||||
endCap.y = waveY(waveClip.endX) - endCap.height / 2;
|
||||
actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
|
||||
}
|
||||
}
|
||||
|
||||
function buildStaticWave() {
|
||||
const start = waveClip.startX - 2 * root.wavelength;
|
||||
const end = width + 2 * root.wavelength;
|
||||
if (end <= start) {
|
||||
waveSvg.path = "";
|
||||
return;
|
||||
}
|
||||
|
||||
const kLocal = k;
|
||||
const halfPeriod = root.wavelength / 2;
|
||||
function y0(x) {
|
||||
return root.midY + root.currentAmp * Math.sin(kLocal * x);
|
||||
}
|
||||
function dy0(x) {
|
||||
return root.currentAmp * Math.cos(kLocal * x) * kLocal;
|
||||
}
|
||||
|
||||
let x0 = start;
|
||||
let d = `M ${x0} ${y0(x0)}`;
|
||||
while (x0 < end) {
|
||||
const x1 = Math.min(x0 + halfPeriod, end);
|
||||
const dx = x1 - x0;
|
||||
const yA = y0(x0), yB = y0(x1);
|
||||
const dyA = dy0(x0), dyB = dy0(x1);
|
||||
const c1x = x0 + dx / 3;
|
||||
const c1y = yA + (dyA * dx) / 3;
|
||||
const c2x = x1 - dx / 3;
|
||||
const c2y = yB - (dyB * dx) / 3;
|
||||
d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
|
||||
x0 = x1;
|
||||
}
|
||||
waveSvg.path = d;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
currentAmp = isPlaying ? amp : 0;
|
||||
buildStaticWave();
|
||||
}
|
||||
onWidthChanged: {
|
||||
flatEnd.x = width;
|
||||
buildStaticWave();
|
||||
}
|
||||
onHeightChanged: buildStaticWave()
|
||||
onCurrentAmpChanged: buildStaticWave()
|
||||
onWavelengthChanged: {
|
||||
k = (2 * Math.PI) / Math.max(1e-6, wavelength);
|
||||
buildStaticWave();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user