mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-03 03:59:11 -04:00
Compare commits
9 Commits
v1.5.3
...
stable-1.5
| Author | SHA1 | Date | |
|---|---|---|---|
| 943ffb432c | |||
| 7063448b80 | |||
| 52afbc9801 | |||
| 6edb985847 | |||
| 41efe6ad15 | |||
| 4fb6a17e1b | |||
| e771e3b675 | |||
| b1b7aa7aa2 | |||
| c128793239 |
@@ -469,6 +469,8 @@ PluginComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: peerMouseArea
|
||||||
|
|
||||||
z: -1
|
z: -1
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackAlbum || ""
|
text: MprisController.stableAlbum
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextSecondary
|
color: Theme.surfaceTextSecondary
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -479,8 +479,6 @@ Item {
|
|||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: delegateItem
|
id: delegateItem
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
property var dockButton: {
|
property var dockButton: {
|
||||||
switch (itemData.type) {
|
switch (itemData.type) {
|
||||||
@@ -601,7 +599,7 @@ Item {
|
|||||||
height: delegateItem.height
|
height: delegateItem.height
|
||||||
actualIconSize: root.iconSize
|
actualIconSize: root.iconSize
|
||||||
dockApps: root
|
dockApps: root
|
||||||
index: delegateItem.index
|
index: model.index
|
||||||
}
|
}
|
||||||
|
|
||||||
DockTrashButton {
|
DockTrashButton {
|
||||||
@@ -626,7 +624,7 @@ Item {
|
|||||||
appData: itemData
|
appData: itemData
|
||||||
contextMenu: root.contextMenu
|
contextMenu: root.contextMenu
|
||||||
dockApps: root
|
dockApps: root
|
||||||
index: delegateItem.index
|
index: model.index
|
||||||
parentDockScreen: root.dockScreen
|
parentDockScreen: root.dockScreen
|
||||||
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
||||||
windowTitle: {
|
windowTitle: {
|
||||||
|
|||||||
@@ -386,16 +386,20 @@ end)
|
|||||||
HYPRLAND_LUA_EOF
|
HYPRLAND_LUA_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
||||||
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
||||||
env = DMS_RUN_GREETER,1
|
hl.env("DMS_RUN_GREETER", "1")
|
||||||
|
|
||||||
misc {
|
hl.config({
|
||||||
disable_hyprland_logo = true
|
misc = {
|
||||||
}
|
disable_hyprland_logo = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
hl.on("hyprland.start", function()
|
||||||
HYPRLAND_EOF
|
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
|
||||||
|
end)
|
||||||
|
HYPRLAND_LUA_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
else
|
else
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp)
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ DankOSD {
|
|||||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const newTitle = player.trackTitle || "";
|
const metaPlayer = MprisController.bestMetadataPlayer(player);
|
||||||
const newArtist = player.trackArtist || "";
|
const newTitle = MprisController.displayTrackTitle(metaPlayer);
|
||||||
const newAlbum = player.trackAlbum || "";
|
const newArtist = metaPlayer.trackArtist || "";
|
||||||
|
const newAlbum = metaPlayer.trackAlbum || "";
|
||||||
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
||||||
|
|
||||||
root._displayTitle = newTitle;
|
root._displayTitle = newTitle;
|
||||||
@@ -263,37 +264,102 @@ DankOSD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Row {
|
||||||
width: Theme.iconSize
|
id: transportControls
|
||||||
height: Theme.iconSize
|
|
||||||
radius: Theme.iconSize / 2
|
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
x: parent.gap
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
DankIcon {
|
Rectangle {
|
||||||
anchors.centerIn: parent
|
width: Theme.iconSize - 4
|
||||||
name: root._displayIcon
|
height: Theme.iconSize - 4
|
||||||
size: Theme.iconSize
|
radius: (Theme.iconSize - 4) / 2
|
||||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
|
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_previous"
|
||||||
|
size: Theme.iconSize - 10
|
||||||
|
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: prevButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: root.player?.canGoPrevious ?? false
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
MprisController.previousOrRewind();
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
Rectangle {
|
||||||
id: playPauseButton
|
width: Theme.iconSize
|
||||||
|
height: Theme.iconSize
|
||||||
|
radius: Theme.iconSize / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
anchors.fill: parent
|
DankIcon {
|
||||||
hoverEnabled: true
|
anchors.centerIn: parent
|
||||||
cursorShape: Qt.PointingHandCursor
|
name: root._displayIcon
|
||||||
onClicked: {
|
size: Theme.iconSize
|
||||||
togglePlaying();
|
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
root.hide();
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: playPauseButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
togglePlaying();
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize - 4
|
||||||
|
height: Theme.iconSize - 4
|
||||||
|
radius: (Theme.iconSize - 4) / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
|
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_next"
|
||||||
|
size: Theme.iconSize - 10
|
||||||
|
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: root.player?.canGoNext ?? false
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
MprisController.next();
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
x: parent.gap * 2 + transportControls.width
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
width: parent.width - transportControls.width - parent.gap * 3
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingXXS
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
|
||||||
|
function onHasPerformanceProfileChanged() {
|
||||||
|
root.applyPowerProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyPowerProfile() {
|
function applyPowerProfile() {
|
||||||
if (!batteryAvailable)
|
if (!batteryAvailable)
|
||||||
return;
|
return;
|
||||||
@@ -32,7 +39,7 @@ Singleton {
|
|||||||
const targetProfile = parseInt(profileValue);
|
const targetProfile = parseInt(profileValue);
|
||||||
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
||||||
return;
|
return;
|
||||||
PowerProfiles.profile = targetProfile;
|
PowerProfileWatcher.applyProfile(targetProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("DMSService")
|
readonly property var log: Log.scoped("DMSService")
|
||||||
|
|
||||||
property bool dmsAvailable: false
|
readonly property bool dmsAvailable: isConnected
|
||||||
property var capabilities: []
|
property var capabilities: []
|
||||||
property int apiVersion: 0
|
property int apiVersion: 0
|
||||||
property string cliVersion: ""
|
property string cliVersion: ""
|
||||||
@@ -21,7 +21,7 @@ Singleton {
|
|||||||
property var availableThemes: []
|
property var availableThemes: []
|
||||||
property var installedThemes: []
|
property var installedThemes: []
|
||||||
property bool isConnected: false
|
property bool isConnected: false
|
||||||
property bool isConnecting: false
|
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
|
||||||
property bool subscribeConnected: false
|
property bool subscribeConnected: false
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||||
@@ -72,9 +72,10 @@ Singleton {
|
|||||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
|
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (socketPath && socketPath.length > 0) {
|
if (!socketPath || socketPath.length === 0)
|
||||||
detectUpdateCommand();
|
return;
|
||||||
}
|
detectUpdateCommand();
|
||||||
|
requestSocket.connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectUpdateCommand() {
|
function detectUpdateCommand() {
|
||||||
@@ -82,12 +83,6 @@ Singleton {
|
|||||||
checkAurHelper.running = true;
|
checkAurHelper.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSocketConnection() {
|
|
||||||
if (socketPath && socketPath.length > 0) {
|
|
||||||
testProcess.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: checkAurHelper
|
id: checkAurHelper
|
||||||
command: ["sh", "-c", "command -v paru || command -v yay"]
|
command: ["sh", "-c", "command -v paru || command -v yay"]
|
||||||
@@ -105,7 +100,6 @@ Singleton {
|
|||||||
} else {
|
} else {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +108,6 @@ Singleton {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +128,6 @@ Singleton {
|
|||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
}
|
}
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,52 +135,27 @@ Singleton {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: testProcess
|
|
||||||
command: ["test", "-S", root.socketPath]
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.dmsAvailable = true;
|
|
||||||
connectSocket();
|
|
||||||
} else {
|
|
||||||
root.dmsAvailable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectSocket() {
|
|
||||||
if (!dmsAvailable || isConnected || isConnecting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnecting = true;
|
|
||||||
requestSocket.connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSocket {
|
DankSocket {
|
||||||
id: requestSocket
|
id: requestSocket
|
||||||
path: root.socketPath
|
path: root.socketPath
|
||||||
connected: false
|
connected: false
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
onConnectionStateChanged: {
|
||||||
if (connected) {
|
if (linkUp) {
|
||||||
root.isConnected = true;
|
root.isConnected = true;
|
||||||
root.isConnecting = false;
|
|
||||||
root.connectionStateChanged();
|
root.connectionStateChanged();
|
||||||
subscribeSocket.connected = true;
|
subscribeSocket.connected = true;
|
||||||
} else {
|
return;
|
||||||
root.isConnected = false;
|
|
||||||
root.isConnecting = false;
|
|
||||||
root.apiVersion = 0;
|
|
||||||
root.capabilities = [];
|
|
||||||
root.connectionStateChanged();
|
|
||||||
}
|
}
|
||||||
|
root.isConnected = false;
|
||||||
|
root.apiVersion = 0;
|
||||||
|
root.capabilities = [];
|
||||||
|
root.failPendingRequests();
|
||||||
|
root.connectionStateChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
@@ -219,10 +186,10 @@ Singleton {
|
|||||||
connected: false
|
connected: false
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
onConnectionStateChanged: {
|
||||||
root.subscribeConnected = connected;
|
root.subscribeConnected = linkUp;
|
||||||
if (connected) {
|
if (!linkUp)
|
||||||
sendSubscribeRequest();
|
return;
|
||||||
}
|
sendSubscribeRequest();
|
||||||
}
|
}
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
@@ -446,10 +413,20 @@ Singleton {
|
|||||||
|
|
||||||
function handleResponse(response) {
|
function handleResponse(response) {
|
||||||
const callback = pendingRequests[response.id];
|
const callback = pendingRequests[response.id];
|
||||||
|
if (!callback)
|
||||||
|
return;
|
||||||
|
delete pendingRequests[response.id];
|
||||||
|
callback(response);
|
||||||
|
}
|
||||||
|
|
||||||
if (callback) {
|
function failPendingRequests() {
|
||||||
delete pendingRequests[response.id];
|
const pending = pendingRequests;
|
||||||
callback(response);
|
pendingRequests = {};
|
||||||
|
clipboardRequestIds = {};
|
||||||
|
for (const id in pending) {
|
||||||
|
pending[id]({
|
||||||
|
"error": "not connected to DMS socket"
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Singleton {
|
|||||||
// Chromium can report blank metadata between tracks
|
// Chromium can report blank metadata between tracks
|
||||||
property string stableTitle: ""
|
property string stableTitle: ""
|
||||||
property string stableArtist: ""
|
property string stableArtist: ""
|
||||||
|
property string stableAlbum: ""
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: root.activePlayer
|
||||||
@@ -59,6 +60,9 @@ Singleton {
|
|||||||
root._syncStableMeta();
|
root._syncStableMeta();
|
||||||
root._checkIdle();
|
root._checkIdle();
|
||||||
}
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
function onLengthChanged() {
|
function onLengthChanged() {
|
||||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||||
root.activePlayerStableLength = root.activePlayer.length;
|
root.activePlayerStableLength = root.activePlayer.length;
|
||||||
@@ -72,8 +76,10 @@ Singleton {
|
|||||||
|
|
||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||||
stableTitle = activePlayer?.trackTitle || "";
|
stableTitle = "";
|
||||||
stableArtist = activePlayer?.trackArtist || "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
_syncStableMeta();
|
||||||
_checkIdle();
|
_checkIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +88,24 @@ Singleton {
|
|||||||
if (!p) {
|
if (!p) {
|
||||||
stableTitle = "";
|
stableTitle = "";
|
||||||
stableArtist = "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFirefoxYoutubeHoverPreview(p))
|
if (isFirefoxYoutubeHoverPreview(p))
|
||||||
return;
|
return;
|
||||||
if (p.trackTitle)
|
const metadataPlayer = bestMetadataPlayer(p);
|
||||||
stableTitle = p.trackTitle;
|
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||||
if (p.trackArtist)
|
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||||
stableArtist = p.trackArtist;
|
if (trackChanged) {
|
||||||
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
}
|
||||||
|
if (nextTitle)
|
||||||
|
stableTitle = nextTitle;
|
||||||
|
if (metadataPlayer.trackArtist)
|
||||||
|
stableArtist = metadataPlayer.trackArtist;
|
||||||
|
if (metadataPlayer.trackAlbum)
|
||||||
|
stableAlbum = metadataPlayer.trackAlbum;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
||||||
@@ -101,6 +117,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
root.stableTitle = "";
|
root.stableTitle = "";
|
||||||
root.stableArtist = "";
|
root.stableArtist = "";
|
||||||
|
root.stableAlbum = "";
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,9 +139,30 @@ Singleton {
|
|||||||
delegate: Connections {
|
delegate: Connections {
|
||||||
required property MprisPlayer modelData
|
required property MprisPlayer modelData
|
||||||
target: modelData
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
function onIsPlayingChanged() {
|
function onIsPlayingChanged() {
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackTitleChanged() {
|
||||||
if (modelData.isPlaying)
|
if (modelData.isPlaying)
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackArtistChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onMetadataChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +171,101 @@ Singleton {
|
|||||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
|
||||||
|
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
|
||||||
|
|
||||||
|
function _stripAppTitleSuffix(title: string): string {
|
||||||
|
const idx = title.lastIndexOf(" | ");
|
||||||
|
if (idx <= 0)
|
||||||
|
return title;
|
||||||
|
const suffix = title.substring(idx + 3).trim().toLowerCase();
|
||||||
|
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedTrackTitle(player: MprisPlayer): string {
|
||||||
|
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayTrackTitle(player: MprisPlayer): string {
|
||||||
|
return (player?.trackTitle || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedTrackArtist(player: MprisPlayer): string {
|
||||||
|
return (player?.trackArtist || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
|
||||||
|
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
|
||||||
|
const firstTitle = normalizedTrackTitle(first);
|
||||||
|
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
|
||||||
|
return false;
|
||||||
|
const firstArtist = normalizedTrackArtist(first);
|
||||||
|
const secondArtist = normalizedTrackArtist(second);
|
||||||
|
if (firstArtist && secondArtist)
|
||||||
|
return firstArtist === secondArtist;
|
||||||
|
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
|
||||||
|
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
|
||||||
|
if (firstUrl && secondUrl)
|
||||||
|
return firstUrl === secondUrl;
|
||||||
|
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
|
||||||
|
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
|
||||||
|
if (firstAlbum && secondAlbum)
|
||||||
|
return firstAlbum === secondAlbum;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataQuality(player: MprisPlayer): int {
|
||||||
|
if (!player)
|
||||||
|
return -1;
|
||||||
|
let quality = player.trackArtist ? 100 : 0;
|
||||||
|
quality += player.trackTitle ? 40 : 0;
|
||||||
|
quality += player.trackAlbum ? 20 : 0;
|
||||||
|
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
|
||||||
|
quality += player.metadata?.["xesam:url"] ? 5 : 0;
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
function equivalentPlayers(player: MprisPlayer): var {
|
||||||
|
if (!player)
|
||||||
|
return [];
|
||||||
|
return availablePlayers.filter(candidate => {
|
||||||
|
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
|
||||||
|
const equivalents = equivalentPlayers(player);
|
||||||
|
if (equivalents.length === 0)
|
||||||
|
return player;
|
||||||
|
return equivalents.reduce((best, candidate) => {
|
||||||
|
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
|
||||||
|
}, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bestPlayingPlayer(): MprisPlayer {
|
||||||
|
const playing = availablePlayers.filter(player => player.isPlaying);
|
||||||
|
if (playing.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const controllable = playing.filter(player => player.canControl);
|
||||||
|
if (activePlayer?.isPlaying) {
|
||||||
|
if (activePlayer.canControl || controllable.length === 0)
|
||||||
|
return activePlayer;
|
||||||
|
// Playing but not controllable: only a same-track controllable peer may take over
|
||||||
|
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
|
||||||
|
return mirror || activePlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
|
||||||
|
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
|
||||||
|
if (onlyEquivalentMirrors)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return controllable[0] || playing[0];
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveActivePlayer(): void {
|
function _resolveActivePlayer(): void {
|
||||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
const playing = _bestPlayingPlayer();
|
||||||
const playing = availablePlayers.find(p => p.isPlaying);
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
if (activePlayer !== playing) {
|
if (activePlayer !== playing) {
|
||||||
activePlayer = playing;
|
activePlayer = playing;
|
||||||
|
|||||||
@@ -217,6 +217,64 @@ Singleton {
|
|||||||
return envObj;
|
return envObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitShellArgs(str) {
|
||||||
|
const args = [];
|
||||||
|
let current = "";
|
||||||
|
let hasToken = false;
|
||||||
|
let quote = "";
|
||||||
|
let escaped = false;
|
||||||
|
for (const ch of str) {
|
||||||
|
if (escaped) {
|
||||||
|
current += ch;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (quote) {
|
||||||
|
case "'":
|
||||||
|
if (ch === "'") {
|
||||||
|
quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
case "\"":
|
||||||
|
switch (ch) {
|
||||||
|
case "\"":
|
||||||
|
quote = "";
|
||||||
|
continue;
|
||||||
|
case "\\":
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (ch) {
|
||||||
|
case "\\":
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
case "'":
|
||||||
|
case "\"":
|
||||||
|
quote = ch;
|
||||||
|
hasToken = true;
|
||||||
|
continue;
|
||||||
|
case " ":
|
||||||
|
case "\t":
|
||||||
|
case "\n":
|
||||||
|
if (!hasToken && current.length === 0)
|
||||||
|
continue;
|
||||||
|
args.push(current);
|
||||||
|
current = "";
|
||||||
|
hasToken = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
if (current.length > 0 || hasToken)
|
||||||
|
args.push(current);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
function launchDesktopEntry(desktopEntry, useNvidia) {
|
function launchDesktopEntry(desktopEntry, useNvidia) {
|
||||||
if (!desktopEntry || !desktopEntry.command)
|
if (!desktopEntry || !desktopEntry.command)
|
||||||
return;
|
return;
|
||||||
@@ -230,8 +288,7 @@ Singleton {
|
|||||||
cmd = [nvidiaCommand].concat(cmd);
|
cmd = [nvidiaCommand].concat(cmd);
|
||||||
|
|
||||||
if (override?.extraFlags) {
|
if (override?.extraFlags) {
|
||||||
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
|
cmd = cmd.concat(splitShellArgs(override.extraFlags));
|
||||||
cmd = cmd.concat(extraArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Singleton {
|
|||||||
return hash.toString(16).padStart(8, '0');
|
return hash.toString(16).padStart(8, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArtworkUrl(player) {
|
function _directArtworkUrl(player) {
|
||||||
if (!player) return "";
|
if (!player) return "";
|
||||||
|
|
||||||
let artUrl = player.trackArtUrl || "";
|
let artUrl = player.trackArtUrl || "";
|
||||||
@@ -54,6 +54,17 @@ Singleton {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getArtworkUrl(player) {
|
||||||
|
const directUrl = _directArtworkUrl(player);
|
||||||
|
if (directUrl !== "")
|
||||||
|
return directUrl;
|
||||||
|
|
||||||
|
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
|
||||||
|
return candidate !== player && _directArtworkUrl(candidate) !== "";
|
||||||
|
});
|
||||||
|
return _directArtworkUrl(equivalent);
|
||||||
|
}
|
||||||
|
|
||||||
function _commit(u, artKey, srcUrl) {
|
function _commit(u, artKey, srcUrl) {
|
||||||
resolvedArtUrl = u;
|
resolvedArtUrl = u;
|
||||||
_committedArtKey = u !== "" ? artKey : "";
|
_committedArtKey = u !== "" ? artKey : "";
|
||||||
@@ -171,11 +182,25 @@ Singleton {
|
|||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: MprisController
|
||||||
ignoreUnknownSignals: true
|
function onAvailablePlayersChanged() {
|
||||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
root._updateArtUrl();
|
||||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
}
|
||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: MprisController.availablePlayers
|
||||||
|
delegate: Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
|
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||||
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _trackKey() {
|
function _trackKey() {
|
||||||
@@ -201,8 +226,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
_pendingArtKey = key;
|
_pendingArtKey = key;
|
||||||
const url = getArtworkUrl(activePlayer);
|
const url = getArtworkUrl(activePlayer);
|
||||||
// Ignore Chrome's same-track thumbnail size updates.
|
// Ignore duplicate notifications, but let a richer peer replace same-track art
|
||||||
if (key !== "" && key === _committedArtKey)
|
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||||
return;
|
return;
|
||||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||||
// Chrome can publish track metadata before its new artwork URL.
|
// Chrome can publish track metadata before its new artwork URL.
|
||||||
|
|||||||
Reference in New Issue
Block a user