media: change player style

theme: restore layer outline option
launcher/theme: tweaks and fix icon service
This commit is contained in:
bbedward
2026-09-22 09:00:34 -04:00
parent 2d1b4f905d
commit 4eb163538c
94 changed files with 866 additions and 539 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ require (
)
require (
github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd
github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/colorprofile v0.4.3 // indirect
+2 -2
View File
@@ -1,7 +1,7 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd h1:8Pa1/uo973yP2TkjBeBEdVY20hwlCfwQMq5D3bFOf7U=
github.com/AvengeMedia/dankgo v1.6.3-0.20260918210430-d03946da44cd/go.mod h1:H6yEdNZ+JJHpBSpvAEJVfXWKhIrttydVU3s2JLyTvQI=
github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba h1:Basmkwiyh8ZAOXpweOw/lu6mEV4mmZl0vK+WPhZPSqA=
github.com/AvengeMedia/dankgo v1.6.3-0.20260922103155-c6cc9c3c32ba/go.mod h1:H6yEdNZ+JJHpBSpvAEJVfXWKhIrttydVU3s2JLyTvQI=
github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9 h1:ssoiAng7ng500BgtZa1riJH6kKudQVM8Uk9a9kF7mDA=
github.com/AvengeMedia/dgop v1.6.3-0.20260920163359-59c7d81ae3b9/go.mod h1:8TFIP72NLxCbogF2hHU6sXj3RoTbHQW8beeCrisHs/Y=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
Generated
+3 -3
View File
@@ -3,11 +3,11 @@
"dank-qml-common": {
"flake": false,
"locked": {
"lastModified": 1790026440,
"narHash": "sha256-oNfBLukk6ppC6IvFksPdh17OeNOUEp8WBLduOkIZAas=",
"lastModified": 1790081738,
"narHash": "sha256-wPe6BGzBoQaH9YtKG+TXgNGxBjgDj+2Qrozg75qLDpc=",
"owner": "AvengeMedia",
"repo": "dank-qml-common",
"rev": "33558beb0630a4bea6718cfcf52bf875c63abc55",
"rev": "c9c29deeb3b6436583d20d23d3b210c5522e5292",
"type": "github"
},
"original": {
+1 -1
View File
@@ -111,7 +111,7 @@
inherit version;
pname = "dms-shell";
src = ./core;
vendorHash = "sha256-eZBP7+9NLdJ88qizCZ+mTmcQO+FHthqQSmNtUErfKv4=";
vendorHash = "sha256-sWvntuwG8BkVykveWdda5aTiXggB7uhf57ufReb4PWk=";
subPackages = [ "cmd/dms" ];
+1 -1
View File
@@ -587,7 +587,7 @@ Singleton {
readonly property color notificationFloatingSurface: notificationForegroundLayers ? readableSurface : withAlpha(readableSurface, 0)
readonly property color notificationFloatingSurfaceHigh: notificationForegroundLayers ? readableSurfaceHigh : withAlpha(readableSurfaceHigh, 0)
readonly property color notificationNestedSurface: notificationFloatingSurfaceHigh
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0.12 : (SettingsData.blurLayerOutlineOpacity ?? 0.12)))
readonly property real blurLayerOutlineOpacity: Math.max(0, Math.min(1, typeof SettingsData === "undefined" ? 0 : (SettingsData.blurLayerOutlineOpacity ?? 0)))
readonly property real layerOutlineOpacity: blurLayerOutlineOpacity
readonly property int layerOutlineWidth: layerOutlineOpacity > 0 ? 1 : 0
readonly property real floatingWindowFieldAlpha: floatingWindowForegroundAlpha
+1 -1
View File
@@ -392,7 +392,7 @@ var LOCAL_SPEC = {
coerce: Util.percentToUnit
},
blurLayerOutlineOpacity: {
def: 0.12,
def: 0,
coerce: Util.percentToUnit
},
focusRingEnabled: {
-1
View File
@@ -253,7 +253,6 @@ Item {
PolkitService.polkitAvailable;
DisplayConfigState.hasOutputBackend;
PortalService.systemColorScheme;
IconThemeService.revision;
DesktopService.isSystemd;
TrashService.count;
WallpaperCyclingService.cyclingActive;
+1 -1
View File
@@ -532,7 +532,7 @@ DankModal {
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: 1
border.width: Theme.layerOutlineWidth
visible: root.showTargetData && root.targetData.length > 0
StyledText {
@@ -220,6 +220,8 @@ DankModal {
height: 56
radius: Theme.cornerRadius
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: requestType === "confirm" || requestType === "display-passkey"
Column {
@@ -16,6 +16,8 @@ Rectangle {
height: Math.round(Theme.fontSizeMedium * 4.2)
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.fill: parent
@@ -76,6 +76,8 @@ DankFloatingWindow {
anchors.bottom: parent.bottom
height: Math.round(Theme.fontSizeMedium * 4.5)
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.top: parent.top
@@ -377,7 +377,7 @@ Item {
radius: Theme.windowRadius
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: 1
border.width: Theme.layerOutlineWidth
}
contentItem: Column {
@@ -83,8 +83,8 @@ ClippingRectangle {
anchors.margins: Theme.spacingL
radius: Theme.cornerRadiusL
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineVariant
border.width: Theme.outlineWidth
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
MouseArea {
anchors.fill: parent
@@ -108,8 +108,8 @@ Item {
anchors.centerIn: parent
radius: Theme.windowRadius
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.color: Theme.outlineVariant
border.width: Theme.outlineWidth
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
MouseArea {
anchors.fill: parent
@@ -121,6 +121,8 @@ Rectangle {
width: parent?.width ?? Theme.fieldDefaultWidth
height: expanded && hasActions ? Theme.listItemHeight : 0
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.cornerRadius
clip: true
@@ -19,8 +19,8 @@ Rectangle {
radius: Theme.cornerRadiusM
clip: true
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.color: Theme.outlineVariant
border.width: Theme.outlineWidth
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
onEntryChanged: reloadPreview()
Component.onCompleted: reloadPreview()
@@ -162,13 +162,6 @@ DankListItem {
anchors.verticalCenter: parent.verticalCenter
}
DankKeycap {
text: "↵"
textColor: root.contentColor
visible: root.isSelected
anchors.verticalCenter: parent.verticalCenter
}
Image {
width: LauncherMetrics.previewWidth
height: LauncherMetrics.previewHeight
@@ -54,6 +54,8 @@ Rectangle {
radius: Theme.cornerRadiusL
color: externalHighlight ? "transparent" : isSelected ? Theme.selectedContainer : Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: externalHighlight ? 0 : Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.fill: parent
@@ -89,16 +91,6 @@ Rectangle {
}
}
DankKeycap {
anchors.top: parent.top
anchors.left: parent.left
anchors.margins: Theme.spacingXS
text: "↵"
textColor: root.contentColor
visible: root.isSelected
z: 1
}
Loader {
anchors.fill: parent
active: !root.imageTile
@@ -1329,7 +1329,9 @@ Item {
newSections = existingNonFile.concat(fileSections);
}
newSections.sort(function (a, b) {
return a.priority - b.priority;
if (a.priority !== b.priority)
return a.priority - b.priority;
return a.id.localeCompare(b.id);
});
_applyHighlights(newSections, searchQuery);
flatModel = Scorer.flattenSections(newSections);
@@ -1713,7 +1715,9 @@ Item {
}
baseDefs.sort(function (a, b) {
return a.priority - b.priority;
if (a.priority !== b.priority)
return a.priority - b.priority;
return a.id.localeCompare(b.id);
});
return baseDefs;
}
@@ -1930,7 +1934,9 @@ Item {
return null;
sectionsData.sort(function (a, b) {
return a.priority - b.priority;
if (a.priority !== b.priority)
return a.priority - b.priority;
return a.id.localeCompare(b.id);
});
return sectionsData;
}
@@ -4,72 +4,72 @@ function getFileIcon(filename) {
var ext = filename.lastIndexOf(".") > 0 ? filename.substring(filename.lastIndexOf(".") + 1).toLowerCase() : "";
switch (ext) {
case "pdf":
return "picture_as_pdf";
case "doc":
case "docx":
case "odt":
return "description";
case "xls":
case "xlsx":
case "ods":
return "table_chart";
case "ppt":
case "pptx":
case "odp":
return "slideshow";
case "txt":
case "md":
case "rst":
return "article";
case "jpg":
case "jpeg":
case "png":
case "gif":
case "svg":
case "webp":
return "image";
case "mp3":
case "wav":
case "flac":
case "ogg":
return "audio_file";
case "mp4":
case "mkv":
case "avi":
case "webm":
return "video_file";
case "zip":
case "tar":
case "gz":
case "7z":
case "rar":
return "folder_zip";
case "js":
case "ts":
case "py":
case "rs":
case "go":
case "java":
case "c":
case "cpp":
case "h":
return "code";
case "html":
case "css":
case "htm":
return "web";
case "json":
case "xml":
case "yaml":
case "yml":
return "data_object";
case "sh":
case "bash":
case "zsh":
return "terminal";
default:
return "insert_drive_file";
case "pdf":
return "picture_as_pdf";
case "doc":
case "docx":
case "odt":
return "description";
case "xls":
case "xlsx":
case "ods":
return "table_chart";
case "ppt":
case "pptx":
case "odp":
return "slideshow";
case "txt":
case "md":
case "rst":
return "article";
case "jpg":
case "jpeg":
case "png":
case "gif":
case "svg":
case "webp":
return "image";
case "mp3":
case "wav":
case "flac":
case "ogg":
return "audio_file";
case "mp4":
case "mkv":
case "avi":
case "webm":
return "video_file";
case "zip":
case "tar":
case "gz":
case "7z":
case "rar":
return "folder_zip";
case "js":
case "ts":
case "py":
case "rs":
case "go":
case "java":
case "c":
case "cpp":
case "h":
return "code";
case "html":
case "css":
case "htm":
return "web";
case "json":
case "xml":
case "yaml":
case "yml":
return "data_object";
case "sh":
case "bash":
case "zsh":
return "terminal";
default:
return "insert_drive_file";
}
}
@@ -116,19 +116,13 @@ function classifyAppSource(app) {
if (cmd0 === "flatpak" || exec.indexOf("flatpak run ") !== -1)
return "flatpak";
if (cmd0 === "snap"
|| exec.indexOf("bamf_desktop_file_hint=") !== -1
|| exec.indexOf("/snap/bin/") !== -1
|| exec.indexOf("/snap/core") !== -1
|| exec.indexOf("snap run ") === 0)
if (cmd0 === "snap" || exec.indexOf("bamf_desktop_file_hint=") !== -1 || exec.indexOf("/snap/bin/") !== -1 || exec.indexOf("/snap/core") !== -1 || exec.indexOf("snap run ") === 0)
return "snap";
if (/\.appimage(\s|$|")/i.test(execRaw) || id.indexOf("appimagekit_") === 0)
return "appimage";
if (exec.indexOf("/nix/store/") !== -1
|| exec.indexOf("/run/current-system/sw/") !== -1
|| exec.indexOf("/etc/profiles/per-user/") !== -1)
if (exec.indexOf("/nix/store/") !== -1 || exec.indexOf("/run/current-system/sw/") !== -1 || exec.indexOf("/etc/profiles/per-user/") !== -1)
return "nix";
return "system";
@@ -174,7 +168,9 @@ function sortPluginIdsByOrder(pluginIds, order) {
return pluginIds.slice().sort(function (a, b) {
var aOrder = orderMap[a] !== undefined ? orderMap[a] : 9999;
var bOrder = orderMap[b] !== undefined ? orderMap[b] : 9999;
return aOrder - bOrder;
if (aOrder !== bOrder)
return aOrder - bOrder;
return a.localeCompare(b);
});
}
@@ -187,7 +183,9 @@ function sortPluginsOrdered(plugins, order) {
return plugins.sort(function (a, b) {
var aOrder = orderMap[a.id] !== undefined ? orderMap[a.id] : 9999;
var bOrder = orderMap[b.id] !== undefined ? orderMap[b.id] : 9999;
return aOrder - bOrder;
if (aOrder !== bOrder)
return aOrder - bOrder;
return a.id.localeCompare(b.id);
});
}
@@ -196,8 +194,17 @@ function parseFileSearchPrefix(query) {
return null;
var rest = query.substring(1);
if (rest === "d" || rest.startsWith("d ") || rest.startsWith("d\t"))
return { type: "dir", query: rest.substring(1).trim() };
return {
type: "dir",
query: rest.substring(1).trim()
};
if (rest === "f" || rest.startsWith("f ") || rest.startsWith("f\t"))
return { type: "file", query: rest.substring(1).trim() };
return { type: null, query: rest.trim() };
return {
type: "file",
query: rest.substring(1).trim()
};
return {
type: null,
query: rest.trim()
};
}
@@ -330,6 +330,8 @@ FocusScope {
visible: !(root.parentModal?.frameOwnsConnectedChrome ?? false) && !Theme.blurLayersActive
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.windowRadius
}
DankFilterChips {
@@ -370,13 +372,6 @@ FocusScope {
color: Theme.onSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: "↵ " + I18n.tr("Open")
font.pixelSize: Theme.fontSizeSmall
color: Theme.onSurfaceVariant
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: I18n.tr("Tab", "keyboard tab key name", true) + " " + I18n.tr("Actions", "noun, launcher footer hint after the tab key, also section label")
@@ -329,6 +329,8 @@ Item {
visible: delegateRoot.visible
opacity: delegateRoot.opacity
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.groupedListInnerRadius
topLeftRadius: delegateRoot.modelData?.firstInGroup ? Theme.groupedListOuterRadius : radius
topRightRadius: topLeftRadius
@@ -417,6 +419,8 @@ Item {
visible: delegateRoot.visible
opacity: delegateRoot.opacity
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.cornerRadiusL
}
+115 -100
View File
@@ -18,180 +18,195 @@ const Weights = {
file: 800,
action: 600
}
}
};
function tokenize(text) {
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) { return w.length > 0 })
return text.toLowerCase().trim().split(/[\s\-_]+/).filter(function (w) {
return w.length > 0;
});
}
function hasWordBoundaryMatch(text, query) {
var textWords = tokenize(text)
var queryWords = tokenize(query)
var textWords = tokenize(text);
var queryWords = tokenize(query);
if (queryWords.length === 0) return false
if (queryWords.length > textWords.length) return false
if (queryWords.length === 0)
return false;
if (queryWords.length > textWords.length)
return false;
for (var i = 0; i <= textWords.length - queryWords.length; i++) {
var allMatch = true
var allMatch = true;
for (var j = 0; j < queryWords.length; j++) {
if (!textWords[i + j].startsWith(queryWords[j])) {
allMatch = false
break
allMatch = false;
break;
}
}
if (allMatch) return true
if (allMatch)
return true;
}
return false
return false;
}
function levenshteinDistance(s1, s2) {
var len1 = s1.length
var len2 = s2.length
var prev = new Array(len2 + 1)
var curr = new Array(len2 + 1)
var len1 = s1.length;
var len2 = s2.length;
var prev = new Array(len2 + 1);
var curr = new Array(len2 + 1);
for (var j = 0; j <= len2; j++)
prev[j] = j
prev[j] = j;
for (var i = 1; i <= len1; i++) {
curr[0] = i
curr[0] = i;
for (var j = 1; j <= len2; j++) {
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost)
var cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
}
var tmp = prev
prev = curr
curr = tmp
var tmp = prev;
prev = curr;
curr = tmp;
}
return prev[len2]
return prev[len2];
}
function fuzzyScore(text, query) {
var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3
var bestScore = 0
var maxDistance = query.length === 3 ? 1 : query.length <= 6 ? 2 : 3;
var bestScore = 0;
if (Math.abs(text.length - query.length) <= maxDistance) {
var distance = levenshteinDistance(text, query)
var distance = levenshteinDistance(text, query);
if (distance <= maxDistance) {
var maxLen = Math.max(text.length, query.length)
bestScore = 1 - (distance / maxLen)
var maxLen = Math.max(text.length, query.length);
bestScore = 1 - (distance / maxLen);
}
}
var words = tokenize(text)
var words = tokenize(text);
for (var i = 0; i < words.length && bestScore < 0.8; i++) {
if (Math.abs(words[i].length - query.length) > maxDistance) continue
var wordDistance = levenshteinDistance(words[i], query)
if (Math.abs(words[i].length - query.length) > maxDistance)
continue;
var wordDistance = levenshteinDistance(words[i], query);
if (wordDistance <= maxDistance) {
var wordMaxLen = Math.max(words[i].length, query.length)
var score = 1 - (wordDistance / wordMaxLen)
bestScore = Math.max(bestScore, score)
var wordMaxLen = Math.max(words[i].length, query.length);
var score = 1 - (wordDistance / wordMaxLen);
bestScore = Math.max(bestScore, score);
}
}
return bestScore
return bestScore;
}
function calculateTextScore(name, query) {
if (name === query) return Weights.exactMatch
if (name.startsWith(query)) return Weights.prefixMatch
if (hasWordBoundaryMatch(name, query)) return Weights.wordBoundary
if (name.includes(query)) return Weights.substring
if (name === query)
return Weights.exactMatch;
if (name.startsWith(query))
return Weights.prefixMatch;
if (hasWordBoundaryMatch(name, query))
return Weights.wordBoundary;
if (name.includes(query))
return Weights.substring;
if (query.length >= 3) {
var fs = fuzzyScore(name, query)
if (fs > 0) return fs * Weights.fuzzy
var fs = fuzzyScore(name, query);
if (fs > 0)
return fs * Weights.fuzzy;
}
return 0
return 0;
}
function score(item, query, frecencyData) {
var typeBonus = Weights.typeBonus[item.type] || 0
var typeBonus = Weights.typeBonus[item.type] || 0;
if (!query || query.length === 0) {
var usageCount = frecencyData ? frecencyData.usageCount : 0
return typeBonus + (usageCount * 100)
var usageCount = frecencyData ? frecencyData.usageCount : 0;
return typeBonus + (usageCount * 100);
}
var name = (item.name || "").toLowerCase()
var q = query.toLowerCase()
var name = (item.name || "").toLowerCase();
var q = query.toLowerCase();
var textScore = calculateTextScore(name, q)
var textScore = calculateTextScore(name, q);
if (textScore === 0 && item.subtitle) {
var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q)
textScore = subtitleScore * 0.5
var subtitleScore = calculateTextScore(item.subtitle.toLowerCase(), q);
textScore = subtitleScore * 0.5;
}
if (textScore === 0 && item.keywords) {
for (var i = 0; i < item.keywords.length; i++) {
var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q)
var keywordScore = calculateTextScore(item.keywords[i].toLowerCase(), q);
if (keywordScore > 0) {
textScore = keywordScore * 0.3
break
textScore = keywordScore * 0.3;
break;
}
}
}
if (textScore === 0 && item.data && item.data.genericName) {
var genericLower = item.data.genericName.toLowerCase()
var genericLower = item.data.genericName.toLowerCase();
if (genericLower.startsWith(q))
textScore = Weights.genericPrefix
textScore = Weights.genericPrefix;
else if (genericLower.includes(q))
textScore = Weights.generic
textScore = Weights.generic;
}
if (textScore === 0 && item.id && item.type === "app") {
var id = item.id.toLowerCase().replace(/\.desktop$/, "")
var id = item.id.toLowerCase().replace(/\.desktop$/, "");
if (id.includes(q)) {
textScore = Weights.id
textScore = Weights.id;
}
}
if (textScore === 0) return 0
if (textScore === 0)
return 0;
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0
var usageBonus = frecencyData ? Math.min(frecencyData.usageCount * 50, Weights.frecency) : 0;
return textScore + usageBonus + typeBonus
return textScore + usageBonus + typeBonus;
}
function scoreItems(items, query, getFrecencyFn) {
var scored = []
var scored = [];
for (var i = 0; i < items.length; i++) {
var item = items[i]
var itemScore
var item = items[i];
var itemScore;
if (item._preScored !== undefined && (query || item._preScored > 900)) {
itemScore = item._preScored
itemScore = item._preScored;
} else {
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null
itemScore = score(item, query, frecencyData)
var frecencyData = getFrecencyFn ? getFrecencyFn(item) : null;
itemScore = score(item, query, frecencyData);
}
if (itemScore > 0 || !query || query.length === 0) {
scored.push({
item: item,
score: itemScore
})
score: itemScore,
order: scored.length
});
}
}
scored.sort(function (a, b) {
return b.score - a.score
})
if (a.score !== b.score)
return b.score - a.score;
return a.order - b.order;
});
return scored
return scored;
}
function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSection) {
var sections = {}
var result = []
var limit = maxPerSection || 50
var sections = {};
var result = [];
var limit = maxPerSection || 50;
for (var i = 0; i < sectionOrder.length; i++) {
var sectionId = sectionOrder[i].id
var sectionId = sectionOrder[i].id;
sections[sectionId] = {
id: sectionId,
title: sectionOrder[i].title,
@@ -200,53 +215,53 @@ function groupBySection(scoredItems, sectionOrder, sortAlphabetically, maxPerSec
items: [],
collapsed: false,
flatStartIndex: 0
}
};
}
for (var i = 0; i < scoredItems.length; i++) {
var scoredItem = scoredItems[i]
var item = scoredItem.item
var sectionId = item.section || "apps"
var scoredItem = scoredItems[i];
var item = scoredItem.item;
var sectionId = item.section || "apps";
if (sections[sectionId] && sections[sectionId].items.length < limit) {
sections[sectionId].items.push(item)
sections[sectionId].items.push(item);
} else if (sections["apps"] && sections["apps"].items.length < limit) {
sections["apps"].items.push(item)
sections["apps"].items.push(item);
}
}
for (var i = 0; i < sectionOrder.length; i++) {
var section = sections[sectionOrder[i].id]
var section = sections[sectionOrder[i].id];
if (section && section.items.length > 0) {
if (sortAlphabetically && section.id === "apps") {
section.items.sort(function (a, b) {
return (a.name || "").localeCompare(b.name || "")
})
return (a.name || "").localeCompare(b.name || "");
});
}
result.push(section)
result.push(section);
}
}
return result
return result;
}
function flattenSections(sections) {
var flat = []
flat._sectionBounds = null
var bounds = {}
var flat = [];
flat._sectionBounds = null;
var bounds = {};
for (var i = 0; i < sections.length; i++) {
var section = sections[i]
var section = sections[i];
flat.push({
isHeader: true,
section: section,
sectionId: section.id,
sectionIndex: i
})
});
var itemStart = flat.length
section.flatStartIndex = itemStart
var itemStart = flat.length;
section.flatStartIndex = itemStart;
if (!section.collapsed) {
for (var j = 0; j < section.items.length; j++) {
@@ -256,21 +271,21 @@ function flattenSections(sections) {
sectionId: section.id,
sectionIndex: i,
indexInSection: j
})
});
}
}
var itemEnd = flat.length - 1
var itemCount = flat.length - itemStart
var itemEnd = flat.length - 1;
var itemCount = flat.length - itemStart;
if (itemCount > 0) {
bounds[section.id] = {
start: itemStart,
end: itemEnd,
count: itemCount
}
};
}
}
flat._sectionBounds = bounds
return flat
flat._sectionBounds = bounds;
return flat;
}
@@ -65,6 +65,8 @@ LauncherTile {
anchors.fill: parent
radius: Theme.cornerRadiusM
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
ScreencopyView {
id: screencopyView
@@ -89,6 +89,8 @@ DankModal {
height: 70
radius: Theme.cornerRadius
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
StyledText {
anchors.centerIn: parent
@@ -172,6 +172,8 @@ Item {
height: keybindsGrid.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
readonly property bool useTwoColumns: width > 500
readonly property int columnCount: useTwoColumns ? 2 : 1
@@ -251,6 +253,8 @@ Item {
height: noKeybindsColumn.height + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: !root.hasKeybinds
Column {
@@ -290,6 +290,8 @@ Item {
anchors.rightMargin: Theme.spacingXL
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
clip: true
Column {
@@ -16,6 +16,8 @@ Rectangle {
height: Math.round(Theme.fontSizeMedium * 6.4)
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.fill: parent
@@ -163,6 +163,8 @@ DankFloatingWindow {
height: indicatorHeight
radius: Theme.fullRadius(width, height)
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Row {
id: pageIndicatorRow
@@ -224,6 +226,8 @@ DankFloatingWindow {
anchors.bottom: parent.bottom
height: Math.round(Theme.fontSizeMedium * 4.5)
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.top: parent.top
@@ -14,6 +14,8 @@ Rectangle {
height: Math.round(Theme.fontSizeMedium * 3.1)
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.fill: parent
@@ -16,6 +16,8 @@ Rectangle {
height: Math.round(Theme.fontSizeMedium * 4.5)
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Rectangle {
anchors.fill: parent
@@ -17,8 +17,8 @@ Rectangle {
height: Math.round(Theme.fontSizeMedium * 5)
radius: Theme.cornerRadius
color: bgColor
border.width: selected ? 2 : 0
border.color: selected ? iconColor : Theme.withAlpha(iconColor, 0)
border.width: selected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: selected ? iconColor : Theme.outlineMedium
scale: mouseArea.pressed ? 0.97 : 1
Behavior on scale {
+4
View File
@@ -327,6 +327,8 @@ DankModal {
height: 56
radius: Theme.cornerRadius
color: muxModal.selectedIndex === -1 ? Theme.selectedContainer : (newMouse.containsMouse ? Theme.foregroundColor(Theme.chipSurface, true) : Theme.floatingWindowNestedSurface)
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
RowLayout {
anchors.fill: parent
@@ -404,6 +406,8 @@ DankModal {
height: 64
radius: Theme.cornerRadius
color: muxModal.selectedIndex === index ? Theme.selectedContainer : (sessionMouse.containsMouse ? Theme.cardSurface : Theme.withAlpha(Theme.cardSurface, 0))
border.width: muxModal.selectedIndex === index || sessionMouse.containsMouse ? Theme.layerOutlineWidth : 0
border.color: Theme.outlineMedium
MouseArea {
id: sessionMouse
+2 -2
View File
@@ -95,8 +95,8 @@ DankModal {
height: parent.height - 140
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineStrong
border.width: 1
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
clip: true
DankFlickable {
+2 -2
View File
@@ -196,8 +196,8 @@ DankModal {
return Theme.floatingWindowNestedSurface;
}
border.color: isActive ? Theme.primary : (isSelected ? Theme.withAlpha(Theme.primary, 0.5) : Theme.withAlpha(Theme.primary, 0))
border.width: (isActive || isSelected) ? 2 : 0
border.color: isActive ? Theme.primary : (isSelected ? Theme.withAlpha(Theme.primary, 0.5) : Theme.outlineMedium)
border.width: (isActive || isSelected) ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
// Shortcut Key Badge on Top-Right Corner
Rectangle {
@@ -325,6 +325,8 @@ DankFloatingWindow {
width: parent.width
height: showBanner ? bannerContent.implicitHeight + Theme.spacingM * 2 : 0
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: showBanner
clip: true
+2
View File
@@ -160,6 +160,8 @@ DankModal {
height: bodyCol.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadiusL
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Row {
anchors.fill: parent
@@ -79,6 +79,8 @@ Item {
height: menu.implicitHeight + Theme.spacingS * 2
radius: Theme.windowRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
x: Math.max(Theme.spacingS, Math.min(root.anchorX + root.anchorWidth - width, root.width - width - Theme.spacingS))
y: root.anchorY - height - Theme.spacingS < Theme.spacingS ? root.anchorY + root.anchorHeight + Theme.spacingS : root.anchorY - height - Theme.spacingS
opacity: root.visible ? 1 : 0
@@ -134,6 +134,8 @@ Item {
height: column.implicitHeight + Theme.spacingS * 2
radius: Theme.windowRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
opacity: root._open ? 1 : 0
scale: root._open ? 1 : CcMetrics.popupEnterScale
transformOrigin: I18n.isRtl ? Item.TopLeft : Item.TopRight
@@ -82,6 +82,8 @@ Item {
height: column.implicitHeight + Theme.spacingL * 2
radius: Theme.windowRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
opacity: root.shown ? 1 : 0
scale: root.shown ? 1 : CcMetrics.popupEnterScale
@@ -48,6 +48,8 @@ Item {
anchors.fill: parent
radius: root.bodyRadius
color: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: root.tall
}
@@ -63,6 +65,8 @@ Item {
y: root.vertical ? parent.height - height : root.tall ? 0 : (parent.height - height) / 2
buttonSize: root.actionSize
backgroundColor: CcMetrics.tileInactiveColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
iconName: root.iconName
iconSize: CcMetrics.iconBoxIconSize
iconColor: CcMetrics.tileInactiveContent
@@ -151,6 +151,8 @@ Item {
anchors.fill: parent
radius: root.bodyRadius
color: root.bodyColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on color {
enabled: CcMetrics.animationsEnabled && !SettingsData.reduceMotion
@@ -221,6 +223,8 @@ Item {
height: width
radius: root.hasIconBox ? root.iconBoxRadius : 0
color: root.hasIconBox ? root.iconBoxColor : "transparent"
border.width: root.hasIconBox ? Theme.layerOutlineWidth : 0
border.color: Theme.outlineMedium
activeFocusOnTab: root.showExpand && root.toggle && root.acceptsInput
Accessible.role: root.toggle ? Accessible.CheckBox : Accessible.Button
Accessible.name: root.title
@@ -27,6 +27,8 @@ Rectangle {
implicitHeight: CcMetrics.libraryPanelHeight
radius: Theme.windowRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
MouseArea {
anchors.fill: parent
@@ -137,6 +137,7 @@ BasePill {
round: false
checkable: true
checked: root._isPlaying
iconFilled: false
iconName: root._isPlaying ? "pause" : "play_arrow"
Accessible.name: root._isPlaying ? I18n.tr("Pause") : I18n.tr("Play")
enabled: root.activePlayer?.canTogglePlaying ?? false
@@ -198,8 +198,12 @@ Singleton {
readonly property real mediaArtLowResScale: 0.85
readonly property real mediaArtSizeMaterial: 200
readonly property real mediaArtRadius: Theme.cornerRadiusXL
readonly property real mediaArtSoftRadiusRatio: 0.3
readonly property real mediaArtPlaceholderIcon: Theme.buttonHeightM
readonly property real mediaSeekbarHeight: 22
readonly property real mediaPlayWidthRatio: 1.5
readonly property real mediaPlayWidthStep: 0.75
readonly property real mediaTransportPressScale: 1.2
readonly property real accentSelectedAlpha: 0.2
readonly property real groupIdleAlpha: 0.1
readonly property int mediaPositionPollInterval: 1000
@@ -113,6 +113,14 @@ Singleton {
{
"value": "circle",
"text": I18n.tr("Circle")
},
{
"value": "square",
"text": I18n.tr("Soft square", "Album artwork shape")
},
{
"value": "slanted",
"text": I18n.tr("Slanted", "Album artwork shape")
}
]), choice("titleFont", I18n.tr("Title font"), MediaOptions.defaultTitleFont, Theme.fontChoices), toggle("seekbar", I18n.tr("Show seekbar"), MediaOptions.defaults.seekbar), toggle("waveProgress", I18n.tr("Wave progress bars"), MediaOptions.defaults.waveProgress), toggle("albumArtBackdrop", I18n.tr("Album art backdrop"), MediaOptions.defaults.albumArtBackdrop), toggle("albumArtAccent", I18n.tr("Use album art accent"), MediaOptions.defaults.albumArtAccent), toggle("animatedArt", I18n.tr("Apple Music animated covers"), MediaOptions.defaults.animatedArt, I18n.tr("Sends the playing artist and album name to Apple")), toggle("lyrics", I18n.tr("Lyrics", "Media player lyrics button"), MediaOptions.defaults.lyrics, I18n.tr("Sends the playing track, artist and album name to enabled lyrics providers"))]
},
@@ -219,11 +219,11 @@ FocusScope {
anchors.topMargin: Theme.spacingM
anchors.horizontalCenter: parent.horizontalCenter
width: parent.width - Theme.spacingM * 2
text: root.unsynced ? I18n.tr("Unsynced", "Lyrics have no timestamps") : I18n.tr("Lyrics", "Media player lyrics button")
text: I18n.tr("Unsynced", "Lyrics have no timestamps")
color: Theme.onSurfaceVariant
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
visible: !root.compact
visible: root.unsynced && !root.compact
}
DankListView {
@@ -0,0 +1,73 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Widgets
import qs.Common
import qs.Modules.DankDash
import "../../../DankCommon/Widgets/MaterialShapes.js" as Shapes
ClippingRectangle {
id: root
property string artStyle: "rounded"
readonly property real contentRadius: artStyle === "slanted" ? width * DashMetrics.mediaArtSoftRadiusRatio * Theme.shapeScale : radius
radius: {
switch (artStyle) {
case "circle":
return width / 2;
case "square":
return width * DashMetrics.mediaArtSoftRadiusRatio * Theme.shapeScale;
case "slanted":
return 0;
}
return DashMetrics.mediaArtRadius;
}
color: "transparent"
antialiasing: true
layer.enabled: artStyle === "slanted"
layer.effect: MultiEffect {
maskEnabled: true
maskSource: shapeMask.item
}
Loader {
id: shapeMask
anchors.fill: parent
active: root.layer.enabled
visible: false
sourceComponent: Item {
layer.enabled: true
Shape {
id: shape
readonly property real scaleX: boundingRect.width > 0 ? root.width / boundingRect.width : 1
readonly property real scaleY: boundingRect.height > 0 ? root.height / boundingRect.height : 1
x: -boundingRect.x * scaleX
y: -boundingRect.y * scaleY
width: root.width
height: root.height
preferredRendererType: Shape.CurveRenderer
transform: Scale {
xScale: shape.scaleX
yScale: shape.scaleY
}
ShapePath {
strokeWidth: 0
strokeColor: "transparent"
fillColor: Theme.withAlpha(Theme.surface, 1)
PathSvg {
path: Shapes.buildPath("slanted", root.width, root.height, Theme.shapeScale === 0)
}
}
}
}
}
}
@@ -1,7 +1,6 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Widgets
import qs.Widgets
import qs.Modules.DankDash
@@ -12,14 +11,12 @@ Item {
property bool showLyrics: true
property bool holdArt: false
readonly property real artRadius: root.player.options?.artStyle === "circle" ? width / 2 : DashMetrics.mediaArtRadius
readonly property real artRadius: surface.contentRadius
ClippingRectangle {
MediaArtSurface {
id: surface
anchors.fill: parent
radius: root.artRadius
color: "transparent"
antialiasing: true
artStyle: root.player.options?.artStyle ?? "rounded"
MediaArtwork {
id: art
@@ -59,6 +59,8 @@ Item {
anchors.fill: parent
radius: root.backdropRadius
color: root.surfaceColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Loader {
anchors.fill: parent
@@ -94,9 +94,10 @@ Item {
iconName: AudioService.sinkIcon(AudioService.sink)
tooltipText: root.fittedSinkName === root.sinkName ? root.devicesLabel : root.sinkName
Accessible.name: root.devicesLabel + ": " + root.sinkName
backgroundColor: "transparent"
textColor: Theme.onSurfaceVariant
horizontalPadding: Theme.spacingS
buttonHeight: Theme.buttonHeightXS
backgroundColor: MediaAccentService.accentSecondaryContainer
textColor: MediaAccentService.onAccentSecondaryContainer
horizontalPadding: Theme.spacingM
onClicked: root.player.togglePanel("devices")
MediaSinkWheel {}
@@ -5,6 +5,7 @@ import QtQuick.Layouts
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation
RowLayout {
@@ -14,7 +15,8 @@ RowLayout {
required property var presentation
readonly property var focusTargets: [shuffleButton, previousButton, playButton, nextButton, repeatButton]
readonly property bool hasControls: !!presentation && (presentation.play || presentation.previous || presentation.next || presentation.shuffle || presentation.repeat)
readonly property bool hasControls: !!presentation
readonly property var pressSpring: Theme.springPreset("fast", Theme.expressiveDurations.expressiveFastSpatial)
spacing: Theme.spacingS
implicitHeight: Theme.buttonHeightM
@@ -38,7 +40,7 @@ RowLayout {
id: playButton
mediaAction: "play"
size: "m"
Layout.preferredWidth: Theme.buttonHeightM + Theme.spacingS
weight: DashMetrics.mediaPlayWidthRatio + (Number(shuffleButton.visible) + Number(repeatButton.visible)) * DashMetrics.mediaPlayWidthStep
variant: "filled"
iconSize: Theme.iconSizeLarge
containerColor: MediaAccentService.accentContainer
@@ -56,15 +58,32 @@ RowLayout {
}
component TransportButton: MediaTransportButton {
id: button
property real weight: 1
readonly property real targetWeight: weight * (pressed ? DashMetrics.mediaTransportPressScale : 1)
player: root.player
keepNavigation: true
Layout.fillWidth: true
Layout.preferredWidth: Theme.minimumTouchTargetSize
Layout.preferredWidth: Theme.minimumTouchTargetSize * widthSpring.value
Layout.minimumWidth: Theme.iconSizeMedium + Theme.spacingXS * 2
Layout.alignment: Qt.AlignVCenter
round: false
buttonSize: size === "m" ? Theme.buttonHeightM : Theme.minimumTouchTargetSize
buttonSize: Theme.buttonHeightM
radius: Theme.buttonRadius(width, height, buttonSize, pressed, false)
variant: "tonal"
containerColor: checked ? MediaAccentService.accentContainer : MediaAccentService.accentSecondaryContainer
contentColor: checked ? MediaAccentService.onAccentContainer : MediaAccentService.onAccentSecondaryContainer
onTargetWeightChanged: widthSpring.retarget(targetWeight)
SpringMotion {
id: widthSpring
enabled: root.visible && root.player.live && !Theme.springMotionDisabled
stiffness: root.pressSpring.stiffness
damping: root.pressSpring.damping
Component.onCompleted: snapTo(button.targetWeight)
}
}
}
@@ -11,6 +11,7 @@ DankIconButton {
required property var player
required property string mediaAction
property bool keepNavigation: false
readonly property var presentation: root.player.presentation
readonly property var activePlayer: root.player.activePlayer
@@ -18,6 +19,7 @@ DankIconButton {
readonly property bool repeatingTrack: root.presentation?.loopState === MprisLoopState.Track
checkable: root.mediaAction === "shuffle" || root.mediaAction === "play" || root.mediaAction === "repeat"
iconFilled: false
iconName: {
switch (root.mediaAction) {
@@ -56,11 +58,11 @@ DankIconButton {
case "shuffle":
return !!root.presentation?.shuffle;
case "previous":
return !!root.presentation?.previous;
return root.keepNavigation ? !!root.presentation : !!root.presentation?.previous;
case "play":
return !!root.presentation?.play;
case "next":
return !!root.presentation?.next;
return root.keepNavigation ? !!root.presentation : !!root.presentation?.next;
case "repeat":
return !!root.presentation?.repeat;
}
@@ -72,7 +74,7 @@ DankIconButton {
case "shuffle":
return !!root.activePlayer?.shuffleSupported && !!root.activePlayer?.canControl;
case "previous":
return !!root.activePlayer?.canGoPrevious || !!root.activePlayer?.canSeek;
return !!root.activePlayer?.canGoPrevious || (!root.keepNavigation && !!root.activePlayer?.canSeek);
case "play":
return !!root.activePlayer?.canTogglePlaying;
case "next":
@@ -10,11 +10,9 @@ import "../../../Common/Format.js" as Format
MediaChromeBase {
id: root
readonly property bool compact: width < Theme.smallBreakpoint
readonly property bool compact: width < Theme.smallBreakpoint || halfWidth < headerHeight + controls.implicitHeight
readonly property real halfWidth: Math.max(0, (width - padding * 2 - Theme.spacingXL) / 2)
readonly property real bodyHeight: Math.max(0, height - padding * 2)
readonly property real paneWidth: artworkSize
readonly property real paneOffset: Math.max(0, (width - padding * 2 - paneWidth * 2 - Theme.spacingXL) / 2)
// !TODO: expose this variant? playerPaneOpen false gives art beside lyrics, or art alone with hover transport
readonly property bool playerPane: root.player.playerPaneOpen
readonly property bool controlsColumn: playerPane
@@ -26,24 +24,24 @@ MediaChromeBase {
readonly property real columnHeight: compact ? artworkSize : Math.max(artworkSize, paneHeight)
readonly property real paneHeight: Math.max(artworkSize, headerHeight + controls.implicitHeight)
readonly property real paneMinHeight: Math.max(naturalArtSize, headerHeight + controls.implicitHeight)
readonly property real headerHeight: sourceRow.height + Theme.spacingL
readonly property real headerHeight: sourceRow.height + Theme.spacingS
readonly property real renderScale: Window.window?.devicePixelRatio ?? Screen.devicePixelRatio
readonly property int artSide: TrackArtService.resolvedArtSide
readonly property bool lowResArt: artSide > 0 && artSide < artworkSize * renderScale * DashMetrics.mediaArtLowResRatio
artSize: DashMetrics.mediaArtSizeDash
surfaceColor: DashMetrics.cardColor
surfaceColor: Theme.foregroundColor(Theme.hostSurface)
baseHeight: Math.max(DashMetrics.tabMinHeight, padding * 2 + (compact ? naturalArtSize + Theme.spacingXL + paneMinHeight : Math.max(naturalArtSize, paneMinHeight)))
focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], seekbar.canSeek && seekBlock.visible ? [seekbar] : [], transport.focusTargets, sourceGroup.focusTargets)
focusTargets: (viewToggle.visible ? [viewToggle] : []).concat(artTransportLoader.item?.focusTargets ?? [], [playerButton], sourceGroup.focusTargets, transport.focusTargets, seekbar.canSeek && seekBlock.visible ? [seekbar] : [])
panelButtons: [playerButton].concat(sourceGroup.panelButtons)
inlineVolume: true
Item {
id: artPane
x: root.compact || !root.splitPanes ? (parent.width - width) / 2 : root.paneOffset + (I18n.isRtl ? root.paneWidth + Theme.spacingXL : 0)
x: root.compact || !root.splitPanes ? (parent.width - width) / 2 : I18n.isRtl ? parent.width - width : 0
y: root.compact && root.splitPanes ? 0 : (parent.height - height) / 2
width: root.compact ? parent.width : root.paneWidth
width: root.compact ? parent.width : art.width
height: root.columnHeight
Behavior on x {
@@ -57,8 +55,11 @@ MediaChromeBase {
Item {
id: art
readonly property real scaledSize: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1)
anchors.centerIn: parent
width: root.artworkSize * (root.lowResArt ? DashMetrics.mediaArtLowResScale : 1)
width: root.compact ? scaledSize : Math.min(root.artworkSize, Math.max(scaledSize, root.headerHeight + controls.implicitHeight))
height: width
HoverHandler {
@@ -191,6 +192,7 @@ MediaChromeBase {
component ArtTransportButton: MediaTransportButton {
player: root.player
keepNavigation: true
round: true
checkable: false
buttonSize: Theme.minimumTouchTargetSize
@@ -202,9 +204,9 @@ MediaChromeBase {
Item {
id: contentPane
x: root.compact ? 0 : root.paneOffset + (I18n.isRtl ? 0 : root.paneWidth + Theme.spacingXL)
x: root.compact || I18n.isRtl ? 0 : artPane.width + Theme.spacingXL
y: root.compact ? root.artworkSize + Theme.spacingXL : (parent.height - height) / 2
width: root.compact ? parent.width : root.paneWidth
width: root.compact ? parent.width : parent.width - artPane.width - Theme.spacingXL
height: root.compact ? root.paneHeight : root.columnHeight
visible: root.splitPanes
@@ -225,20 +227,22 @@ MediaChromeBase {
Column {
id: controls
visible: root.controlsColumn
y: root.headerHeight + (parent.height - root.headerHeight - height) / 2
y: root.compact ? parent.height - height : art.y + art.height - height
width: parent.width
spacing: Theme.spacingL
spacing: Theme.spacingS
Rectangle {
width: parent.width
height: metadata.implicitHeight + Theme.spacingL * 2
height: metadata.implicitHeight + Theme.spacingS * 2
radius: Theme.cornerRadiusL
color: DashMetrics.cardColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Column {
id: metadata
x: Theme.spacingL
y: Theme.spacingL
y: Theme.spacingS
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingXS
@@ -248,7 +252,7 @@ MediaChromeBase {
font: Qt.font({
family: titleText.font.family,
weight: titleText.font.weight,
pixelSize: Theme.fontSizeXXLarge
pixelSize: Theme.fontSizeDisplay
})
}
@@ -262,9 +266,9 @@ MediaChromeBase {
readonly property real fittedPixelSize: {
if (fullSizeTitle.advanceWidth <= width)
return Theme.fontSizeXXLarge;
const fitted = Math.floor(Theme.fontSizeXXLarge * width / fullSizeTitle.advanceWidth);
return Math.max(Theme.fontSizeXLarge, fitted);
return Theme.fontSizeDisplay;
const fitted = Math.floor(Theme.fontSizeDisplay * width / fullSizeTitle.advanceWidth);
return Math.max(Theme.fontSizeXXLarge, fitted);
}
width: parent.width
@@ -299,7 +303,8 @@ MediaChromeBase {
height: playback.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadiusL
color: DashMetrics.cardColor
visible: root.hasSeekbar || transport.hasControls
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Column {
id: playback
@@ -308,6 +313,20 @@ MediaChromeBase {
width: parent.width - Theme.spacingL * 2
spacing: Theme.spacingS
MediaSourceGroup {
id: sourceGroup
width: parent.width
player: root.player
}
MediaTransport {
id: transport
width: parent.width
player: root.player
presentation: root.presentation
visible: hasControls
}
Item {
id: seekBlock
width: parent.width
@@ -316,16 +335,12 @@ MediaChromeBase {
LayoutMirroring.enabled: false
LayoutMirroring.childrenInherit: true
NumericText {
TimeLabel {
id: elapsed
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
width: Math.ceil(reservedWidth)
text: Format.formatDuration(seekbar.value * (root.presentation?.length ?? 0))
reserveText: duration.text.replace(/\d/g, "8")
isMonospace: false
color: Theme.onSurfaceVariant
font.pixelSize: Theme.fontSizeSmall
}
MediaSeekbar {
@@ -337,41 +352,22 @@ MediaChromeBase {
player: root.player
}
NumericText {
TimeLabel {
id: duration
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
width: Math.ceil(reservedWidth)
text: Format.formatDuration(root.presentation?.length ?? 0)
reserveText: text.replace(/\d/g, "8")
isMonospace: false
color: Theme.onSurfaceVariant
font.pixelSize: Theme.fontSizeSmall
}
}
MediaTransport {
id: transport
anchors.horizontalCenter: parent.horizontalCenter
width: Math.min(parent.width, implicitWidth)
player: root.player
presentation: root.presentation
visible: hasControls
}
}
}
MediaSourceGroup {
id: sourceGroup
width: parent.width
player: root.player
}
}
Item {
id: sourceRow
y: Math.round((controls.y - height) / 2)
y: root.compact ? Math.round((controls.y - height) / 2) : art.y
width: parent.width
height: Theme.buttonHeightXS
@@ -409,4 +405,12 @@ MediaChromeBase {
}
}
}
component TimeLabel: NumericText {
width: Math.ceil(reservedWidth)
isMonospace: false
color: Theme.onSurfaceVariant
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
}
}
@@ -6,6 +6,7 @@ import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
import qs.Modules.DankDash.Media
import "../../../DankCommon/Common/FocusNavigation.js" as FocusNavigation
Card {
@@ -25,7 +26,6 @@ Card {
readonly property string artUrl: TrackArtService.resolvedArtUrl
readonly property color accent: MediaAccentService.accent
readonly property bool showSeekbar: options.seekbar !== false && !tiny
readonly property bool circleArt: options.artStyle === "circle"
readonly property bool zurvan: options.playerStyle === "zurvan"
readonly property bool inlineMetadata: compact && titleLabel.implicitHeight + artistLabel.implicitHeight + trackText.spacing > transport.y - Theme.spacingXS
readonly property var playbackFocusTargets: [playButton, previousButton, nextButton].concat(seekbar.canSeek ? [seekbar] : [])
@@ -92,15 +92,20 @@ Card {
visible: !!root.activePlayer
enabled: root.interactive
MediaArtwork {
MediaArtSurface {
id: artwork
anchors.right: parent.right
anchors.top: parent.top
width: root.showArtwork ? Math.max(0, Math.min(DashMetrics.overviewArtHero, parent.width / 3, seekBlock.y - Theme.spacingM)) : 0
height: width
cornerRadius: root.circleArt ? width / 2 : DashMetrics.mediaArtRadius
artUrl: root.artUrl
artStyle: root.options.artStyle ?? "rounded"
visible: root.showArtwork
MediaArtwork {
anchors.fill: parent
cornerRadius: 0
artUrl: root.artUrl
}
}
Column {
@@ -188,6 +193,8 @@ Card {
round: false
checkable: true
checked: root.playing
iconFilled: false
radius: Theme.buttonRadius(width, height, buttonSize, pressed, !root.zurvan && checked)
iconName: root.playing ? "pause" : "play_arrow"
Accessible.name: root.playing ? I18n.tr("Pause") : I18n.tr("Play")
enabled: !!root.activePlayer?.canTogglePlaying
@@ -204,6 +211,7 @@ Card {
width: transport.stacked ? (parent.width - transport.spacing) / 2 : (parent.width - playButton.width - transport.spacing * 2) / 2
variant: root.zurvan ? "tonal" : "standard"
round: !root.zurvan
radius: Theme.buttonRadius(width, height, buttonSize, pressed, round)
containerColor: MediaAccentService.accentSecondaryContainer
contentColor: root.zurvan ? MediaAccentService.onAccentSecondaryContainer : Theme.onSecondaryContainer
iconName: "skip_previous"
@@ -221,6 +229,7 @@ Card {
width: previousButton.width
variant: previousButton.variant
round: previousButton.round
radius: Theme.buttonRadius(width, height, buttonSize, pressed, round)
containerColor: previousButton.containerColor
contentColor: previousButton.contentColor
iconName: "skip_next"
@@ -662,6 +662,8 @@ Item {
anchors.fill: parent
anchors.margins: Theme.spacingXS
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(root))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: DashMetrics.wallpaperThumbRadius
ClippingRectangle {
@@ -953,6 +955,8 @@ Item {
width: DashMetrics.pageJumpWidth
height: jumpColumn.height + Theme.spacingM * 2
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.cornerRadiusM
visible: false
z: DashMetrics.overlayZ + 1
@@ -11,6 +11,8 @@ Rectangle {
radius: Theme.cornerRadiusXL
color: DashMetrics.cardColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
LayoutMirroring.enabled: false
LayoutMirroring.childrenInherit: true
@@ -117,6 +117,8 @@ Rectangle {
radius: graphics && reading.shape ? Theme.fullRadius(width, height) : Theme.cornerRadiusXL
color: graphics && widgetId === "uv" ? "transparent" : DashMetrics.cardColor
border.width: graphics && widgetId === "uv" ? 0 : Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Accessible.role: Accessible.StaticText
Accessible.name: reading.label + " " + (hasValue ? reading.value : I18n.tr("Not available")) + " " + (reading.detail ?? "")
Accessible.description: graphicDescription
@@ -40,6 +40,8 @@ Rectangle {
implicitHeight: DashMetrics.gridRowUnit * 2 + DashMetrics.gridGap
radius: Theme.cornerRadiusXL
color: DashMetrics.cardColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Row {
anchors.left: parent.left
+2 -2
View File
@@ -914,8 +914,8 @@ Item {
clip: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.cardSurface, 0.9)
border.color: inputField.activeFocus ? Theme.primary : Qt.rgba(1, 1, 1, 0.3)
border.width: inputField.activeFocus ? 2 : 1
border.color: inputField.activeFocus ? Theme.primary : Theme.outlineMedium
border.width: inputField.activeFocus ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
GreeterUserPicker {
id: userPicker
@@ -623,8 +623,8 @@ Item {
Layout.preferredHeight: LockMetrics.fieldHeight
radius: Theme.fullRadius(width, height)
color: Theme.cardSurface
border.width: passwordField.activeFocus ? Math.max(Theme.outlineWidth, Theme.focusRingWidth) : Theme.outlineWidth
border.color: passwordField.activeFocus ? Theme.focusRingColor : Theme.outlineVariant
border.width: passwordField.activeFocus ? Math.max(Theme.outlineWidth, Theme.focusRingWidth) : Theme.layerOutlineWidth
border.color: passwordField.activeFocus ? Theme.focusRingColor : Theme.outlineMedium
Accessible.name: I18n.tr("Password")
visible: SettingsData.lockScreenShowPasswordField || root.passwordBuffer.length > 0
@@ -92,7 +92,7 @@ Item {
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency)
border.color: Theme.outlineMedium
border.width: 1
border.width: Theme.layerOutlineWidth
z: 100
Rectangle {
@@ -365,6 +365,8 @@ Column {
visible: searchVisible
opacity: searchVisible ? 1 : 0
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.cornerRadius
Behavior on opacity {
@@ -486,7 +488,7 @@ Column {
height: parent.height - bottomControls.height - Theme.spacingM - (searchVisible ? searchBar.height + Theme.spacingM : 0)
color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency)
border.color: Theme.outlineMedium
border.width: 1
border.width: Theme.layerOutlineWidth
radius: Theme.cornerRadius
RowLayout {
@@ -720,6 +722,8 @@ Column {
anchors.right: parent.right
height: 36
color: Theme.withAlpha(Theme.cardSurface, Theme.notepadTransparency)
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
z: 2
Row {
@@ -930,7 +934,7 @@ Column {
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineMedium
border.width: 1
border.width: Theme.layerOutlineWidth
z: 10
Row {
@@ -74,6 +74,8 @@ Rectangle {
bottomLeftRadius: lastInGroup ? outerRadius : radius
bottomRightRadius: bottomLeftRadius
color: surfaceColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
clip: true
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
@@ -294,6 +294,7 @@ DankOSD {
round: false
checkable: true
checked: root.playing
iconFilled: false
iconName: root._displayIcon
containerColor: MediaAccentService.accentContainer
contentColor: MediaAccentService.onAccentContainer
@@ -360,6 +361,7 @@ DankOSD {
round: false
checkable: true
checked: root.playing
iconFilled: false
backgroundColor: "transparent"
iconColor: MediaAccentService.onAccent
iconName: root._displayIcon
@@ -23,6 +23,8 @@ Item {
Layout.preferredHeight: 80
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
RowLayout {
anchors.fill: parent
@@ -100,6 +102,8 @@ Item {
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
ColumnLayout {
anchors.fill: parent
@@ -23,6 +23,8 @@ Rectangle {
implicitHeight: compact ? cardContent.implicitHeight + Theme.spacingM * 2 : 0
radius: Theme.cornerRadiusL
color: Theme.foregroundColor(Theme.cardSurface, Theme.isFloatingWindow(card))
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Connections {
target: DgopService
@@ -201,6 +201,8 @@ Popup {
background: Rectangle {
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.windowRadius
}
@@ -22,6 +22,8 @@ Item {
Layout.preferredHeight: systemInfoColumn.implicitHeight + Theme.spacingM * 2
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
ColumnLayout {
id: systemInfoColumn
@@ -95,6 +97,8 @@ Item {
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.nestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
ColumnLayout {
anchors.fill: parent
+13 -23
View File
@@ -56,29 +56,19 @@ Item {
subtitle: I18n.tr("The Island holds this edge on a display this bar covers, so the bar stays hidden there")
}
SettingsRow {
body: Item {
width: parent.width
height: positionButtonGroup.height
DankButtonGroup {
id: positionButtonGroup
anchors.horizontalCenter: parent.horizontalCenter
model: bar.positionChoices.map(pos => bar.positionLabel(pos))
currentIndex: {
bar.selectedBarId;
const config = SettingsData.getBarConfig(bar.selectedBarId);
return bar.positionChoices.indexOf(config?.position ?? SettingsData.Position.Top);
}
onSelectionChanged: (index, selected) => {
if (!selected || index < 0 || index >= bar.positionChoices.length)
return;
SettingsData.updateBarConfig(bar.selectedBarId, {
position: bar.positionChoices[index]
});
bar.notifyHorizontalBarChange();
}
}
SettingsLayoutPicker {
edgePlacement: true
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
key: String(position),
label: bar.positionLabel(position),
enabled: bar.positionChoices.includes(position)
}))
selectedKey: String(bar.selectedBarConfig?.position ?? SettingsData.Position.Top)
onSelected: key => {
SettingsData.updateBarConfig(bar.selectedBarId, {
position: Number(key)
});
bar.notifyHorizontalBarChange();
}
}
@@ -42,8 +42,8 @@ WidgetPickerWindow {
radius: Theme.cornerRadius
property bool isSelected: root.keyboardNavigationActive && index === root.selectedIndex
color: isSelected ? Theme.primarySelected : widgetArea.containsMouse ? Theme.primaryHover : Theme.floatingWindowNestedSurface
border.color: isSelected ? Theme.primary : Theme.withAlpha(Theme.outline, 0.2)
border.width: isSelected ? Theme.outlineWidthFocused : Theme.outlineWidth
border.color: isSelected ? Theme.primary : Theme.outlineMedium
border.width: isSelected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
Row {
anchors.fill: parent
@@ -164,8 +164,8 @@ DankModal {
height: parent.height - 120
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineStrong
border.width: 1
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
clip: true
DankFlickable {
@@ -142,7 +142,7 @@ Column {
tags: ["dock", "layout", "placement", "position", "edge"]
SettingsLayoutPicker {
dockPlacement: true
edgePlacement: true
choices: [SettingsData.Position.Top, SettingsData.Position.Bottom, SettingsData.Position.Left, SettingsData.Position.Right].map(position => ({
key: String(position),
label: dock.positionLabel(position),
@@ -97,8 +97,8 @@ Item {
height: 56
radius: Theme.cornerRadius
color: modemMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface
border.width: isConnected ? Theme.outlineWidthFocused : 0
border.color: Theme.primary
border.width: isConnected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: isConnected ? Theme.primary : Theme.outlineMedium
Row {
anchors.left: parent.left
@@ -232,8 +232,8 @@ Item {
height: 56
radius: Theme.cornerRadius
color: profileMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface
border.color: isActive ? Theme.primary : Theme.outlineLight
border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth
border.color: isActive ? Theme.primary : Theme.outlineMedium
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
Row {
anchors.left: parent.left
@@ -85,8 +85,8 @@ Item {
height: isExpanded ? 56 + ethExpandedContent.height : 56
radius: Theme.cornerRadius
color: ethDeviceMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface
border.width: isConnected ? Theme.outlineWidthFocused : 0
border.color: Theme.primary
border.width: isConnected ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: isConnected ? Theme.primary : Theme.outlineMedium
clip: true
Behavior on height {
@@ -381,8 +381,8 @@ Item {
height: 48
radius: Theme.cornerRadius
color: wiredMouseArea.containsMouse ? Theme.primaryHoverLight : Theme.floatingWindowNestedSurface
border.width: modelData.isActive ? Theme.outlineWidthFocused : 0
border.color: Theme.primary
border.width: modelData.isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: modelData.isActive ? Theme.primary : Theme.outlineMedium
Row {
anchors.left: parent.left
@@ -231,8 +231,8 @@ Item {
height: isExpanded ? 56 + vpnExpandedContent.height : 56
radius: Theme.cornerRadius
color: vpnRowArea.containsMouse ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.floatingWindowNestedSurface)
border.width: isActive ? Theme.outlineWidthFocused : 0
border.color: Theme.primary
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: isActive ? Theme.primary : Theme.outlineMedium
opacity: DMSNetworkService.isBusy ? 0.6 : 1.0
clip: true
@@ -966,8 +966,8 @@ RegistryBrowserWindow {
height: Math.round(width * SettingsMetrics.choiceCardPreviewRatio)
radius: Theme.cornerRadiusM
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineVariant
border.width: Theme.outlineWidth
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
ClippingRectangle {
anchors.fill: parent
+2 -2
View File
@@ -27,8 +27,8 @@ DankCard {
implicitHeight: previewHeight + infoHeight + Theme.spacingS * 2 + Theme.spacingM
radius: Theme.cornerRadiusM
color: Theme.floatingWindowNestedSurface
border.color: Theme.focusRingColor
border.width: selected ? Theme.focusRingWidth : 0
border.color: selected ? Theme.focusRingColor : Theme.outlineMedium
border.width: selected ? Theme.focusRingWidth : Theme.layerOutlineWidth
pad: 0
clickable: true
Accessible.name: plugin.name || ""
@@ -21,8 +21,8 @@ StyledRect {
height: visible ? innerColumn.implicitHeight + Theme.spacingL * 2 : 0
radius: Theme.cornerRadiusM
color: Theme.floatingWindowNestedSurface
border.color: Theme.outlineVariant
border.width: Theme.outlineWidth
border.color: Theme.outlineMedium
border.width: Theme.layerOutlineWidth
clip: true
visible: false
@@ -195,6 +195,8 @@ Item {
height: 90
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
ClippingRectangle {
anchors.fill: parent
@@ -523,8 +525,8 @@ Item {
height: themeGrid.cardHeight
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.color: isActive ? Theme.primary : Theme.outline
border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth
border.color: isActive ? Theme.primary : Theme.outlineMedium
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
scale: isActive ? 1.03 : 1
Behavior on scale {
@@ -178,8 +178,9 @@ Column {
tags: ["foreground", "layers", "outline", "border", "cards", "pills", "widgets", "notifications", "control center"]
settingKey: "blurLayerOutlineOpacity"
text: I18n.tr("Layer outline opacity")
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0.12) * 100)
value: Math.round((SettingsData.blurLayerOutlineOpacity ?? 0) * 100)
minimum: 0
minimumLabel: I18n.tr("Off")
maximum: 40
onSliderValueChanged: newValue => SettingsData.set("blurLayerOutlineOpacity", newValue / 100)
}
@@ -114,6 +114,8 @@ Item {
bottomLeftRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
bottomRightRadius: last ? Theme.groupedListOuterRadius : Theme.groupedListInnerRadius
color: root.highlighted ? Theme.blend(root.slotColor, Theme.primary, SettingsMetrics.highlightBlend) : root.slotColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
@@ -1,20 +1,21 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Modules.DankBar
import qs.Widgets
Row {
GridLayout {
id: root
property bool dockPlacement: false
property bool edgePlacement: false
property bool widgetStyle: false
property var choices: barModes
property string selectedKey: activeBarMode
signal selected(string key)
onSelected: key => {
if (!dockPlacement && !widgetStyle)
if (!edgePlacement && !widgetStyle)
applyBarMode(key);
}
@@ -37,6 +38,8 @@ Row {
readonly property real previewAspect: 0.62
readonly property real previewStripRatio: 0.55
readonly property real previewIslandRatio: 0.42
readonly property real previewWidth: Math.round(Theme.iconSize * previewWidthRatio)
readonly property real minimumCardWidth: previewWidth + Theme.spacingL * 2
readonly property var targetConfig: {
SettingsData.barConfigs;
SettingsUiState.selectedBarId;
@@ -70,7 +73,13 @@ Row {
}
width: parent?.width ?? 0
spacing: Theme.spacingS
columns: {
const count = Math.max(1, root.choices.length);
const availableColumns = Math.max(1, Math.floor((width + columnSpacing) / (minimumCardWidth + columnSpacing)));
return Math.ceil(count / Math.ceil(count / availableColumns));
}
columnSpacing: Theme.spacingS
rowSpacing: Theme.spacingS
Repeater {
model: root.choices
@@ -83,11 +92,14 @@ Row {
readonly property bool isActive: root.selectedKey === modelData.key
width: (root.width - root.spacing * (root.choices.length - 1)) / Math.max(1, root.choices.length)
height: Math.round(Theme.fontSizeMedium * root.cardHeightRatio)
Layout.fillWidth: true
Layout.fillHeight: true
Layout.minimumWidth: 0
implicitWidth: root.minimumCardWidth
implicitHeight: Math.max(Math.round(Theme.fontSizeMedium * root.cardHeightRatio), cardContent.implicitHeight + Theme.spacingM * 2)
radius: Theme.cornerRadius
color: Theme.floatingWindowNestedSurface
border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: isActive ? Theme.primary : Theme.outlineMedium
activeFocusOnTab: true
@@ -108,7 +120,10 @@ Row {
}
Column {
id: cardContent
anchors.centerIn: parent
width: Math.max(0, parent.width - Theme.spacingM * 2)
spacing: Theme.spacingS
Rectangle {
@@ -116,7 +131,7 @@ Row {
readonly property real edgePad: Math.max(2, Math.round(width * 0.045))
readonly property real stripSize: Math.round(width * 0.11)
width: Math.round(Theme.iconSize * root.previewWidthRatio)
width: Math.min(root.previewWidth, cardContent.width)
height: Math.round(width * root.previewAspect)
radius: Theme.spacingXS
color: Theme.chipSurface
@@ -148,7 +163,7 @@ Row {
Rectangle {
readonly property int edge: Number(modeCard.modelData.key)
readonly property bool vertical: edge === SettingsData.Position.Left || edge === SettingsData.Position.Right
visible: root.dockPlacement
visible: root.edgePlacement
x: edge === SettingsData.Position.Right ? parent.width - width - screenPreview.edgePad : screenPreview.edgePad
y: edge === SettingsData.Position.Bottom ? parent.height - height - screenPreview.edgePad : screenPreview.edgePad
width: vertical ? screenPreview.stripSize : parent.width - screenPreview.edgePad * 2
@@ -158,7 +173,7 @@ Row {
}
Rectangle {
visible: !root.widgetStyle && !root.dockPlacement && modeCard.modelData.key === "standard"
visible: !root.widgetStyle && !root.edgePlacement && modeCard.modelData.key === "standard"
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
@@ -202,11 +217,13 @@ Row {
}
StyledText {
width: parent.width
text: modeCard.modelData.label
font.pixelSize: Theme.fontSizeSmall
font.weight: Theme.fontWeightMedium
color: modeCard.isActive ? Theme.primary : Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.Wrap
}
}
@@ -116,6 +116,8 @@ T.Control {
anchors.fill: parent
visible: root.paintBackground
color: root.isHighlighted ? Theme.blend(root.rowColor, Theme.primary, SettingsMetrics.highlightBlend) : root.rowColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
topLeftRadius: root.topRadius
topRightRadius: root.topRadius
bottomLeftRadius: root.bottomRadius
@@ -46,6 +46,8 @@ Rectangle {
bottomLeftRadius: bottomRadius
bottomRightRadius: bottomRadius
color: active ? SettingsMetrics.selectedRowColor : SettingsMetrics.rowColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
@@ -46,6 +46,8 @@ Item {
Layer {
label: I18n.tr("Cards")
color: Theme.cardSurface
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
radius: Theme.cornerRadiusM
Layer {
@@ -37,7 +37,7 @@ Flow {
height: root.compact ? root.compactTileHeight : swatch.height + label.implicitHeight + Theme.spacingS * 3
radius: Theme.cornerRadiusM
color: Theme.floatingWindowNestedSurface
border.width: isActive ? Theme.outlineWidthFocused : Theme.outlineWidth
border.width: isActive ? Theme.outlineWidthFocused : Theme.layerOutlineWidth
border.color: isActive ? Theme.primary : Theme.outlineMedium
activeFocusOnTab: true
@@ -37,6 +37,8 @@ Item {
anchors.fill: parent
radius: Theme.groupedListOuterRadius
color: SettingsMetrics.rowColor
border.width: Theme.layerOutlineWidth
border.color: Theme.outlineMedium
visible: root.standalone
}
+65
View File
@@ -0,0 +1,65 @@
.pragma library
function create() {
return {
paths: Object.create(null),
scores: Object.create(null),
directories: Object.create(null)
};
}
function add(index, path, searchDirs, looseDirs) {
const separator = path.lastIndexOf("/") + 1;
const fileName = path.substring(separator);
const name = fileName.replace(/\.(svg|png|xpm)$/, "");
if (!/^[\w.+-]+$/.test(name))
return;
const directory = path.substring(0, separator);
let directoryScore = index.directories[directory];
if (directoryScore === undefined) {
directoryScore = score(directory, searchDirs, looseDirs);
index.directories[directory] = directoryScore;
}
const candidateScore = directoryScore + (fileName.endsWith(".svg") ? 100000 : 0);
const currentScore = index.scores[name];
if (currentScore !== undefined && candidateScore <= currentScore)
return;
index.paths[name] = path;
index.scores[name] = candidateScore;
}
function chainIndex(path, searchDirs, looseDirs) {
for (let i = 0; i < searchDirs.length; i++) {
if (path.startsWith(searchDirs[i] + "/"))
return i;
}
for (let i = 0; i < looseDirs.length; i++) {
if (path.startsWith(looseDirs[i] + "/"))
return searchDirs.length + i;
}
return searchDirs.length + looseDirs.length;
}
function score(path, searchDirs, looseDirs) {
let s = 0;
if (path.includes("/apps/"))
s += 3000000000;
else if (path.includes("/categories/"))
s += 1000000000;
else if (path.includes("/places/") || path.includes("/devices/") || path.includes("/mimetypes/") || path.includes("/status/") || path.includes("/actions/"))
s += 100000000;
s += Math.min(99, Math.max(0, searchDirs.length + looseDirs.length - chainIndex(path, searchDirs, looseDirs))) * 1000000;
if (path.endsWith(".svg"))
s += 100000;
if (path.includes("/scalable/")) {
s += 1000;
} else {
const m = path.match(/\/(\d+)(?:x\d+)?(?:@\d+x)?\//);
if (m)
s += Math.min(parseInt(m[1]), 999);
}
return s;
}
+127 -172
View File
@@ -4,9 +4,11 @@ pragma ComponentBehavior: Bound
import QtCore
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
import qs.Services
import "../Common/GSettings.js" as GSettings // ! This import is actually used even though qmlls claims it isnt, do not delete
import "../Common/GSettings.js" as GSettings // qmllint disable unused-imports
import "IconThemeIndex.js" as IconIndex
Singleton {
id: root
@@ -15,23 +17,18 @@ Singleton {
readonly property string settingsTheme: {
if (typeof SettingsData === "undefined")
return "";
const t = SettingsData.resolveIconTheme();
return (!t || t === "System Default") ? "" : t;
const theme = SettingsData.resolveIconTheme();
return (!theme || theme === "System Default") ? "" : theme;
}
property string systemProbedTheme: ""
readonly property string managedTheme: settingsTheme || systemProbedTheme
property bool ready: false
property var _iconPaths: Object.create(null)
property bool _initialized: false
property bool _probing: false
property bool _building: false
property bool _rebuildPending: false
property var _searchDirs: []
property string _dirsForTheme: ""
property var _cache: ({})
property var _looseIconPaths: ({})
property int _cacheGeneration: 0
property int _rebuildGeneration: 0
property int _looseIndexGeneration: 0
property int revision: 0
property bool _bumpPending: false
// XDG icon spec order: user dirs win over system ones, so overrides in ~/.local/share/icons beat installed themes.
readonly property var _userIconRoots: {
const localData = Paths.strip(StandardPaths.writableLocation(StandardPaths.GenericDataLocation));
const home = Paths.strip(StandardPaths.writableLocation(StandardPaths.HomeLocation));
@@ -59,101 +56,63 @@ Singleton {
return bases;
}
// Loose icon files (AppImage managers, legacy /usr/share/pixmaps installs) live outside any theme.
readonly property var _looseDirs: {
const dirs = _userIconRoots.concat(_dataDirs.map(d => d + "/pixmaps"));
return dirs.filter((dir, index) => dirs.indexOf(dir) === index);
}
onManagedThemeChanged: _rebuild()
onSettingsThemeChanged: {
if (_initialized && !settingsTheme)
_probeSystemTheme();
}
onManagedThemeChanged: Qt.callLater(_rebuild)
Component.onCompleted: {
_initialized = true;
Paths.iconResolver = name => resolve(name);
_probeSystemTheme();
_rebuild();
Qt.callLater(_rebuild);
}
Connections {
target: DesktopEntries
function onApplicationsChanged() {
root._indexLooseIcons(() => root._invalidateMisses());
if (!root.ready)
return;
Qt.callLater(root._rebuild);
}
}
// "System Default" leaves Qt's lookup, which sees only hicolor unless a Qt platform theme or QS_ICON_THEME is configured.
function _probeSystemTheme() {
if (Quickshell.env("QS_ICON_THEME"))
if (Quickshell.env("QS_ICON_THEME") || _probing)
return;
_probing = true;
const script = `v=$(sed -n 's/^gtk-icon-theme-name *= *//p' "\${XDG_CONFIG_HOME:-$HOME/.config}/gtk-3.0/settings.ini" 2>/dev/null | head -1)
[ -z "$v" ] && v=$(${GSettings.getCmd("org.gnome.desktop.interface", "icon-theme")})
printf '%s' "$v" | tr -d "'\\""`;
Proc.runCommand("iconThemeProbe", ["sh", "-c", script], (out, code) => {
const theme = (out || "").trim();
if (!theme)
return;
root.systemProbedTheme = theme;
});
}
function _bumpRevision() {
if (_bumpPending)
return;
_bumpPending = true;
Qt.callLater(() => {
_bumpPending = false;
revision++;
});
}
function _invalidateMisses() {
const c = {};
let changed = false;
for (const name in _cache) {
if (_cache[name])
c[name] = _cache[name];
else
changed = true;
}
if (!changed)
return;
_cacheGeneration++;
_cache = c;
_bumpRevision();
}
function _indexLooseIcons(callback) {
const generation = ++_looseIndexGeneration;
const args = ["find", "-L"].concat(_looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"]);
Proc.runCommand("iconLooseIndex", args, (out, code) => {
if (root._looseIndexGeneration !== generation)
return;
const index = {};
const paths = (out || "").trim().split("\n").filter(s => s);
for (const path of paths) {
const fileName = path.substring(path.lastIndexOf("/") + 1);
const name = fileName.replace(/\.(svg|png|xpm)$/, "");
const current = index[name];
index[name] = current ? root._pickBest([current, path]) : path;
}
root._looseIconPaths = index;
if (callback)
callback();
Proc.runCommand(null, ["sh", "-c", script], (out, code) => {
root.systemProbedTheme = (out || "").trim();
root._probing = false;
Qt.callLater(root._rebuild);
}, 0);
}
function _rebuild() {
const rebuildGeneration = ++_rebuildGeneration;
_cacheGeneration++;
_cache = ({});
_dirsForTheme = "";
_indexLooseIcons(() => root._invalidateMisses());
if (!managedTheme) {
_searchDirs = [];
_bumpRevision();
if (!_initialized || _probing)
return;
if (_building) {
_rebuildPending = true;
return;
}
_building = true;
_rebuildPending = false;
const theme = managedTheme;
if (!theme) {
_publishIndex(theme, Object.create(null));
return;
}
const script = `theme=$1
shift
find_index() { target=$1; shift; for b; do [ -f "$b/$target/index.theme" ] && { echo "$b/$target/index.theme"; return 0; }; done; return 1; }
@@ -170,105 +129,101 @@ done
case " $visited " in *" hicolor "*) ;; *) order="$order hicolor";; esac
for t in $order; do for b; do d="$b/$t"; [ -d "$d" ] && echo "$d"; done; done`;
Proc.runCommand("iconChain:" + theme, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => {
if (root.managedTheme !== theme || root._rebuildGeneration !== rebuildGeneration)
return;
root._searchDirs = (out || "").trim().split("\n").filter(s => s);
root._dirsForTheme = theme;
root._cacheGeneration++;
root._cache = ({});
root._bumpRevision();
});
}
function resolve(name) {
const _dep = revision;
if (!managedTheme || !name)
return "";
if (name.startsWith("/") || name.startsWith("file://") || name.startsWith("image://") || name.startsWith("~"))
return "";
if (!/^[\w.+-]+$/.test(name))
return "";
if (_dirsForTheme !== managedTheme)
return "";
if (name in _cache)
return _cache[name] || "";
_cache[name] = null;
_resolveAsync(name);
return "";
}
function _resolveAsync(name) {
const generation = _cacheGeneration;
function finish(paths) {
if (root._cacheGeneration !== generation)
return;
const best = paths.length > 0 ? root._pickBest(paths) : root._looseIconPaths[name] || "";
const c = root._cache;
c[name] = best ? Paths.toFileUrl(best) : "";
root._cache = c;
root._bumpRevision();
}
if (_searchDirs.length === 0) {
finish([]);
return;
}
const args = ["find", "-L"].concat(_searchDirs, ["(", "-name", name + ".svg", "-o", "-name", name + ".png", ")"]);
Proc.runCommand("iconResolveTheme:" + name, args, (out, code) => {
const paths = (out || "").trim().split("\n").filter(s => s);
finish(paths);
Proc.runCommand(null, ["sh", "-c", script, "icon-chain", theme].concat(_baseDirs), (out, code) => {
const dirs = (out || "").trim().split("\n").filter(dir => dir);
root._indexTheme(theme, dirs);
}, 0);
}
function _pickBest(paths) {
let best = "";
let bestScore = -1;
for (let i = 0; i < paths.length; i++) {
const s = _score(paths[i]);
if (s > bestScore) {
bestScore = s;
best = paths[i];
function _indexTheme(theme, dirs) {
indexJob.createObject(root, {
theme: theme,
searchDirs: dirs,
looseDirs: _looseDirs
});
}
function _publishIndex(theme, paths) {
_building = false;
if (managedTheme === theme && !_probing) {
_iconPaths = paths;
ready = true;
}
if (_rebuildPending)
Qt.callLater(_rebuild);
}
Component {
id: indexJob
Scope {
id: job
required property string theme
required property var searchDirs
required property var looseDirs
property var themeIndex: IconIndex.create()
property var looseIndex: IconIndex.create()
property int remaining: searchDirs.length ? 2 : 1
property bool finished: false
function complete() {
if (--remaining > 0)
return;
publish();
}
function publish() {
if (finished)
return;
finished = true;
for (const name in looseIndex.paths) {
if (!themeIndex.paths[name])
themeIndex.paths[name] = looseIndex.paths[name];
}
root._publishIndex(theme, themeIndex.paths);
destroy();
}
Process {
command: ["find", "-L"].concat(job.searchDirs, ["(", "-name", "*.svg", "-o", "-name", "*.png", ")"])
running: job.searchDirs.length > 0
stdout: SplitParser {
onRead: data => {
if (!job.finished)
IconIndex.add(job.themeIndex, data, job.searchDirs, job.looseDirs);
}
}
onExited: job.complete()
}
Process {
command: ["find", "-L"].concat(job.looseDirs, ["-maxdepth", "1", "(", "-name", "*.svg", "-o", "-name", "*.png", "-o", "-name", "*.xpm", ")"])
running: true
stdout: SplitParser {
onRead: data => {
if (!job.finished)
IconIndex.add(job.looseIndex, data, job.searchDirs, job.looseDirs);
}
}
onExited: job.complete()
}
Timer {
interval: 10000
running: true
onTriggered: {
root.log.warn("Icon theme index timed out:", job.theme);
job.publish();
}
}
}
return best;
}
function _chainIndex(path) {
for (let i = 0; i < _searchDirs.length; i++) {
if (path.startsWith(_searchDirs[i] + "/"))
return i;
}
for (let i = 0; i < _looseDirs.length; i++) {
if (path.startsWith(_looseDirs[i] + "/"))
return _searchDirs.length + i;
}
return _searchDirs.length + _looseDirs.length;
}
function _score(path) {
let s = 0;
if (path.includes("/apps/"))
s += 3000000000;
else if (path.includes("/categories/"))
s += 1000000000;
else if (path.includes("/places/") || path.includes("/devices/") || path.includes("/mimetypes/") || path.includes("/status/") || path.includes("/actions/"))
s += 100000000;
s += Math.min(99, Math.max(0, _searchDirs.length + _looseDirs.length - _chainIndex(path))) * 1000000;
if (path.endsWith(".svg"))
s += 100000;
if (path.includes("/scalable/")) {
s += 1000;
} else {
const m = path.match(/\/(\d+)(?:x\d+)?(?:@\d+x)?\//);
if (m)
s += Math.min(parseInt(m[1]), 999);
}
return s;
function resolve(name) {
if (!name || !/^[\w.+-]+$/.test(name))
return "";
const path = _iconPaths[name];
return path ? Paths.toFileUrl(path) : "";
}
}
+2 -2
View File
@@ -58,7 +58,7 @@ ShellRoot {
Loader {
id: shellCoreLoader
active: !entrypoint.runGreeter
active: !entrypoint.runGreeter && IconThemeService.ready
asynchronous: true
source: "ShellCore.qml"
onLoaded: dmsShellLoader.setSource("DMSShell.qml", {
@@ -73,7 +73,7 @@ ShellRoot {
Loader {
id: dmsGreeterLoader
active: entrypoint.runGreeter
active: entrypoint.runGreeter && IconThemeService.ready
asynchronous: false
source: "DMSGreeter.qml"
}
@@ -0,0 +1,60 @@
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import vm from "node:vm";
import test from "node:test";
const index = vm.createContext({});
vm.runInContext(readFileSync(new URL("../Services/IconThemeIndex.js", import.meta.url), "utf8").replace(/^\.pragma.*$/m, ""), index);
const searchDirs = ["/user/theme", "/system/theme", "/system/inherited", "/system/hicolor"];
const looseDirs = ["/user/icons", "/system/pixmaps"];
function build(paths) {
const result = index.create();
for (const path of paths)
index.add(result, path, searchDirs, looseDirs);
return result.paths;
}
test("index keeps the existing category, root, format, size and tie priorities", () => {
const cases = [
["/user/theme/actions/icon.svg", "/system/inherited/apps/icon.png"],
["/system/theme/apps/scalable/icon.svg", "/user/theme/apps/16/icon.png"],
["/user/theme/apps/512/icon.png", "/user/theme/apps/16/icon.svg"],
["/user/theme/apps/512/icon.svg", "/user/theme/apps/scalable/icon.svg"],
["/user/theme/apps/16x16/icon.png", "/user/theme/apps/48x48@2x/icon.png"],
["/system/pixmaps/icon.svg", "/user/icons/icon.png"]
];
for (const [loser, winner] of cases) {
assert.equal(build([loser, winner]).icon, winner);
assert.equal(build([winner, loser]).icon, winner);
}
const ties = ["/user/theme/apps/a/icon.svg", "/user/theme/apps/b/icon.svg"];
assert.equal(build(ties).icon, ties[0]);
});
test("index bounds retained paths by resolvable names and handles object property names", () => {
const paths = ["/user/theme/apps/icon.svg", "/user/theme/apps/16/icon.png", "/system/pixmaps/loose.xpm", "/user/theme/apps/__proto__.svg", "/user/theme/apps/constructor.png", "/user/theme/apps/not a name.svg"];
const result = build(paths);
assert.equal(Object.keys(result).length, 4);
assert.equal(result.__proto__, paths[3]);
assert.equal(result.constructor, paths[4]);
assert.equal(result.loose, paths[2]);
});
test("first lookup uses the completed index without spawning or retaining misses", () => {
const source = readFileSync(process.env.ICON_SERVICE_SOURCE || new URL("../Services/IconThemeService.qml", import.meta.url), "utf8");
const resolve = source.match(/^ function resolve\(name\) \{[\s\S]*?^ \}/m)[0];
let spawns = 0;
const context = vm.createContext({
_iconPaths: build(["/user/theme/apps/icon.svg"]),
Paths: { toFileUrl: path => "file://" + path },
revision: 0, managedTheme: "theme", _dirsForTheme: "theme", _cache: {},
_resolveAsync: () => spawns++
});
vm.runInContext(resolve, context);
assert.equal(context.resolve("icon"), "file:///user/theme/apps/icon.svg");
for (let i = 0; i < 100; i++)
assert.equal(context.resolve("missing-" + i), "");
assert.equal(spawns, 0);
assert.equal(Object.keys(context._iconPaths).length, 1);
});
+14
View File
@@ -81,6 +81,12 @@ def format_js(qmlformat, file):
return result.stdout
def truncated(original, new_text):
"""qmlls and qmlformat can succeed and still hand back an empty or gutted
document; writing that back destroys the file and stages the wreckage."""
return not new_text.strip() or len(new_text) < len(original) // 2
def find_qmllint():
for candidate in QMLLINT_CANDIDATES:
path = candidate if "/" in candidate and Path(candidate).is_file() else shutil.which(candidate)
@@ -276,6 +282,10 @@ def format_js_files(qmlformat, root, files):
skipped += 1
print("skipped (qmlformat rejected the file)")
continue
if truncated(original, new_text):
skipped += 1
print("skipped (qmlformat returned a truncated document)")
continue
if new_text == original:
print("unchanged")
continue
@@ -377,6 +387,10 @@ def main():
client.notify("textDocument/didClose", {"textDocument": {"uri": uri}})
new_text = apply_edits(original, edits or [])
if truncated(original, new_text):
skipped += 1
print("skipped (qmlls returned a truncated document)")
continue
if new_text == original:
print("unchanged")
continue