mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-10-28 14:07:47 -04:00
island: show synced lyrics in the media slot (#3979)
port 2.0
(cherry picked from commit 9d279e7f88)
This commit is contained in:
1 parent
4a1fadc349
commit
9fd55f6f5e
10 files changed
+195
-16
No files matched your search
@@ -0,0 +1,23 @@
|
||||
.pragma library
|
||||
|
||||
const FILLER = /\s|[♩♪♫♬.…·*~()\[\]\-–—]|🎵|🎶|🎼|\uD834[\uDD00-\uDDFF]/g;
|
||||
|
||||
function hasWords(text) {
|
||||
return String(text ?? "").replace(FILLER, "") !== "";
|
||||
}
|
||||
|
||||
// Halving a too-wide median line gives two even pages instead of a full page and a stub;
|
||||
// slack absorbs the word the greedy pager pushes over the half.
|
||||
function pageWidth(widths, cap, minimum, slack) {
|
||||
const sorted = widths.filter(width => width > 0).sort((a, b) => a - b);
|
||||
if (sorted.length === 0)
|
||||
return 0;
|
||||
const mid = sorted.length >> 1;
|
||||
const median = sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
||||
let width = cap;
|
||||
if (median <= cap)
|
||||
width = median;
|
||||
else if (median / 2 + slack <= cap)
|
||||
width = median / 2 + slack;
|
||||
return Math.ceil(Math.max(minimum, width));
|
||||
}
|
||||
@@ -37,6 +37,7 @@ var ISLAND_DEFAULTS = {
|
||||
islandPalette: "default",
|
||||
islandHighContrast: false,
|
||||
islandMediaClockVisible: true,
|
||||
islandMediaLyrics: false,
|
||||
islandMediaAutoSwitch: false,
|
||||
islandNotificationBadgeClearOnOpen: false,
|
||||
islandNotificationExpand: false,
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Modules.Plugins
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Widgets
|
||||
import "../../../Common/LyricLayout.js" as LyricLayout
|
||||
|
||||
BasePill {
|
||||
id: root
|
||||
@@ -47,7 +48,7 @@ BasePill {
|
||||
if (!controller.enabled || !controller.synced)
|
||||
return false;
|
||||
const parts = controller.lines[controller.activeIndex]?.parts ?? [];
|
||||
return parts.some(part => part.x.trim() !== "");
|
||||
return parts.some(part => LyricLayout.hasWords(part.x));
|
||||
}
|
||||
readonly property int maxTextWidth: {
|
||||
const size = SettingsData.widgetOption("music", widgetData, "mediaSize");
|
||||
|
||||
@@ -10,6 +10,8 @@ MediaActivityFace {
|
||||
isVertical: controller.isVertical
|
||||
artworkSize: controller.compactIconSize
|
||||
clockVisible: controller.mediaClockVisible
|
||||
lyricsEnabled: controller.mediaLyrics
|
||||
maxLength: controller.mediaCompactMaxLength
|
||||
function updateLength() {
|
||||
controller.setMediaContentLength(isVertical ? implicitHeight : implicitWidth);
|
||||
}
|
||||
|
||||
@@ -435,6 +435,7 @@ Item {
|
||||
homeBrightnessDisplay: SettingsData.islandLevelDisplay(root.barConfig, "islandHomeBrightnessDisplay")
|
||||
batteryStyle: root.setting("islandBatteryStyle")
|
||||
mediaClockVisible: root.setting("islandMediaClockVisible")
|
||||
mediaLyrics: root.setting("islandMediaLyrics")
|
||||
// Home and media share the dot face; see mediaFaceToggleAvailable.
|
||||
mediaAutoSwitch: !root.dotMode && root.setting("islandMediaAutoSwitch")
|
||||
launcherCycleEnabled: SettingsData.launcherStyle === "island"
|
||||
|
||||
@@ -67,6 +67,7 @@ QtObject {
|
||||
property real compactThickness: 38
|
||||
property string batteryStyle: "duo"
|
||||
property bool mediaClockVisible: true
|
||||
property bool mediaLyrics: false
|
||||
property int transientTimeout: 2200
|
||||
property int notificationTimeout: 5000
|
||||
property string transientReturnActivity: ""
|
||||
|
||||
@@ -183,6 +183,16 @@ SettingsCard {
|
||||
onToggled: checked => root.store.apply("islandMediaClockVisible", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: root.keyPrefix + "MediaLyrics"
|
||||
tags: ["island", "media", "lyrics", "compact"]
|
||||
visible: !root.isDot
|
||||
text: I18n.tr("Lyrics", "Media player lyrics button")
|
||||
description: I18n.tr("Shows the line being sung instead of the title and artist while synced lyrics are available", "media widget lyrics toggle description")
|
||||
checked: root.store.setting("islandMediaLyrics")
|
||||
onToggled: checked => root.store.apply("islandMediaLyrics", checked)
|
||||
}
|
||||
|
||||
SettingsToggleRow {
|
||||
settingKey: root.keyPrefix + "MediaAutoSwitch"
|
||||
tags: ["island", "media", "playback", "auto", "switch", "compact"]
|
||||
|
||||
@@ -7,6 +7,7 @@ import qs.Modules.DBar.Widgets
|
||||
import qs.Services
|
||||
import qs.DCommon.Widgets
|
||||
import qs.Widgets
|
||||
import "../../Common/LyricLayout.js" as LyricLayout
|
||||
|
||||
Item {
|
||||
id: root
|
||||
@@ -35,13 +36,52 @@ Item {
|
||||
}
|
||||
readonly property string minuteText: String(systemClock.date.getMinutes()).padStart(2, "0")
|
||||
property bool clockVisible: false
|
||||
readonly property real naturalTextWidth: Math.max(root.minTextWidth, titleMetrics.width, root.dense ? 0 : artistMetrics.width) + root.textGutter * 2
|
||||
readonly property real measuredWidth: {
|
||||
let width = Theme.spacingS + root.artworkSize + Theme.spacingXS + root.cavaWidth + Theme.spacingXS + root.naturalTextWidth + Theme.spacingXS;
|
||||
property bool lyricsEnabled: false
|
||||
readonly property bool lyricsWanted: lyricsEnabled && LyricsService.allowed && !isVertical
|
||||
readonly property bool lyricsLive: lyricsWanted && enabled
|
||||
readonly property bool lyricActive: {
|
||||
if (!lyricsLive)
|
||||
return false;
|
||||
const controller = LyricsService.controller;
|
||||
if (!controller.enabled || !controller.synced)
|
||||
return false;
|
||||
const parts = controller.lines[controller.activeIndex]?.parts ?? [];
|
||||
return parts.some(part => LyricLayout.hasWords(part.x));
|
||||
}
|
||||
property real maxLength: Infinity
|
||||
property var lyricWidths: []
|
||||
property real lyricWordWidth: 0
|
||||
readonly property real titleFontSize: Theme.fontSizeSmall * root.contentScale
|
||||
readonly property real chromeWidth: {
|
||||
let width = Theme.spacingS + root.artworkSize + Theme.spacingXS + root.cavaWidth + Theme.spacingXS + root.textGutter * 2 + Theme.spacingXS;
|
||||
if (root.clockVisible)
|
||||
width += clockText.width + Theme.spacingXS * 2 + Theme.spacingS;
|
||||
return width;
|
||||
}
|
||||
readonly property real lyricWidth: LyricsService.controller.synced ? LyricLayout.pageWidth(root.lyricWidths, root.maxLength - root.chromeWidth, root.minTextWidth, root.lyricWordWidth / 2) : 0
|
||||
readonly property real naturalTextWidth: root.lyricWidth > 0 ? root.lyricWidth : Math.max(root.minTextWidth, titleMetrics.width, root.dense ? 0 : artistMetrics.width)
|
||||
readonly property real measuredWidth: root.chromeWidth + root.naturalTextWidth
|
||||
|
||||
onLyricsWantedChanged: measureLyrics()
|
||||
|
||||
function measureLyrics() {
|
||||
if (!root.lyricsWanted && root.lyricWidths.length === 0)
|
||||
return;
|
||||
const widths = [];
|
||||
let words = 0;
|
||||
for (const line of root.lyricsWanted ? LyricsService.controller.lines : []) {
|
||||
for (const part of line.parts) {
|
||||
if (part.background || !LyricLayout.hasWords(part.x))
|
||||
continue;
|
||||
lyricMetrics.text = part.x.replace(/\s+/g, " ").trim();
|
||||
widths.push(lyricMetrics.width);
|
||||
words += lyricMetrics.text.split(" ").length;
|
||||
}
|
||||
}
|
||||
lyricMetrics.text = "";
|
||||
root.lyricWordWidth = words > 0 ? widths.reduce((sum, width) => sum + width, 0) / words : 0;
|
||||
root.lyricWidths = widths;
|
||||
}
|
||||
|
||||
implicitWidth: isVertical ? artworkSize + verticalPad * 2 : measuredWidth
|
||||
implicitHeight: isVertical ? verticalFace.implicitHeight + verticalPad * 2 : artworkSize
|
||||
@@ -51,11 +91,27 @@ Item {
|
||||
StyledTextMetrics {
|
||||
id: titleMetrics
|
||||
|
||||
font.pixelSize: Theme.fontSizeSmall * root.contentScale
|
||||
font.pixelSize: root.titleFontSize
|
||||
font.weight: Theme.fontWeightMedium
|
||||
text: root.mediaModel.title
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: lyricMetrics
|
||||
|
||||
font.pixelSize: root.titleFontSize
|
||||
font.weight: Theme.fontWeightMedium
|
||||
onFontChanged: root.measureLyrics()
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: LyricsService.controller
|
||||
|
||||
function onLinesChanged() {
|
||||
root.measureLyrics();
|
||||
}
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: artistMetrics
|
||||
|
||||
@@ -63,6 +119,10 @@ Item {
|
||||
text: root.dense ? "" : root.mediaModel.artist
|
||||
}
|
||||
|
||||
LyricsSubscription {
|
||||
active: root.lyricsLive && root.mediaModel.available
|
||||
}
|
||||
|
||||
SystemClock {
|
||||
id: systemClock
|
||||
|
||||
@@ -110,8 +170,11 @@ Item {
|
||||
spacing: 0
|
||||
|
||||
ScrollingText {
|
||||
id: titleText
|
||||
|
||||
x: root.textGutter
|
||||
width: parent.width - root.textGutter * 2
|
||||
visible: !root.lyricActive
|
||||
text: root.mediaModel.title
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeSmall * root.contentScale
|
||||
@@ -123,6 +186,18 @@ Item {
|
||||
overscroll: root.textGutter
|
||||
}
|
||||
|
||||
MediaLyricLine {
|
||||
x: root.textGutter
|
||||
width: parent.width - root.textGutter * 2
|
||||
height: titleText.height
|
||||
visible: root.lyricActive
|
||||
controller: LyricsService.controller
|
||||
color: Theme.surfaceText
|
||||
font.pixelSize: titleText.font.pixelSize
|
||||
font.weight: titleText.font.weight
|
||||
live: root.enabled
|
||||
}
|
||||
|
||||
StyledText {
|
||||
x: root.textGutter
|
||||
width: parent.width - root.textGutter * 2
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { loadScript } from "./qml-script.mjs";
|
||||
|
||||
const layout = loadScript(new URL("../Common/LyricLayout.js", import.meta.url));
|
||||
|
||||
test("hasWords rejects instrumental filler and keeps sung text", () => {
|
||||
for (const text of ["", " ", "♪", " ♫ ♫ ", "🎵🎶", "(...)", "♪ ~ ♪", "[—]", "\u{1D160}", " \u{1D158}\u{1D165}\u{1D16E} "])
|
||||
assert.equal(layout.hasWords(text), false, JSON.stringify(text));
|
||||
for (const text of ["I", "la la ♪", "(oh)", "夜に駆ける", "привет", "🎤"])
|
||||
assert.equal(layout.hasWords(text), true, JSON.stringify(text));
|
||||
});
|
||||
|
||||
test("pageWidth sizes from the median line", () => {
|
||||
const cases = [
|
||||
[[100, 150, 400], 200, 48, 10, 150],
|
||||
[[100, 140, 160, 900], 200, 48, 10, 150],
|
||||
[[300, 320, 340], 200, 48, 10, 170],
|
||||
[[300, 380, 1000], 210, 48, 10, 200],
|
||||
[[300, 500, 600], 200, 48, 10, 200],
|
||||
[[20, 30], 200, 48, 10, 48],
|
||||
[[0, 0], 200, 48, 10, 0],
|
||||
[[], 200, 48, 10, 0]
|
||||
];
|
||||
for (const [widths, cap, minimum, slack, expected] of cases)
|
||||
assert.equal(layout.pageWidth(widths, cap, minimum, slack), expected, JSON.stringify(widths));
|
||||
});
|
||||
@@ -3776,6 +3776,36 @@
|
||||
"parentLabel": "Bar",
|
||||
"conditionKey": "frameEnabled"
|
||||
},
|
||||
{
|
||||
"section": "islandMediaLyrics",
|
||||
"label": "Lyrics",
|
||||
"tabIndex": 3,
|
||||
"category": "General",
|
||||
"keywords": [
|
||||
"artist",
|
||||
"available",
|
||||
"bar",
|
||||
"being",
|
||||
"compact",
|
||||
"general",
|
||||
"island",
|
||||
"line",
|
||||
"lyrics",
|
||||
"media",
|
||||
"panel",
|
||||
"shows",
|
||||
"statusbar",
|
||||
"sung",
|
||||
"synced",
|
||||
"taskbar",
|
||||
"title",
|
||||
"topbar",
|
||||
"while"
|
||||
],
|
||||
"parentLabel": "Bar",
|
||||
"description": "Shows the line being sung instead of the title and artist while synced lyrics are available",
|
||||
"conditionKey": "islandEnabled"
|
||||
},
|
||||
{
|
||||
"section": "barManualVisibility",
|
||||
"label": "Manual show/hide",
|
||||
@@ -9879,7 +9909,7 @@
|
||||
"optionFile": "PrivacyOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:27",
|
||||
"section": "barWidgetOption:IslandOptions.qml:28",
|
||||
"label": "Charging",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -9887,7 +9917,7 @@
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:29",
|
||||
"section": "barWidgetOption:IslandOptions.qml:30",
|
||||
"label": "Clear badge on open",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10103,7 +10133,7 @@
|
||||
"optionFile": "WorkspaceSwitcherOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:28",
|
||||
"section": "barWidgetOption:IslandOptions.qml:29",
|
||||
"label": "Expand by default",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10326,6 +10356,14 @@
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "AppsDockOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:23",
|
||||
"label": "Lyrics",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
"runtimeType": "barWidgetOption",
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:MusicOptions.qml:4",
|
||||
"label": "Lyrics",
|
||||
@@ -10407,7 +10445,7 @@
|
||||
"optionFile": "WorkspaceSwitcherOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:30",
|
||||
"section": "barWidgetOption:IslandOptions.qml:31",
|
||||
"label": "Motion",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10455,7 +10493,7 @@
|
||||
"optionFile": "ClockOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:24",
|
||||
"section": "barWidgetOption:IslandOptions.qml:25",
|
||||
"label": "Notifications",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10735,7 +10773,7 @@
|
||||
"optionFile": "AppsDockOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:32",
|
||||
"section": "barWidgetOption:IslandOptions.qml:33",
|
||||
"label": "Spring damping",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10743,7 +10781,7 @@
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:33",
|
||||
"section": "barWidgetOption:IslandOptions.qml:34",
|
||||
"label": "Spring mass",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10751,7 +10789,7 @@
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:31",
|
||||
"section": "barWidgetOption:IslandOptions.qml:32",
|
||||
"label": "Spring stiffness",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10783,7 +10821,7 @@
|
||||
"optionFile": "WorkspaceSwitcherOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:23",
|
||||
"section": "barWidgetOption:IslandOptions.qml:24",
|
||||
"label": "Switch to media when playback starts",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10839,7 +10877,7 @@
|
||||
"optionFile": "SystemTrayOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:26",
|
||||
"section": "barWidgetOption:IslandOptions.qml:27",
|
||||
"label": "Use standard OSDs",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
@@ -10847,7 +10885,7 @@
|
||||
"optionFile": "IslandOptions.qml"
|
||||
},
|
||||
{
|
||||
"section": "barWidgetOption:IslandOptions.qml:25",
|
||||
"section": "barWidgetOption:IslandOptions.qml:26",
|
||||
"label": "Use standard popups",
|
||||
"tabIndex": 22,
|
||||
"keywords": [],
|
||||
|
||||
Reference in new issue
Block a user