Add .config/DankMaterialShell/plugin_settings.json Add .config/DankMaterialShell/plugins/dankDesktopWeather.meta Add .config/DankMaterialShell/plugins/dankHooks.meta Add .config/DankMaterialShell/plugins/desktopCommand/LICENSE Add .config/DankMaterialShell/plugins/desktopCommand/README.md Add .config/DankMaterialShell/plugins/desktopCommand/Settings.qml Add .config/DankMaterialShell/plugins/desktopCommand/Widget.qml Add .config/DankMaterialShell/plugins/desktopCommand/assets/screenshot.jpg Add .config/DankMaterialShell/plugins/desktopCommand/.git/HEAD Add .config/DankMaterialShell/plugins/desktopCommand/.git/config Add .config/DankMaterialShell/plugins/desktopCommand/.git/index Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.idx Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.rev Add .config/DankMaterialShell/plugins/desktopCommand/.git/objects/pack/pack-c2ca48eacecc3ab45931476641d058a89d755775.pack Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/heads/main Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/desktopCommand/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/desktopCommand/.gitignore Add .config/DankMaterialShell/plugins/desktopCommand/wrapCommand Add .config/DankMaterialShell/plugins/desktopCommand/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/DankActionsWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankActions/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlerts.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/DankBatteryAlertsSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankBatteryAlerts/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeather.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/DankDesktopWeatherSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankDesktopWeather/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooks.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/DankHooksSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankHooks/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroSettings.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/DankPomodoroWidget.qml Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/DankPomodoroTimer/plugin.json Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/LICENSE Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/README.md Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/HEAD Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/config Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/index Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.idx Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.rev Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-3221a15c022ef4a7bb6bf2c47e40068b66b3588b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-9aca069a8b76b40fcc472eba1ed9b8219a87776b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/objects/pack/pack-e6f6cdfe9914bfb4a5717ef6036821794d59ab4b.pack Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/heads/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/remotes/origin/master Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/.repos/0026f1eba8dedaec/.gitignore Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncher.qml Add .config/DankMaterialShell/plugins/emojiLauncher/EmojiLauncherSettings.qml Add .config/DankMaterialShell/plugins/emojiLauncher/LICENSE Add .config/DankMaterialShell/plugins/emojiLauncher/README.md Add .config/DankMaterialShell/plugins/emojiLauncher/catalog.js Add .config/DankMaterialShell/plugins/emojiLauncher/data/emojis.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/math.txt Add .config/DankMaterialShell/plugins/emojiLauncher/data/nerdfont.txt Add .config/DankMaterialShell/plugins/emojiLauncher/.git/HEAD Add .config/DankMaterialShell/plugins/emojiLauncher/.git/config Add .config/DankMaterialShell/plugins/emojiLauncher/.git/index Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.idx Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.rev Add .config/DankMaterialShell/plugins/emojiLauncher/.git/objects/pack/pack-e04a5b1ea381dc3a792b8bf08cf70e735b195c0d.pack Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/heads/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/emojiLauncher/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/emojiLauncher/plugin.json Add .config/DankMaterialShell/plugins/emojiLauncher/screenshot.png Add .config/DankMaterialShell/plugins/emojiLauncher/scripts/generate_catalog.py Add .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerSettings.qml Add .config/DankMaterialShell/plugins/mediaPlayer/MediaPlayerTab.qml Add .config/DankMaterialShell/plugins/mediaPlayer/README.md Add .config/DankMaterialShell/plugins/mediaPlayer/.git/HEAD Add .config/DankMaterialShell/plugins/mediaPlayer/.git/config Add .config/DankMaterialShell/plugins/mediaPlayer/.git/index Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/info/.keep Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.idx Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.rev Add .config/DankMaterialShell/plugins/mediaPlayer/.git/objects/pack/pack-0b9cb33f7da23f6ff361ee3aa5117928714bc3be.pack Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/heads/main Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/remotes/origin/main Add .config/DankMaterialShell/plugins/mediaPlayer/.git/refs/tags/.keep Add .config/DankMaterialShell/plugins/mediaPlayer/plugin.json Add .config/DankMaterialShell/plugins/mediaPlayer/screenshot_8.png Add .config/DankMaterialShell/plugins/dankDesktopWeather Add .config/DankMaterialShell/plugins/dankHooks Add .config/DankMaterialShell/settings.json
693 lines
25 KiB
QML
693 lines
25 KiB
QML
import QtQuick
|
|
import QtQuick.Effects
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Services.Mpris
|
|
import Quickshell.Io
|
|
import qs.Common
|
|
import qs.Services
|
|
import qs.Widgets
|
|
import qs.Modules.Plugins
|
|
|
|
DesktopPluginComponent {
|
|
id: root
|
|
|
|
// settings data here
|
|
property real backgroundOpacity: (pluginData.backgroundOpacity ?? 80) / 100
|
|
property real borderOpacity: (pluginData.borderOpacity ?? 100) / 100
|
|
|
|
LayoutMirroring.enabled: I18n.isRtl
|
|
LayoutMirroring.childrenInherit: true
|
|
|
|
opacity: showNoPlayerNow ? 0 : 1
|
|
Behavior on opacity { NumberAnimation { duration: 300 } }
|
|
|
|
property MprisPlayer activePlayer: MprisController.activePlayer
|
|
property var allPlayers: MprisController.availablePlayers
|
|
|
|
property bool isSwitching: false
|
|
property string _lastArtUrl: ""
|
|
property string _bgArtSource: ""
|
|
|
|
property string activeTrackArtFile: ""
|
|
|
|
function loadArtwork(url) {
|
|
if (!url)
|
|
return;
|
|
if (url.startsWith("http://") || url.startsWith("https://")) {
|
|
const filename = "/tmp/.dankshell/trackart_" + Date.now() + ".jpg";
|
|
activeTrackArtFile = filename;
|
|
|
|
cleanupProcess.command = ["sh", "-c", "mkdir -p /tmp/.dankshell && find /tmp/.dankshell -name 'trackart_*' ! -name '" + filename.split('/').pop() + "' -delete"];
|
|
cleanupProcess.running = true;
|
|
|
|
imageDownloader.command = ["curl", "-L", "-s", "--user-agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", "-o", filename, url];
|
|
imageDownloader.targetFile = filename;
|
|
imageDownloader.running = true;
|
|
return;
|
|
}
|
|
_bgArtSource = url;
|
|
}
|
|
|
|
function maybeFinishSwitch() {
|
|
if (activePlayer && activePlayer.trackTitle !== "") {
|
|
isSwitching = false;
|
|
_switchHold = false;
|
|
_stalePositionDetected = false;
|
|
}
|
|
}
|
|
|
|
function getDisplayPosition() {
|
|
if (!activePlayer) return 0;
|
|
|
|
const rawPos = Math.max(0, activePlayer.position || 0);
|
|
const length = Math.max(1, activePlayer.length || 1);
|
|
|
|
// If we detected stale position, show 0 until proper data arrives
|
|
if (_stalePositionDetected) {
|
|
return 0;
|
|
}
|
|
|
|
// Handle stale position data when switching videos
|
|
if (isSwitching && rawPos >= length * 0.9) {
|
|
return 0;
|
|
}
|
|
|
|
const pos = activePlayer.length ? rawPos % Math.max(1, activePlayer.length) : rawPos;
|
|
return pos;
|
|
}
|
|
|
|
function formatTime(seconds) {
|
|
const minutes = Math.floor(seconds / 60);
|
|
const secs = Math.floor(seconds % 60);
|
|
return minutes + ":" + (secs < 10 ? "0" : "") + secs;
|
|
}
|
|
|
|
Component.onCompleted: {
|
|
// Initialize with current player state if available
|
|
if (activePlayer) {
|
|
// Get actual position after MPRIS fully loads
|
|
Qt.callLater(() => {
|
|
try {
|
|
const actualPos = activePlayer.position || 0;
|
|
const length = activePlayer.length || 1;
|
|
root._positionSnapshot = actualPos;
|
|
if (progressSeekbar && actualPos > 0) {
|
|
progressSeekbar.value = Math.min(1, actualPos / length);
|
|
}
|
|
} catch (e) {
|
|
// Handle MPRIS errors
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
// Derived "no players" state: always correct, no timers.
|
|
readonly property int _playerCount: allPlayers ? allPlayers.length : 0
|
|
readonly property bool _noneAvailable: _playerCount === 0
|
|
readonly property bool _trulyIdle: activePlayer && activePlayer.playbackState === MprisPlaybackState.Stopped && !activePlayer.trackTitle && !activePlayer.trackArtist
|
|
readonly property bool showNoPlayerNow: (!_switchHold) && (_noneAvailable || _trulyIdle)
|
|
|
|
property bool _switchHold: false
|
|
Timer {
|
|
id: _switchHoldTimer
|
|
interval: 650
|
|
repeat: false
|
|
onTriggered: {
|
|
_switchHold = false;
|
|
if (isSwitching) {
|
|
isSwitching = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
onActivePlayerChanged: {
|
|
root._positionSnapshot = 0;
|
|
root._forceUpdate = !root._forceUpdate;
|
|
if (!activePlayer) {
|
|
isSwitching = false;
|
|
_switchHold = false;
|
|
return;
|
|
}
|
|
isSwitching = true;
|
|
_switchHold = true;
|
|
_switchHoldTimer.restart();
|
|
if (activePlayer.trackArtUrl)
|
|
loadArtwork(activePlayer.trackArtUrl);
|
|
|
|
// Get actual current position after a short delay to allow MPRIS to sync
|
|
Qt.callLater(() => {
|
|
try {
|
|
const actualPos = activePlayer.position || 0;
|
|
root._positionSnapshot = actualPos;
|
|
if (progressSeekbar && actualPos > 0) {
|
|
progressSeekbar.value = Math.min(1, actualPos / Math.max(1, activePlayer.length || 1));
|
|
isSwitching = false;
|
|
}
|
|
} catch (e) {
|
|
// Handle errors gracefully
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
|
|
// Responsive sizing with min/max constraints
|
|
property real userScale: 1.0
|
|
readonly property real minWidth: 320
|
|
readonly property real maxWidth: 800
|
|
readonly property real minHeight: 160
|
|
readonly property real maxHeight: 400
|
|
readonly property real baseWidth: 380
|
|
readonly property real baseHeight: 200
|
|
|
|
implicitWidth: Math.max(minWidth, Math.min(maxWidth, baseWidth * userScale))
|
|
implicitHeight: Math.max(minHeight, Math.min(maxHeight, baseHeight * userScale))
|
|
|
|
Connections {
|
|
target: activePlayer
|
|
function onTrackTitleChanged() {
|
|
root._positionSnapshot = 0;
|
|
root._forceUpdate = !root._forceUpdate;
|
|
// Force immediate position reset for new track
|
|
if (activePlayer.position > 0 && activePlayer.length > 0) {
|
|
const progressRatio = activePlayer.position / activePlayer.length;
|
|
if (progressRatio > 0.9) {
|
|
// Likely stale data - force reset
|
|
root._stalePositionDetected = true;
|
|
}
|
|
}
|
|
_switchHoldTimer.restart();
|
|
maybeFinishSwitch();
|
|
// Reset progress bar immediately on track change
|
|
if (progressSeekbar) {
|
|
progressSeekbar.value = 0;
|
|
}
|
|
}
|
|
function onTrackArtUrlChanged() {
|
|
if (activePlayer?.trackArtUrl) {
|
|
_lastArtUrl = activePlayer.trackArtUrl;
|
|
loadArtwork(activePlayer.trackArtUrl);
|
|
}
|
|
}
|
|
function onPositionChanged() {
|
|
try {
|
|
if (root._stalePositionDetected && activePlayer.position < activePlayer.length * 0.5) {
|
|
// Position updated properly now
|
|
root._stalePositionDetected = false;
|
|
root._forceUpdate = !root._forceUpdate;
|
|
}
|
|
} catch (e) {
|
|
// MPRIS service disappeared - reset state
|
|
root._stalePositionDetected = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
Connections {
|
|
target: MprisController
|
|
function onAvailablePlayersChanged() {
|
|
const count = (MprisController.availablePlayers?.length || 0);
|
|
if (count === 0) {
|
|
isSwitching = false;
|
|
_switchHold = false;
|
|
} else {
|
|
_switchHold = true;
|
|
_switchHoldTimer.restart();
|
|
}
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: imageDownloader
|
|
running: false
|
|
property string targetFile: ""
|
|
|
|
onExited: exitCode => {
|
|
if (exitCode === 0 && targetFile)
|
|
_bgArtSource = "file://" + targetFile;
|
|
}
|
|
}
|
|
|
|
Process {
|
|
id: cleanupProcess
|
|
running: false
|
|
}
|
|
|
|
|
|
|
|
property bool isSeeking: false
|
|
property real _positionSnapshot: 0
|
|
property bool _forceUpdate: false
|
|
property real _animationTick: 0
|
|
property bool _stalePositionDetected: false
|
|
|
|
Timer {
|
|
id: positionUpdateTimer
|
|
interval: 100
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
// Update snapshot to trigger binding re-evaluation
|
|
if (activePlayer) {
|
|
try {
|
|
const newPosition = activePlayer.position || 0;
|
|
root._positionSnapshot = newPosition;
|
|
// Force progress bar refresh when switching
|
|
if (isSwitching || _stalePositionDetected) {
|
|
if (progressSeekbar) {
|
|
progressSeekbar.value = progressSeekbar.calculateProgress();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
// Handle MPRIS service errors gracefully
|
|
root._positionSnapshot = 0;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Use animation to drive constant updates for smooth progress bar
|
|
NumberAnimation {
|
|
id: progressUpdateAnimation
|
|
target: root
|
|
property: "_animationTick"
|
|
from: 0
|
|
to: 10000
|
|
duration: 10000
|
|
loops: Animation.Infinite
|
|
running: activePlayer?.playbackState === MprisPlaybackState.Playing && !isSeeking
|
|
}
|
|
|
|
Item {
|
|
id: bgContainer
|
|
anchors.fill: parent
|
|
visible: _bgArtSource !== ""
|
|
|
|
Image {
|
|
id: bgImage
|
|
anchors.centerIn: parent
|
|
width: Math.max(parent.width, parent.height) * 1.1
|
|
height: width
|
|
// source: _bgArtSource
|
|
fillMode: Image.PreserveAspectCrop
|
|
asynchronous: true
|
|
cache: true
|
|
visible: false
|
|
onStatusChanged: {
|
|
if (status === Image.Ready)
|
|
maybeFinishSwitch();
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: blurredBg
|
|
anchors.fill: parent
|
|
visible: false
|
|
|
|
MultiEffect {
|
|
anchors.centerIn: parent
|
|
width: bgImage.width
|
|
height: bgImage.height
|
|
source: bgImage
|
|
blurEnabled: true
|
|
blurMax: 64
|
|
blur: 2
|
|
saturation: -0.2
|
|
brightness: -0.25
|
|
}
|
|
}
|
|
|
|
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
|
|
color: Theme.withAlpha(Theme.surface, root.backgroundOpacity)
|
|
}
|
|
}
|
|
|
|
// --- Ocean Wave Background ---
|
|
Canvas {
|
|
id: waveCanvas
|
|
anchors.fill: parent
|
|
z: 1 // Ensures it stays behind the main content
|
|
opacity: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? 0.4 : 0.1
|
|
|
|
property real phase: 0
|
|
|
|
// This timer drives the animation "movement"
|
|
Timer {
|
|
interval: 16 // ~60 FPS
|
|
running: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
|
|
repeat: true
|
|
onTriggered: {
|
|
waveCanvas.phase += 0.05;
|
|
waveCanvas.requestPaint();
|
|
}
|
|
}
|
|
|
|
onPaint: {
|
|
var ctx = getContext("2d");
|
|
ctx.clearRect(0, 0, width, height);
|
|
|
|
// Draw two overlapping waves for a "deep" ocean feel
|
|
drawWave(ctx, "#40" + Theme.primary.toString().substring(1), 0.5, 15, phase);
|
|
drawWave(ctx, "#60" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.7);
|
|
drawWave(ctx, "#70" + Theme.primary.toString().substring(1), 0.8, 10, phase * 0.9);
|
|
}
|
|
|
|
function drawWave(ctx, color, speed, amplitude, currentPhase) {
|
|
ctx.beginPath();
|
|
ctx.fillStyle = color;
|
|
|
|
var waveHeight = height * 0.7; // Position waves at the bottom 30%
|
|
ctx.moveTo(0, height);
|
|
|
|
for (var x = 0; x <= width; x += 5) {
|
|
// Sine wave calculation: y = amplitude * sin(frequency * x + phase)
|
|
var y = waveHeight + Math.sin(x * 0.02 + currentPhase) * amplitude;
|
|
ctx.lineTo(x, y);
|
|
}
|
|
|
|
ctx.lineTo(width, height);
|
|
ctx.closePath();
|
|
ctx.fill();
|
|
}
|
|
}
|
|
// --- End Ocean Wave Background ---
|
|
|
|
Column {
|
|
anchors.centerIn: parent
|
|
spacing: Theme.spacingM
|
|
visible: showNoPlayerNow
|
|
|
|
DankIcon {
|
|
name: "music_note"
|
|
size: Theme.iconSize * 3
|
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.5)
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
}
|
|
}
|
|
|
|
// Main content container - Layout with thumbnail
|
|
Item {
|
|
anchors.fill: parent
|
|
anchors.margins: Theme.spacingM * userScale
|
|
visible: !_noneAvailable && (!showNoPlayerNow)
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
spacing: Theme.spacingM * userScale
|
|
|
|
// Album Thumbnail Section (Left)
|
|
Rectangle {
|
|
id: thumbnailContainer
|
|
width: parent.height * 0.85
|
|
height: parent.height * 0.85
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
radius: 6 * userScale
|
|
color: "transparent"
|
|
clip: true
|
|
|
|
property real albumRotation: 0
|
|
|
|
NumberAnimation {
|
|
id: rotationAnimation
|
|
target: thumbnailContainer
|
|
property: "albumRotation"
|
|
from: 0
|
|
to: 360
|
|
duration: 20000
|
|
running: activePlayer?.playbackState === MprisPlaybackState.Playing
|
|
loops: Animation.Infinite
|
|
}
|
|
|
|
DankAlbumArt {
|
|
id: albumArt
|
|
width: parent.width * 0.76
|
|
height: parent.height * 0.76
|
|
anchors.centerIn: parent
|
|
activePlayer: root.activePlayer
|
|
rotation: thumbnailContainer.albumRotation
|
|
}
|
|
|
|
// Subtle border
|
|
Rectangle {
|
|
anchors.fill: parent
|
|
radius: parent.radius
|
|
color: "transparent"
|
|
border.color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, root.borderOpacity)
|
|
border.width: 1
|
|
}
|
|
}
|
|
|
|
// Content Section (Right)
|
|
Column {
|
|
width: parent.width - thumbnailContainer.width - parent.spacing
|
|
height: parent.height
|
|
spacing: Theme.spacingS * userScale
|
|
|
|
// Song Info Section (Top)
|
|
Column {
|
|
id: songInfo
|
|
width: parent.width
|
|
spacing: 2 * userScale
|
|
|
|
StyledText {
|
|
text: activePlayer?.trackTitle || "The (Overdue) Collapse of Wind..."
|
|
font.pixelSize: Theme.fontSizeMedium * 1.1 * userScale
|
|
font.weight: Font.Bold
|
|
color: Theme.surfaceText
|
|
width: parent.width
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1
|
|
}
|
|
|
|
StyledText {
|
|
text: activePlayer?.trackArtist || "Catalyst"
|
|
font.pixelSize: Theme.fontSizeSmall * userScale
|
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
|
width: parent.width
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1
|
|
}
|
|
}
|
|
|
|
// Spacer
|
|
Item {
|
|
width: parent.width
|
|
height: Theme.spacingXS * userScale
|
|
}
|
|
|
|
// Controls Row (Middle)
|
|
Row {
|
|
id: controlsRow
|
|
width: parent.width
|
|
spacing: Theme.spacingS * userScale
|
|
|
|
// Previous Button
|
|
Rectangle {
|
|
width: 32 * userScale
|
|
height: 32 * userScale
|
|
radius: 4 * userScale
|
|
color: "transparent"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "skip_previous"
|
|
size: 28 * userScale
|
|
color: Theme.primary
|
|
}
|
|
|
|
MouseArea {
|
|
id: prevBtnArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: {
|
|
if (!activePlayer)
|
|
return;
|
|
if (activePlayer.position > 8 && activePlayer.canSeek) {
|
|
activePlayer.position = 0;
|
|
} else {
|
|
activePlayer.previous();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Play/Pause Button
|
|
Rectangle {
|
|
width: 32 * userScale
|
|
height: 32 * userScale
|
|
radius: 4 * userScale
|
|
color: "transparent"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing ? "pause" : "play_arrow"
|
|
size: 28 * userScale
|
|
color: Theme.primary
|
|
}
|
|
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: activePlayer && activePlayer.togglePlaying()
|
|
}
|
|
}
|
|
|
|
// Next Button
|
|
Rectangle {
|
|
width: 32 * userScale
|
|
height: 32 * userScale
|
|
radius: 4 * userScale
|
|
color: "transparent"
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
|
|
DankIcon {
|
|
anchors.centerIn: parent
|
|
name: "skip_next"
|
|
size: 28 * userScale
|
|
color: Theme.primary
|
|
}
|
|
|
|
MouseArea {
|
|
id: nextBtnArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
onClicked: activePlayer && activePlayer.next()
|
|
}
|
|
}
|
|
|
|
// Spacer to push buttons to right
|
|
Item {
|
|
width: parent.width - (32 * 3 * userScale) - (Theme.spacingS * 3 * userScale)
|
|
height: 1
|
|
}
|
|
}
|
|
|
|
// Seekbar Section (Bottom)
|
|
Column {
|
|
id: seekbarSection
|
|
width: parent.width
|
|
spacing: 2 * userScale
|
|
|
|
// Integrated DankSeekbar
|
|
DankSeekbar {
|
|
id: progressSeekbar
|
|
width: parent.width
|
|
height: 16 * userScale // Consistent with previous design
|
|
activePlayer: root.activePlayer
|
|
|
|
// Keep the Tab's seeking state in sync with the component
|
|
onIsSeekingChanged: root.isSeeking = isSeeking
|
|
|
|
function calculateProgress() {
|
|
if (!root.activePlayer || root.activePlayer.length <= 0) return 0;
|
|
|
|
const rawPos = Math.max(0, root.activePlayer.position || 0);
|
|
const length = Math.max(1, root.activePlayer.length || 1);
|
|
|
|
// If we detected stale position, show 0 until proper data arrives
|
|
if (root._stalePositionDetected) {
|
|
// Check if position is now valid
|
|
if (rawPos < length * 0.8) {
|
|
root._stalePositionDetected = false;
|
|
root.isSwitching = false;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
// Reset if position seems stale (at end for new video)
|
|
if (root.isSwitching && rawPos >= length * 0.9) {
|
|
root._stalePositionDetected = true;
|
|
return 0;
|
|
}
|
|
|
|
// Force position refresh when switching videos
|
|
if (root.isSwitching && rawPos > 0 && rawPos < length * 0.8) {
|
|
root.isSwitching = false;
|
|
}
|
|
|
|
return Math.min(1, rawPos / length);
|
|
}
|
|
|
|
// Connect position updates to seekbar value directly
|
|
Timer {
|
|
interval: 50
|
|
running: true
|
|
repeat: true
|
|
onTriggered: {
|
|
if (progressSeekbar && activePlayer) {
|
|
try {
|
|
progressSeekbar.value = progressSeekbar.calculateProgress();
|
|
} catch (e) {
|
|
// Handle MPRIS errors
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Time labels
|
|
Item {
|
|
width: parent.width
|
|
height: 12 * userScale
|
|
|
|
StyledText {
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: {
|
|
// Force dependency on position updates
|
|
root._positionSnapshot;
|
|
return formatTime(getDisplayPosition());
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
|
|
StyledText {
|
|
anchors.right: parent.right
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: {
|
|
if (!activePlayer || !activePlayer.length)
|
|
return "0:00";
|
|
const dur = Math.max(0, activePlayer.length || 0);
|
|
const minutes = Math.floor(dur / 60);
|
|
const seconds = Math.floor(dur % 60);
|
|
return minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
|
|
}
|
|
font.pixelSize: Theme.fontSizeSmall * 0.9 * userScale
|
|
color: Theme.surfaceVariantText
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
}
|