mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-05 04:58:30 -04:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a7f767c2 | |||
| c9c11f8a27 | |||
| 8cffdab4d7 | |||
| 0e8a4c0cc1 | |||
| 1db6381c78 | |||
| d5c7efc861 | |||
| 943ffb432c | |||
| 7063448b80 | |||
| 52afbc9801 | |||
| 6edb985847 | |||
| 41efe6ad15 |
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
Lf, af, bf := cf.Lab()
|
||||
Lf *= 100.0
|
||||
|
||||
dir := 1.0
|
||||
if isLightMode {
|
||||
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
||||
fg := HexToRGB(hexColor)
|
||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||
origL, af, bf := cf.Lab()
|
||||
origL *= 100.0
|
||||
|
||||
var darkerResult, lighterResult string
|
||||
darkerL, lighterL := origL, origL
|
||||
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
|
||||
return result
|
||||
}
|
||||
|
||||
// color8 sits a fixed L* offset from the background so it keeps its dim role
|
||||
// regardless of primary brightness (conventional palettes put ANSI bright
|
||||
// black ~2-2.5:1 from the background, e.g. catppuccin-mocha #585b70 on #1e1e2e)
|
||||
func DeriveDim(bgHex string, hue, sat float64, isLight bool) string {
|
||||
offset := 22.0
|
||||
if isLight {
|
||||
offset = -offset
|
||||
}
|
||||
|
||||
bgL := getLstar(bgHex)
|
||||
targetL := math.Max(0, math.Min(100, bgL+offset))
|
||||
|
||||
tint := HSVToRGB(HSV{H: hue, S: sat, V: 0.5})
|
||||
c := colorful.Color{R: tint.R, G: tint.G, B: tint.B}
|
||||
_, af, bf := c.Lab()
|
||||
return labToHex(targetL, af, bf)
|
||||
}
|
||||
|
||||
func DeriveContainer(primary string, isLight bool) string {
|
||||
rgb := HexToRGB(primary)
|
||||
hsv := RGBToHSV(rgb)
|
||||
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
gray7V := baseVal * 0.28
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.05
|
||||
gray8V := baseVal * 0.85
|
||||
dimTarget := secondaryTarget * 0.5
|
||||
palette.Color8 = NewColorInfo(ensureContrastBidirectional(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, dimTarget, opts))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
|
||||
|
||||
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.2, 1.0)
|
||||
@@ -559,9 +576,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
||||
gray7V := math.Min(baseVal*1.05, 1.0)
|
||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||
|
||||
gray8S := baseSat * 0.15
|
||||
gray8V := baseVal * 0.65
|
||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
||||
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
|
||||
|
||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||
brightRedV := math.Min(baseVal*1.35, 1.0)
|
||||
|
||||
@@ -679,3 +679,73 @@ func TestContrastAlgorithmComparison(t *testing.T) {
|
||||
|
||||
t.Logf("WCAG and DPS palettes differ in %d/16 colors", differentCount)
|
||||
}
|
||||
|
||||
func TestEnsureContrastDPSLightModeStaysLight(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
result string
|
||||
bg string
|
||||
minLstar float64
|
||||
}{
|
||||
{
|
||||
name: "bidirectional adjustment",
|
||||
result: EnsureContrastDPSBidirectional("#d0ccc6", "#f8f8f8", 17.5, true),
|
||||
bg: "#f8f8f8",
|
||||
minLstar: 20.0,
|
||||
},
|
||||
{
|
||||
name: "lstar adjustment",
|
||||
result: EnsureContrastDPSLstar("#c0c0c0", "#f8f8f8", 30.0, true),
|
||||
bg: "#f8f8f8",
|
||||
minLstar: 20.0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
lstar := getLstar(tt.result)
|
||||
if lstar < tt.minLstar {
|
||||
t.Errorf("result %s has L* %.2f on light bg %s, expected >= %.2f (collapsed to near-black)",
|
||||
tt.result, lstar, tt.bg, tt.minLstar)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteColor8Dim(t *testing.T) {
|
||||
hues := []string{"#e91e63", "#f59e0b", "#22c55e", "#06b6d4", "#8b5cf6", "#ef4444"}
|
||||
|
||||
for _, base := range hues {
|
||||
t.Run(base, func(t *testing.T) {
|
||||
palette := GeneratePalette(base, PaletteOptions{IsLight: false, UseDPS: true})
|
||||
|
||||
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
|
||||
if bgRatio < 1.5 || bgRatio > 3.0 {
|
||||
t.Errorf("Color8 %s vs bg %s ratio %.2f, expected 1.5-3.0 (bright black stays near bg)",
|
||||
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
|
||||
}
|
||||
|
||||
sepRatio := ContrastRatio(palette.Color4.Hex, palette.Color8.Hex)
|
||||
if sepRatio < 2.0 {
|
||||
t.Errorf("Color4 %s vs Color8 %s ratio %.2f, expected >= 2.0 (blue must not collide with bright black)",
|
||||
palette.Color4.Hex, palette.Color8.Hex, sepRatio)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratePaletteLightColor8StaysLight(t *testing.T) {
|
||||
palette := GeneratePalette("#f59e0b", PaletteOptions{IsLight: true, UseDPS: true})
|
||||
|
||||
lstar := getLstar(palette.Color8.Hex)
|
||||
if lstar < 60.0 {
|
||||
t.Errorf("light mode Color8 %s has L* %.2f, expected >= 60 (dim grey, not near-black)",
|
||||
palette.Color8.Hex, lstar)
|
||||
}
|
||||
|
||||
bgRatio := ContrastRatio(palette.Color8.Hex, palette.Color0.Hex)
|
||||
if bgRatio > 3.0 {
|
||||
t.Errorf("light mode Color8 %s vs bg %s ratio %.2f, expected <= 3.0",
|
||||
palette.Color8.Hex, palette.Color0.Hex, bgRatio)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,11 @@ func NewGentooDistribution(config DistroConfig, logChan chan<- string) *GentooDi
|
||||
}
|
||||
}
|
||||
|
||||
func emergeInstallArgs(packages []string) []string {
|
||||
args := []string{"emerge", "--ask=n", "--quiet", "--autounmask-continue=y", "--autounmask-keep-keywords=y", "--autounmask-keep-masks=y"}
|
||||
return append(args, packages...)
|
||||
}
|
||||
|
||||
func (g *GentooDistribution) getArchKeyword() string {
|
||||
arch := runtime.GOARCH
|
||||
switch arch {
|
||||
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
}
|
||||
g.log("Portage tree synced successfully")
|
||||
|
||||
args := emergeInstallArgs(missingPkgs)
|
||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhasePrerequisites,
|
||||
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||
IsComplete: false,
|
||||
NeedsSudo: true,
|
||||
CommandInfo: fmt.Sprintf("sudo emerge --ask=n %s", strings.Join(missingPkgs, " ")),
|
||||
CommandInfo: fmt.Sprintf("sudo %s", strings.Join(args, " ")),
|
||||
LogOutput: fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")),
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, missingPkgs...)
|
||||
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
||||
}
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, packageNames...)
|
||||
args := emergeInstallArgs(packageNames)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseSystemPackages,
|
||||
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
||||
guruPackages[i] = pkg + "::guru"
|
||||
}
|
||||
|
||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
||||
args = append(args, guruPackages...)
|
||||
args := emergeInstallArgs(guruPackages)
|
||||
|
||||
progressChan <- InstallProgressMsg{
|
||||
Phase: PhaseAURPackages,
|
||||
|
||||
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
|
||||
return nil, fmt.Errorf("failed to find keyboards: %w", err)
|
||||
}
|
||||
|
||||
initialCapsLock := readInitialCapsLockState(devices[0])
|
||||
initialCapsLock, _ := capsLockFromDevices(devices)
|
||||
|
||||
watcher, err := fsnotify.NewWatcher()
|
||||
if err != nil {
|
||||
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func readInitialCapsLockState(device EvdevDevice) bool {
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Debugf("Could not read LED state: %v", err)
|
||||
return false
|
||||
func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
|
||||
for _, device := range devices {
|
||||
if device == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil || len(ledStates) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
return ledStates[ledCapslockKey], true
|
||||
}
|
||||
|
||||
return ledStates[ledCapslockKey]
|
||||
return false, false
|
||||
}
|
||||
|
||||
func findKeyboards() ([]EvdevDevice, error) {
|
||||
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
|
||||
m.devicesMutex.RUnlock()
|
||||
return
|
||||
}
|
||||
device := m.devices[deviceIndex]
|
||||
ordered := make([]EvdevDevice, 0, len(m.devices))
|
||||
ordered = append(ordered, m.devices[deviceIndex])
|
||||
for i, device := range m.devices {
|
||||
if i == deviceIndex {
|
||||
continue
|
||||
}
|
||||
ordered = append(ordered, device)
|
||||
}
|
||||
m.devicesMutex.RUnlock()
|
||||
|
||||
ledStates, err := device.State(evLedType)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to read LED state: %v", err)
|
||||
capsLockState, ok := capsLockFromDevices(ordered)
|
||||
if !ok {
|
||||
log.Debug("No LED-capable device available for caps lock state")
|
||||
return
|
||||
}
|
||||
|
||||
if len(ledStates) == 0 {
|
||||
log.Debug("No LED state available (empty map)")
|
||||
|
||||
// This means the device either:
|
||||
// - doesn't support LED reporting at all, or
|
||||
// - the kernel returned an empty state
|
||||
return
|
||||
}
|
||||
|
||||
capsLockState := ledStates[ledCapslockKey]
|
||||
m.updateCapsLockStateDirect(capsLockState)
|
||||
}
|
||||
|
||||
|
||||
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
|
||||
m.Close()
|
||||
}
|
||||
|
||||
func TestReadInitialCapsLockState(t *testing.T) {
|
||||
func TestCapsLockFromDevices(t *testing.T) {
|
||||
t.Run("caps lock is on", func(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
ledState := evdev.StateMap{
|
||||
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
|
||||
|
||||
result := readInitialCapsLockState(mockDevice)
|
||||
result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
|
||||
assert.True(t, ok)
|
||||
assert.True(t, result)
|
||||
})
|
||||
|
||||
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
}
|
||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
|
||||
|
||||
result := readInitialCapsLockState(mockDevice)
|
||||
result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
|
||||
assert.True(t, ok)
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
|
||||
|
||||
result := readInitialCapsLockState(mockDevice)
|
||||
result, ok := capsLockFromDevices([]EvdevDevice{mockDevice})
|
||||
assert.False(t, ok)
|
||||
assert.False(t, result)
|
||||
})
|
||||
|
||||
t.Run("falls back past device without LED state", func(t *testing.T) {
|
||||
noLedDevice := mocks.NewMockEvdevDevice(t)
|
||||
noLedDevice.EXPECT().State(evdev.EvType(evLedType)).Return(evdev.StateMap{}, nil).Once()
|
||||
|
||||
ledDevice := mocks.NewMockEvdevDevice(t)
|
||||
ledState := evdev.StateMap{
|
||||
ledCapslockKey: true,
|
||||
}
|
||||
ledDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
|
||||
|
||||
result, ok := capsLockFromDevices([]EvdevDevice{noLedDevice, nil, ledDevice})
|
||||
assert.True(t, ok)
|
||||
assert.True(t, result)
|
||||
})
|
||||
}
|
||||
|
||||
func TestHasInputGroupAccess(t *testing.T) {
|
||||
|
||||
@@ -982,6 +982,9 @@ Item {
|
||||
case "reboot":
|
||||
SessionService.reboot();
|
||||
break;
|
||||
case "softreboot":
|
||||
SessionService.softReboot();
|
||||
break;
|
||||
case "poweroff":
|
||||
SessionService.poweroff();
|
||||
break;
|
||||
|
||||
@@ -819,16 +819,24 @@ Item {
|
||||
}
|
||||
|
||||
if (isCategoryFiltered) {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; i++) {
|
||||
allItems.push(getOrTransformApp(rawApps[i]));
|
||||
}
|
||||
// Also include core apps (DMS Settings etc.) that match this category
|
||||
var allCoreApps = AppSearchService.getCoreApps("");
|
||||
for (var i = 0; i < allCoreApps.length; i++) {
|
||||
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
||||
var categoryPluginId = AppSearchService.getPluginIdForCategory(appCategory);
|
||||
if (categoryPluginId) {
|
||||
var pluginCategoryItems = getPluginItems(categoryPluginId, "");
|
||||
for (var i = 0; i < pluginCategoryItems.length; i++) {
|
||||
allItems.push(pluginCategoryItems[i]);
|
||||
}
|
||||
} else {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; i++) {
|
||||
allItems.push(getOrTransformApp(rawApps[i]));
|
||||
}
|
||||
// Also include core apps (DMS Settings etc.) that match this category
|
||||
var allCoreApps = AppSearchService.getCoreApps("");
|
||||
for (var i = 0; i < allCoreApps.length; i++) {
|
||||
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
var apps = searchApps(searchQuery);
|
||||
@@ -839,7 +847,7 @@ Item {
|
||||
|
||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
|
||||
var newSections = Scorer.groupBySection(scoredItems, sectionDefinitions, sortAlpha, searchQuery ? 50 : 500);
|
||||
var newSections = Scorer.groupBySection(scoredItems, buildDynamicSectionDefs(allItems), sortAlpha, searchQuery ? 50 : 500);
|
||||
|
||||
for (var sid in collapsedSections) {
|
||||
for (var i = 0; i < newSections.length; i++) {
|
||||
|
||||
@@ -175,6 +175,8 @@ DankModal {
|
||||
visibleActions = allActions.filter(action => {
|
||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||
return false;
|
||||
if (action === "softreboot" && !SessionService.softRebootSupported)
|
||||
return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
@@ -223,6 +225,12 @@ DankModal {
|
||||
"label": I18n.tr("Reboot"),
|
||||
"key": "R"
|
||||
};
|
||||
case "softreboot":
|
||||
return {
|
||||
"icon": "autorenew",
|
||||
"label": I18n.tr("Soft Reboot"),
|
||||
"key": "B"
|
||||
};
|
||||
case "logout":
|
||||
return {
|
||||
"icon": "logout",
|
||||
@@ -370,7 +378,7 @@ DankModal {
|
||||
|
||||
function handleListNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -429,6 +437,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -464,7 +478,7 @@ DankModal {
|
||||
|
||||
function handleGridNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_B || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
@@ -539,6 +553,12 @@ DankModal {
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_B:
|
||||
if (visibleActions.includes("softreboot")) {
|
||||
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_X:
|
||||
if (visibleActions.includes("logout")) {
|
||||
startHold("logout", visibleActions.indexOf("logout"));
|
||||
@@ -597,7 +617,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
|
||||
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
||||
|
||||
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||
@@ -627,7 +647,7 @@ DankModal {
|
||||
color: {
|
||||
if (gridButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (gridButtonRect.modelData === "reboot")
|
||||
if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
@@ -722,7 +742,7 @@ DankModal {
|
||||
|
||||
readonly property var actionData: root.getActionData(modelData)
|
||||
readonly property bool isSelected: root.selectedIndex === index
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
|
||||
readonly property bool showWarning: modelData === "reboot" || modelData === "softreboot" || modelData === "poweroff"
|
||||
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
||||
|
||||
width: parent.width
|
||||
@@ -752,7 +772,7 @@ DankModal {
|
||||
color: {
|
||||
if (listButtonRect.modelData === "poweroff")
|
||||
return Theme.errorSelected;
|
||||
if (listButtonRect.modelData === "reboot")
|
||||
if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
|
||||
return Theme.withAlpha(Theme.warning, 0.3);
|
||||
return Theme.primarySelected;
|
||||
}
|
||||
|
||||
@@ -469,6 +469,8 @@ PluginComponent {
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: peerMouseArea
|
||||
|
||||
z: -1
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
|
||||
@@ -201,7 +201,16 @@ Item {
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 4
|
||||
targetRadius: root.rt
|
||||
targetColor: barWindow._bgColor
|
||||
// wing-side body corners are square where the gothic fillets attach;
|
||||
// rounding the shadow there leaves a shadow-filled notch under the
|
||||
// translucent bar fill (#2975)
|
||||
topLeftRadius: root.gothEnabled && (root.isBottom || root.isRight) ? 0 : root.rt
|
||||
topRightRadius: root.gothEnabled && (root.isBottom || root.isLeft) ? 0 : root.rt
|
||||
bottomLeftRadius: root.gothEnabled && (root.isTop || root.isRight) ? 0 : root.rt
|
||||
bottomRightRadius: root.gothEnabled && (root.isTop || root.isLeft) ? 0 : root.rt
|
||||
// barShape below is the sole painter of the bar; a fill here doubles
|
||||
// the alpha of translucent bars while the wings stay single-painted
|
||||
targetColor: "transparent"
|
||||
|
||||
shadowBlurPx: root.shadowBlurPx
|
||||
shadowOffsetX: root.shadowOffsetX
|
||||
|
||||
@@ -490,7 +490,7 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: activePlayer?.trackAlbum || ""
|
||||
text: MprisController.stableAlbum
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextSecondary
|
||||
width: parent.width
|
||||
|
||||
@@ -156,9 +156,10 @@ DankOSD {
|
||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||
return;
|
||||
|
||||
const newTitle = player.trackTitle || "";
|
||||
const newArtist = player.trackArtist || "";
|
||||
const newAlbum = player.trackAlbum || "";
|
||||
const metaPlayer = MprisController.bestMetadataPlayer(player);
|
||||
const newTitle = MprisController.displayTrackTitle(metaPlayer);
|
||||
const newArtist = metaPlayer.trackArtist || "";
|
||||
const newAlbum = metaPlayer.trackAlbum || "";
|
||||
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
||||
|
||||
root._displayTitle = newTitle;
|
||||
@@ -263,37 +264,102 @@ DankOSD {
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
color: "transparent"
|
||||
Row {
|
||||
id: transportControls
|
||||
|
||||
x: parent.gap
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
Rectangle {
|
||||
width: Theme.iconSize - 4
|
||||
height: Theme.iconSize - 4
|
||||
radius: (Theme.iconSize - 4) / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: prevButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
opacity: (root.player?.canGoPrevious ?? false) ? 1 : 0.3
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_previous"
|
||||
size: Theme.iconSize - 10
|
||||
color: prevButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: prevButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.player?.canGoPrevious ?? false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MprisController.previousOrRewind();
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playPauseButton
|
||||
Rectangle {
|
||||
width: Theme.iconSize
|
||||
height: Theme.iconSize
|
||||
radius: Theme.iconSize / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: "transparent"
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
togglePlaying();
|
||||
root.hide();
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: root._displayIcon
|
||||
size: Theme.iconSize
|
||||
color: playPauseButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: playPauseButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
togglePlaying();
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: Theme.iconSize - 4
|
||||
height: Theme.iconSize - 4
|
||||
radius: (Theme.iconSize - 4) / 2
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "skip_next"
|
||||
size: Theme.iconSize - 10
|
||||
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: nextButton
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: root.player?.canGoNext ?? false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
MprisController.next();
|
||||
root.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
x: parent.gap * 2 + Theme.iconSize
|
||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
||||
x: parent.gap * 2 + transportControls.width
|
||||
width: parent.width - transportControls.width - parent.gap * 3
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
|
||||
@@ -400,8 +400,8 @@ Item {
|
||||
settingKey: "powerMenuDefaultAction"
|
||||
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
|
||||
text: I18n.tr("Default selected action")
|
||||
options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate")]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate"]
|
||||
options: [I18n.tr("Reboot"), I18n.tr("Log Out"), I18n.tr("Power Off"), I18n.tr("Lock"), I18n.tr("Suspend"), I18n.tr("Restart DMS"), I18n.tr("Hibernate"), I18n.tr("Soft Reboot")]
|
||||
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate", "softreboot"]
|
||||
|
||||
Component.onCompleted: {
|
||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
||||
@@ -465,6 +465,12 @@ Item {
|
||||
label: I18n.tr("Show Hibernate"),
|
||||
desc: I18n.tr("Only visible if hibernate is supported by your system"),
|
||||
hibernate: true
|
||||
},
|
||||
{
|
||||
key: "softreboot",
|
||||
label: I18n.tr("Show Soft Reboot"),
|
||||
desc: I18n.tr("Restart userspace without rebooting the kernel, requires systemd"),
|
||||
softreboot: true
|
||||
}
|
||||
]
|
||||
|
||||
@@ -474,7 +480,13 @@ Item {
|
||||
tags: ["power", "menu", "action", "show", modelData.key]
|
||||
text: modelData.label
|
||||
description: modelData.desc || ""
|
||||
visible: !modelData.hibernate || SessionService.hibernateSupported
|
||||
visible: {
|
||||
if (modelData.hibernate)
|
||||
return SessionService.hibernateSupported;
|
||||
if (modelData.softreboot)
|
||||
return SessionService.softRebootSupported;
|
||||
return true;
|
||||
}
|
||||
checked: SettingsData.powerMenuActions.includes(modelData.key)
|
||||
onToggled: checked => {
|
||||
let actions = [...SettingsData.powerMenuActions];
|
||||
|
||||
@@ -749,16 +749,24 @@ Singleton {
|
||||
if (category === I18n.tr("All"))
|
||||
return visibleApps;
|
||||
|
||||
const pluginItems = getPluginItems(category, "");
|
||||
if (pluginItems.length > 0)
|
||||
return pluginItems;
|
||||
|
||||
return visibleApps.filter(app => {
|
||||
const appCategories = getCategoriesForApp(app);
|
||||
return appCategories.includes(category);
|
||||
});
|
||||
}
|
||||
|
||||
function getPluginIdForCategory(category) {
|
||||
if (typeof PluginService === "undefined")
|
||||
return null;
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
if ((launchers[pluginId].name || pluginId) === category)
|
||||
return pluginId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Plugin launcher support functions
|
||||
function getPluginCategories() {
|
||||
if (typeof PluginService === "undefined") {
|
||||
@@ -778,17 +786,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getPluginCategoryIcon(category) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return null;
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return plugin.icon || "extension";
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
|
||||
}
|
||||
|
||||
function getAllPluginItems() {
|
||||
@@ -809,17 +811,11 @@ Singleton {
|
||||
}
|
||||
|
||||
function getPluginItems(category, query) {
|
||||
if (typeof PluginService === "undefined")
|
||||
const pluginId = getPluginIdForCategory(category);
|
||||
if (!pluginId)
|
||||
return [];
|
||||
|
||||
const launchers = PluginService.getLauncherPlugins();
|
||||
for (const pluginId in launchers) {
|
||||
const plugin = launchers[pluginId];
|
||||
if ((plugin.name || pluginId) === category) {
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
return getPluginItemsForPlugin(pluginId, query);
|
||||
}
|
||||
|
||||
function getPluginItemsForPlugin(pluginId, query) {
|
||||
|
||||
@@ -23,6 +23,13 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
|
||||
function onHasPerformanceProfileChanged() {
|
||||
root.applyPowerProfile();
|
||||
}
|
||||
}
|
||||
|
||||
function applyPowerProfile() {
|
||||
if (!batteryAvailable)
|
||||
return;
|
||||
@@ -32,7 +39,7 @@ Singleton {
|
||||
const targetProfile = parseInt(profileValue);
|
||||
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
||||
return;
|
||||
PowerProfiles.profile = targetProfile;
|
||||
PowerProfileWatcher.applyProfile(targetProfile);
|
||||
}
|
||||
|
||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||
|
||||
@@ -47,6 +47,7 @@ Singleton {
|
||||
// Chromium can report blank metadata between tracks
|
||||
property string stableTitle: ""
|
||||
property string stableArtist: ""
|
||||
property string stableAlbum: ""
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
@@ -59,6 +60,9 @@ Singleton {
|
||||
root._syncStableMeta();
|
||||
root._checkIdle();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onLengthChanged() {
|
||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||
root.activePlayerStableLength = root.activePlayer.length;
|
||||
@@ -72,8 +76,10 @@ Singleton {
|
||||
|
||||
onActivePlayerChanged: {
|
||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||
stableTitle = activePlayer?.trackTitle || "";
|
||||
stableArtist = activePlayer?.trackArtist || "";
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
_syncStableMeta();
|
||||
_checkIdle();
|
||||
}
|
||||
|
||||
@@ -82,14 +88,24 @@ Singleton {
|
||||
if (!p) {
|
||||
stableTitle = "";
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
return;
|
||||
}
|
||||
if (isFirefoxYoutubeHoverPreview(p))
|
||||
return;
|
||||
if (p.trackTitle)
|
||||
stableTitle = p.trackTitle;
|
||||
if (p.trackArtist)
|
||||
stableArtist = p.trackArtist;
|
||||
const metadataPlayer = bestMetadataPlayer(p);
|
||||
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||
if (trackChanged) {
|
||||
stableArtist = "";
|
||||
stableAlbum = "";
|
||||
}
|
||||
if (nextTitle)
|
||||
stableTitle = nextTitle;
|
||||
if (metadataPlayer.trackArtist)
|
||||
stableArtist = metadataPlayer.trackArtist;
|
||||
if (metadataPlayer.trackAlbum)
|
||||
stableAlbum = metadataPlayer.trackAlbum;
|
||||
}
|
||||
|
||||
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
||||
@@ -101,6 +117,7 @@ Singleton {
|
||||
return;
|
||||
root.stableTitle = "";
|
||||
root.stableArtist = "";
|
||||
root.stableAlbum = "";
|
||||
root._resolveActivePlayer();
|
||||
}
|
||||
}
|
||||
@@ -122,9 +139,30 @@ Singleton {
|
||||
delegate: Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
ignoreUnknownSignals: true
|
||||
function onIsPlayingChanged() {
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackTitleChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackArtistChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onTrackAlbumChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
function onMetadataChanged() {
|
||||
if (modelData.isPlaying)
|
||||
root._resolveActivePlayer();
|
||||
root._syncStableMeta();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,9 +171,101 @@ Singleton {
|
||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||
}
|
||||
|
||||
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
|
||||
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
|
||||
|
||||
function _stripAppTitleSuffix(title: string): string {
|
||||
const idx = title.lastIndexOf(" | ");
|
||||
if (idx <= 0)
|
||||
return title;
|
||||
const suffix = title.substring(idx + 3).trim().toLowerCase();
|
||||
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
|
||||
}
|
||||
|
||||
function normalizedTrackTitle(player: MprisPlayer): string {
|
||||
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
|
||||
}
|
||||
|
||||
function displayTrackTitle(player: MprisPlayer): string {
|
||||
return (player?.trackTitle || "").trim();
|
||||
}
|
||||
|
||||
function normalizedTrackArtist(player: MprisPlayer): string {
|
||||
return (player?.trackArtist || "").trim().toLowerCase();
|
||||
}
|
||||
|
||||
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
|
||||
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
|
||||
const firstTitle = normalizedTrackTitle(first);
|
||||
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
|
||||
return false;
|
||||
const firstArtist = normalizedTrackArtist(first);
|
||||
const secondArtist = normalizedTrackArtist(second);
|
||||
if (firstArtist && secondArtist)
|
||||
return firstArtist === secondArtist;
|
||||
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
|
||||
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
|
||||
if (firstUrl && secondUrl)
|
||||
return firstUrl === secondUrl;
|
||||
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
|
||||
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
|
||||
if (firstAlbum && secondAlbum)
|
||||
return firstAlbum === secondAlbum;
|
||||
return true;
|
||||
}
|
||||
|
||||
function metadataQuality(player: MprisPlayer): int {
|
||||
if (!player)
|
||||
return -1;
|
||||
let quality = player.trackArtist ? 100 : 0;
|
||||
quality += player.trackTitle ? 40 : 0;
|
||||
quality += player.trackAlbum ? 20 : 0;
|
||||
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
|
||||
quality += player.metadata?.["xesam:url"] ? 5 : 0;
|
||||
return quality;
|
||||
}
|
||||
|
||||
function equivalentPlayers(player: MprisPlayer): var {
|
||||
if (!player)
|
||||
return [];
|
||||
return availablePlayers.filter(candidate => {
|
||||
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
|
||||
});
|
||||
}
|
||||
|
||||
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
|
||||
const equivalents = equivalentPlayers(player);
|
||||
if (equivalents.length === 0)
|
||||
return player;
|
||||
return equivalents.reduce((best, candidate) => {
|
||||
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
|
||||
}, player);
|
||||
}
|
||||
|
||||
function _bestPlayingPlayer(): MprisPlayer {
|
||||
const playing = availablePlayers.filter(player => player.isPlaying);
|
||||
if (playing.length === 0)
|
||||
return null;
|
||||
|
||||
const controllable = playing.filter(player => player.canControl);
|
||||
if (activePlayer?.isPlaying) {
|
||||
if (activePlayer.canControl || controllable.length === 0)
|
||||
return activePlayer;
|
||||
// Playing but not controllable: only a same-track controllable peer may take over
|
||||
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
|
||||
return mirror || activePlayer;
|
||||
}
|
||||
|
||||
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
|
||||
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
|
||||
if (onlyEquivalentMirrors)
|
||||
return null;
|
||||
}
|
||||
return controllable[0] || playing[0];
|
||||
}
|
||||
|
||||
function _resolveActivePlayer(): void {
|
||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
||||
const playing = availablePlayers.find(p => p.isPlaying);
|
||||
const playing = _bestPlayingPlayer();
|
||||
if (playing) {
|
||||
if (activePlayer !== playing) {
|
||||
activePlayer = playing;
|
||||
|
||||
@@ -17,6 +17,7 @@ Singleton {
|
||||
property bool loginctlCommandAvailable: false
|
||||
property bool systemctlCommandAvailable: false
|
||||
property bool hibernateSupported: false
|
||||
readonly property bool softRebootSupported: systemctlCommandAvailable
|
||||
property bool inhibitorAvailable: true
|
||||
property bool idleInhibited: false
|
||||
property string inhibitReason: "Keep system awake"
|
||||
@@ -452,6 +453,10 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function softReboot() {
|
||||
Quickshell.execDetached(["systemctl", "soft-reboot"]);
|
||||
}
|
||||
|
||||
function poweroff() {
|
||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||
|
||||
@@ -26,7 +26,7 @@ Singleton {
|
||||
return hash.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
function getArtworkUrl(player) {
|
||||
function _directArtworkUrl(player) {
|
||||
if (!player) return "";
|
||||
|
||||
let artUrl = player.trackArtUrl || "";
|
||||
@@ -54,6 +54,17 @@ Singleton {
|
||||
return "";
|
||||
}
|
||||
|
||||
function getArtworkUrl(player) {
|
||||
const directUrl = _directArtworkUrl(player);
|
||||
if (directUrl !== "")
|
||||
return directUrl;
|
||||
|
||||
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
|
||||
return candidate !== player && _directArtworkUrl(candidate) !== "";
|
||||
});
|
||||
return _directArtworkUrl(equivalent);
|
||||
}
|
||||
|
||||
function _commit(u, artKey, srcUrl) {
|
||||
resolvedArtUrl = u;
|
||||
_committedArtKey = u !== "" ? artKey : "";
|
||||
@@ -171,11 +182,25 @@ Singleton {
|
||||
onActivePlayerChanged: _updateArtUrl()
|
||||
|
||||
Connections {
|
||||
target: root.activePlayer
|
||||
ignoreUnknownSignals: true
|
||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||
function onMetadataChanged() { root._updateArtUrl(); }
|
||||
target: MprisController
|
||||
function onAvailablePlayersChanged() {
|
||||
root._updateArtUrl();
|
||||
}
|
||||
}
|
||||
|
||||
Instantiator {
|
||||
model: MprisController.availablePlayers
|
||||
delegate: Connections {
|
||||
required property MprisPlayer modelData
|
||||
target: modelData
|
||||
ignoreUnknownSignals: true
|
||||
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||
function onMetadataChanged() { root._updateArtUrl(); }
|
||||
}
|
||||
}
|
||||
|
||||
function _trackKey() {
|
||||
@@ -201,8 +226,8 @@ Singleton {
|
||||
}
|
||||
_pendingArtKey = key;
|
||||
const url = getArtworkUrl(activePlayer);
|
||||
// Ignore Chrome's same-track thumbnail size updates.
|
||||
if (key !== "" && key === _committedArtKey)
|
||||
// Ignore duplicate notifications, but let a richer peer replace same-track art
|
||||
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||
return;
|
||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||
// Chrome can publish track metadata before its new artwork URL.
|
||||
|
||||
Reference in New Issue
Block a user