mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-05 04:58:30 -04:00
Compare commits
15 Commits
v1.5.3
...
stable-1.5
| Author | SHA1 | Date | |
|---|---|---|---|
| c2a7f767c2 | |||
| c9c11f8a27 | |||
| 8cffdab4d7 | |||
| 0e8a4c0cc1 | |||
| 1db6381c78 | |||
| d5c7efc861 | |||
| 943ffb432c | |||
| 7063448b80 | |||
| 52afbc9801 | |||
| 6edb985847 | |||
| 41efe6ad15 | |||
| 4fb6a17e1b | |||
| e771e3b675 | |||
| b1b7aa7aa2 | |||
| c128793239 |
@@ -313,6 +313,7 @@ func EnsureContrastDPSLstar(hexColor, hexBg string, minLc float64, isLightMode b
|
|||||||
fg := HexToRGB(hexColor)
|
fg := HexToRGB(hexColor)
|
||||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||||
Lf, af, bf := cf.Lab()
|
Lf, af, bf := cf.Lab()
|
||||||
|
Lf *= 100.0
|
||||||
|
|
||||||
dir := 1.0
|
dir := 1.0
|
||||||
if isLightMode {
|
if isLightMode {
|
||||||
@@ -341,6 +342,7 @@ func EnsureContrastDPSBidirectional(hexColor, hexBg string, minLc float64, isLig
|
|||||||
fg := HexToRGB(hexColor)
|
fg := HexToRGB(hexColor)
|
||||||
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
cf := colorful.Color{R: fg.R, G: fg.G, B: fg.B}
|
||||||
origL, af, bf := cf.Lab()
|
origL, af, bf := cf.Lab()
|
||||||
|
origL *= 100.0
|
||||||
|
|
||||||
var darkerResult, lighterResult string
|
var darkerResult, lighterResult string
|
||||||
darkerL, lighterL := origL, origL
|
darkerL, lighterL := origL, origL
|
||||||
@@ -420,6 +422,24 @@ func blendHue(base, target, factor float64) float64 {
|
|||||||
return result
|
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 {
|
func DeriveContainer(primary string, isLight bool) string {
|
||||||
rgb := HexToRGB(primary)
|
rgb := HexToRGB(primary)
|
||||||
hsv := RGBToHSV(rgb)
|
hsv := RGBToHSV(rgb)
|
||||||
@@ -500,10 +520,7 @@ func GeneratePalette(primaryColor string, opts PaletteOptions) Palette {
|
|||||||
gray7V := baseVal * 0.28
|
gray7V := baseVal * 0.28
|
||||||
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
gray8S := baseSat * 0.05
|
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.05, true))
|
||||||
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))
|
|
||||||
|
|
||||||
brightRedS := math.Min(baseSat*1.0, 1.0)
|
brightRedS := math.Min(baseSat*1.0, 1.0)
|
||||||
brightRedV := math.Min(baseVal*1.2, 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)
|
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))
|
palette.Color7 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray7S, V: gray7V})), bgColor, normalTextTarget, opts))
|
||||||
|
|
||||||
gray8S := baseSat * 0.15
|
palette.Color8 = NewColorInfo(DeriveDim(bgColor, hsv.H, baseSat*0.15, false))
|
||||||
gray8V := baseVal * 0.65
|
|
||||||
palette.Color8 = NewColorInfo(ensureContrastAuto(RGBToHex(HSVToRGB(HSV{H: hsv.H, S: gray8S, V: gray8V})), bgColor, secondaryTarget, opts))
|
|
||||||
|
|
||||||
brightRedS := math.Min(baseSat*0.75, 1.0)
|
brightRedS := math.Min(baseSat*0.75, 1.0)
|
||||||
brightRedV := math.Min(baseVal*1.35, 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)
|
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 {
|
func (g *GentooDistribution) getArchKeyword() string {
|
||||||
arch := runtime.GOARCH
|
arch := runtime.GOARCH
|
||||||
switch arch {
|
switch arch {
|
||||||
@@ -319,6 +324,7 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
}
|
}
|
||||||
g.log("Portage tree synced successfully")
|
g.log("Portage tree synced successfully")
|
||||||
|
|
||||||
|
args := emergeInstallArgs(missingPkgs)
|
||||||
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
g.log(fmt.Sprintf("Installing prerequisites: %s", strings.Join(missingPkgs, ", ")))
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhasePrerequisites,
|
Phase: PhasePrerequisites,
|
||||||
@@ -326,12 +332,10 @@ func (g *GentooDistribution) InstallPrerequisites(ctx context.Context, sudoPassw
|
|||||||
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
Step: fmt.Sprintf("Installing %d prerequisites...", len(missingPkgs)),
|
||||||
IsComplete: false,
|
IsComplete: false,
|
||||||
NeedsSudo: true,
|
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, ", ")),
|
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, " "))
|
cmd := privesc.ExecCommand(ctx, sudoPassword, strings.Join(args, " "))
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -521,8 +525,7 @@ func (g *GentooDistribution) installPortagePackages(ctx context.Context, package
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := emergeInstallArgs(packageNames)
|
||||||
args = append(args, packageNames...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseSystemPackages,
|
Phase: PhaseSystemPackages,
|
||||||
@@ -713,8 +716,7 @@ func (g *GentooDistribution) installGURUPackages(ctx context.Context, packages [
|
|||||||
guruPackages[i] = pkg + "::guru"
|
guruPackages[i] = pkg + "::guru"
|
||||||
}
|
}
|
||||||
|
|
||||||
args := []string{"emerge", "--ask=n", "--quiet"}
|
args := emergeInstallArgs(guruPackages)
|
||||||
args = append(args, guruPackages...)
|
|
||||||
|
|
||||||
progressChan <- InstallProgressMsg{
|
progressChan <- InstallProgressMsg{
|
||||||
Phase: PhaseAURPackages,
|
Phase: PhaseAURPackages,
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ func NewManager() (*Manager, error) {
|
|||||||
return nil, fmt.Errorf("failed to find keyboards: %w", err)
|
return nil, fmt.Errorf("failed to find keyboards: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
initialCapsLock := readInitialCapsLockState(devices[0])
|
initialCapsLock, _ := capsLockFromDevices(devices)
|
||||||
|
|
||||||
watcher, err := fsnotify.NewWatcher()
|
watcher, err := fsnotify.NewWatcher()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -85,14 +85,21 @@ func NewManager() (*Manager, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func readInitialCapsLockState(device EvdevDevice) bool {
|
func capsLockFromDevices(devices []EvdevDevice) (bool, bool) {
|
||||||
ledStates, err := device.State(evLedType)
|
for _, device := range devices {
|
||||||
if err != nil {
|
if device == nil {
|
||||||
log.Debugf("Could not read LED state: %v", err)
|
continue
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return ledStates[ledCapslockKey]
|
ledStates, err := device.State(evLedType)
|
||||||
|
if err != nil || len(ledStates) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return ledStates[ledCapslockKey], true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, false
|
||||||
}
|
}
|
||||||
|
|
||||||
func findKeyboards() ([]EvdevDevice, error) {
|
func findKeyboards() ([]EvdevDevice, error) {
|
||||||
@@ -297,25 +304,22 @@ func (m *Manager) readAndUpdateCapsLockState(deviceIndex int) {
|
|||||||
m.devicesMutex.RUnlock()
|
m.devicesMutex.RUnlock()
|
||||||
return
|
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()
|
m.devicesMutex.RUnlock()
|
||||||
|
|
||||||
ledStates, err := device.State(evLedType)
|
capsLockState, ok := capsLockFromDevices(ordered)
|
||||||
if err != nil {
|
if !ok {
|
||||||
log.Warnf("Failed to read LED state: %v", err)
|
log.Debug("No LED-capable device available for caps lock state")
|
||||||
return
|
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)
|
m.updateCapsLockStateDirect(capsLockState)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -306,7 +306,7 @@ func TestNotifySubscribers(t *testing.T) {
|
|||||||
m.Close()
|
m.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadInitialCapsLockState(t *testing.T) {
|
func TestCapsLockFromDevices(t *testing.T) {
|
||||||
t.Run("caps lock is on", func(t *testing.T) {
|
t.Run("caps lock is on", func(t *testing.T) {
|
||||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||||
ledState := evdev.StateMap{
|
ledState := evdev.StateMap{
|
||||||
@@ -314,7 +314,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
|
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)
|
assert.True(t, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -325,7 +326,8 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
|||||||
}
|
}
|
||||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(ledState, nil).Once()
|
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)
|
assert.False(t, result)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -333,9 +335,25 @@ func TestReadInitialCapsLockState(t *testing.T) {
|
|||||||
mockDevice := mocks.NewMockEvdevDevice(t)
|
mockDevice := mocks.NewMockEvdevDevice(t)
|
||||||
mockDevice.EXPECT().State(evdev.EvType(evLedType)).Return(nil, errors.New("read error")).Once()
|
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)
|
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) {
|
func TestHasInputGroupAccess(t *testing.T) {
|
||||||
|
|||||||
@@ -982,6 +982,9 @@ Item {
|
|||||||
case "reboot":
|
case "reboot":
|
||||||
SessionService.reboot();
|
SessionService.reboot();
|
||||||
break;
|
break;
|
||||||
|
case "softreboot":
|
||||||
|
SessionService.softReboot();
|
||||||
|
break;
|
||||||
case "poweroff":
|
case "poweroff":
|
||||||
SessionService.poweroff();
|
SessionService.poweroff();
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -819,6 +819,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isCategoryFiltered) {
|
if (isCategoryFiltered) {
|
||||||
|
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);
|
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||||
for (var i = 0; i < rawApps.length; i++) {
|
for (var i = 0; i < rawApps.length; i++) {
|
||||||
allItems.push(getOrTransformApp(rawApps[i]));
|
allItems.push(getOrTransformApp(rawApps[i]));
|
||||||
@@ -830,6 +837,7 @@ Item {
|
|||||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
if (coreAppCats.indexOf(appCategory) !== -1)
|
||||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
allItems.push(transformCoreApp(allCoreApps[i]));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
var apps = searchApps(searchQuery);
|
var apps = searchApps(searchQuery);
|
||||||
for (var i = 0; i < apps.length; i++) {
|
for (var i = 0; i < apps.length; i++) {
|
||||||
@@ -839,7 +847,7 @@ Item {
|
|||||||
|
|
||||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||||
var sortAlpha = !searchQuery && SettingsData.sortAppsAlphabetically;
|
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 sid in collapsedSections) {
|
||||||
for (var i = 0; i < newSections.length; i++) {
|
for (var i = 0; i < newSections.length; i++) {
|
||||||
|
|||||||
@@ -175,6 +175,8 @@ DankModal {
|
|||||||
visibleActions = allActions.filter(action => {
|
visibleActions = allActions.filter(action => {
|
||||||
if (action === "hibernate" && !SessionService.hibernateSupported)
|
if (action === "hibernate" && !SessionService.hibernateSupported)
|
||||||
return false;
|
return false;
|
||||||
|
if (action === "softreboot" && !SessionService.softRebootSupported)
|
||||||
|
return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -223,6 +225,12 @@ DankModal {
|
|||||||
"label": I18n.tr("Reboot"),
|
"label": I18n.tr("Reboot"),
|
||||||
"key": "R"
|
"key": "R"
|
||||||
};
|
};
|
||||||
|
case "softreboot":
|
||||||
|
return {
|
||||||
|
"icon": "autorenew",
|
||||||
|
"label": I18n.tr("Soft Reboot"),
|
||||||
|
"key": "B"
|
||||||
|
};
|
||||||
case "logout":
|
case "logout":
|
||||||
return {
|
return {
|
||||||
"icon": "logout",
|
"icon": "logout",
|
||||||
@@ -370,7 +378,7 @@ DankModal {
|
|||||||
|
|
||||||
function handleListNavigation(event, isPressed) {
|
function handleListNavigation(event, isPressed) {
|
||||||
if (!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();
|
cancelHold();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
@@ -429,6 +437,12 @@ DankModal {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Qt.Key_B:
|
||||||
|
if (visibleActions.includes("softreboot")) {
|
||||||
|
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Qt.Key_X:
|
case Qt.Key_X:
|
||||||
if (visibleActions.includes("logout")) {
|
if (visibleActions.includes("logout")) {
|
||||||
startHold("logout", visibleActions.indexOf("logout"));
|
startHold("logout", visibleActions.indexOf("logout"));
|
||||||
@@ -464,7 +478,7 @@ DankModal {
|
|||||||
|
|
||||||
function handleGridNavigation(event, isPressed) {
|
function handleGridNavigation(event, isPressed) {
|
||||||
if (!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();
|
cancelHold();
|
||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
@@ -539,6 +553,12 @@ DankModal {
|
|||||||
event.accepted = true;
|
event.accepted = true;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case Qt.Key_B:
|
||||||
|
if (visibleActions.includes("softreboot")) {
|
||||||
|
startHold("softreboot", visibleActions.indexOf("softreboot"));
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
case Qt.Key_X:
|
case Qt.Key_X:
|
||||||
if (visibleActions.includes("logout")) {
|
if (visibleActions.includes("logout")) {
|
||||||
startHold("logout", visibleActions.indexOf("logout"));
|
startHold("logout", visibleActions.indexOf("logout"));
|
||||||
@@ -597,7 +617,7 @@ DankModal {
|
|||||||
|
|
||||||
readonly property var actionData: root.getActionData(modelData)
|
readonly property var actionData: root.getActionData(modelData)
|
||||||
readonly property bool isSelected: root.selectedIndex === index
|
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
|
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
||||||
|
|
||||||
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
|
||||||
@@ -627,7 +647,7 @@ DankModal {
|
|||||||
color: {
|
color: {
|
||||||
if (gridButtonRect.modelData === "poweroff")
|
if (gridButtonRect.modelData === "poweroff")
|
||||||
return Theme.errorSelected;
|
return Theme.errorSelected;
|
||||||
if (gridButtonRect.modelData === "reboot")
|
if (gridButtonRect.modelData === "reboot" || gridButtonRect.modelData === "softreboot")
|
||||||
return Theme.withAlpha(Theme.warning, 0.3);
|
return Theme.withAlpha(Theme.warning, 0.3);
|
||||||
return Theme.primarySelected;
|
return Theme.primarySelected;
|
||||||
}
|
}
|
||||||
@@ -722,7 +742,7 @@ DankModal {
|
|||||||
|
|
||||||
readonly property var actionData: root.getActionData(modelData)
|
readonly property var actionData: root.getActionData(modelData)
|
||||||
readonly property bool isSelected: root.selectedIndex === index
|
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
|
readonly property bool isHolding: root.holdActionIndex === index && root.holdProgress > 0
|
||||||
|
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -752,7 +772,7 @@ DankModal {
|
|||||||
color: {
|
color: {
|
||||||
if (listButtonRect.modelData === "poweroff")
|
if (listButtonRect.modelData === "poweroff")
|
||||||
return Theme.errorSelected;
|
return Theme.errorSelected;
|
||||||
if (listButtonRect.modelData === "reboot")
|
if (listButtonRect.modelData === "reboot" || listButtonRect.modelData === "softreboot")
|
||||||
return Theme.withAlpha(Theme.warning, 0.3);
|
return Theme.withAlpha(Theme.warning, 0.3);
|
||||||
return Theme.primarySelected;
|
return Theme.primarySelected;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -469,6 +469,8 @@ PluginComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
|
id: peerMouseArea
|
||||||
|
|
||||||
z: -1
|
z: -1
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
hoverEnabled: true
|
hoverEnabled: true
|
||||||
|
|||||||
@@ -201,7 +201,16 @@ Item {
|
|||||||
direction: root.effectiveShadowDirection
|
direction: root.effectiveShadowDirection
|
||||||
fallbackOffset: 4
|
fallbackOffset: 4
|
||||||
targetRadius: root.rt
|
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
|
shadowBlurPx: root.shadowBlurPx
|
||||||
shadowOffsetX: root.shadowOffsetX
|
shadowOffsetX: root.shadowOffsetX
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: activePlayer?.trackAlbum || ""
|
text: MprisController.stableAlbum
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceTextSecondary
|
color: Theme.surfaceTextSecondary
|
||||||
width: parent.width
|
width: parent.width
|
||||||
|
|||||||
@@ -479,8 +479,6 @@ Item {
|
|||||||
|
|
||||||
delegate: Item {
|
delegate: Item {
|
||||||
id: delegateItem
|
id: delegateItem
|
||||||
required property var modelData
|
|
||||||
required property int index
|
|
||||||
|
|
||||||
property var dockButton: {
|
property var dockButton: {
|
||||||
switch (itemData.type) {
|
switch (itemData.type) {
|
||||||
@@ -601,7 +599,7 @@ Item {
|
|||||||
height: delegateItem.height
|
height: delegateItem.height
|
||||||
actualIconSize: root.iconSize
|
actualIconSize: root.iconSize
|
||||||
dockApps: root
|
dockApps: root
|
||||||
index: delegateItem.index
|
index: model.index
|
||||||
}
|
}
|
||||||
|
|
||||||
DockTrashButton {
|
DockTrashButton {
|
||||||
@@ -626,7 +624,7 @@ Item {
|
|||||||
appData: itemData
|
appData: itemData
|
||||||
contextMenu: root.contextMenu
|
contextMenu: root.contextMenu
|
||||||
dockApps: root
|
dockApps: root
|
||||||
index: delegateItem.index
|
index: model.index
|
||||||
parentDockScreen: root.dockScreen
|
parentDockScreen: root.dockScreen
|
||||||
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
showWindowTitle: itemData?.type === "window" || itemData?.type === "grouped"
|
||||||
windowTitle: {
|
windowTitle: {
|
||||||
|
|||||||
@@ -386,16 +386,20 @@ end)
|
|||||||
HYPRLAND_LUA_EOF
|
HYPRLAND_LUA_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
elif [[ -z "$COMPOSITOR_CONFIG" ]]; then
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp --suffix=.lua)
|
||||||
cat > "$TEMP_CONFIG" << HYPRLAND_EOF
|
cat > "$TEMP_CONFIG" << HYPRLAND_LUA_EOF
|
||||||
env = DMS_RUN_GREETER,1
|
hl.env("DMS_RUN_GREETER", "1")
|
||||||
|
|
||||||
misc {
|
hl.config({
|
||||||
disable_hyprland_logo = true
|
misc = {
|
||||||
}
|
disable_hyprland_logo = true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
exec-once = sh -c "$QS_CMD; hyprctl dispatch exit"
|
hl.on("hyprland.start", function()
|
||||||
HYPRLAND_EOF
|
hl.exec_cmd('sh -c "$QS_CMD; hyprctl dispatch exit"')
|
||||||
|
end)
|
||||||
|
HYPRLAND_LUA_EOF
|
||||||
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
COMPOSITOR_CONFIG="$TEMP_CONFIG"
|
||||||
else
|
else
|
||||||
TEMP_CONFIG=$(mktemp)
|
TEMP_CONFIG=$(mktemp)
|
||||||
|
|||||||
@@ -156,9 +156,10 @@ DankOSD {
|
|||||||
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
if (MprisController.isFirefoxYoutubeHoverPreview(player))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const newTitle = player.trackTitle || "";
|
const metaPlayer = MprisController.bestMetadataPlayer(player);
|
||||||
const newArtist = player.trackArtist || "";
|
const newTitle = MprisController.displayTrackTitle(metaPlayer);
|
||||||
const newAlbum = player.trackAlbum || "";
|
const newArtist = metaPlayer.trackArtist || "";
|
||||||
|
const newAlbum = metaPlayer.trackAlbum || "";
|
||||||
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
const trackChanged = newTitle !== root._displayTitle || newArtist !== root._displayArtist || newAlbum !== root._displayAlbum;
|
||||||
|
|
||||||
root._displayTitle = newTitle;
|
root._displayTitle = newTitle;
|
||||||
@@ -263,13 +264,48 @@ DankOSD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: transportControls
|
||||||
|
|
||||||
|
x: parent.gap
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
width: Theme.iconSize
|
width: Theme.iconSize
|
||||||
height: Theme.iconSize
|
height: Theme.iconSize
|
||||||
radius: Theme.iconSize / 2
|
radius: Theme.iconSize / 2
|
||||||
color: "transparent"
|
|
||||||
x: parent.gap
|
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: "transparent"
|
||||||
|
|
||||||
DankIcon {
|
DankIcon {
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
@@ -291,9 +327,39 @@ DankOSD {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: Theme.iconSize - 4
|
||||||
|
height: Theme.iconSize - 4
|
||||||
|
radius: (Theme.iconSize - 4) / 2
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: nextButton.containsMouse ? Theme.surfaceTextHover : "transparent"
|
||||||
|
opacity: (root.player?.canGoNext ?? false) ? 1 : 0.3
|
||||||
|
|
||||||
|
DankIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: "skip_next"
|
||||||
|
size: Theme.iconSize - 10
|
||||||
|
color: nextButton.containsMouse ? Theme.primary : Theme.surfaceText
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: nextButton
|
||||||
|
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
enabled: root.player?.canGoNext ?? false
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: {
|
||||||
|
MprisController.next();
|
||||||
|
root.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
x: parent.gap * 2 + Theme.iconSize
|
x: parent.gap * 2 + transportControls.width
|
||||||
width: parent.width - Theme.iconSize - parent.gap * 3
|
width: parent.width - transportControls.width - parent.gap * 3
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingXXS
|
spacing: Theme.spacingXXS
|
||||||
|
|
||||||
|
|||||||
@@ -400,8 +400,8 @@ Item {
|
|||||||
settingKey: "powerMenuDefaultAction"
|
settingKey: "powerMenuDefaultAction"
|
||||||
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
|
tags: ["power", "menu", "default", "action", "reboot", "logout", "shutdown"]
|
||||||
text: I18n.tr("Default selected action")
|
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")]
|
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"]
|
property var actionValues: ["reboot", "logout", "poweroff", "lock", "suspend", "restart", "hibernate", "softreboot"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
const currentAction = SettingsData.powerMenuDefaultAction || "logout";
|
||||||
@@ -465,6 +465,12 @@ Item {
|
|||||||
label: I18n.tr("Show Hibernate"),
|
label: I18n.tr("Show Hibernate"),
|
||||||
desc: I18n.tr("Only visible if hibernate is supported by your system"),
|
desc: I18n.tr("Only visible if hibernate is supported by your system"),
|
||||||
hibernate: true
|
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]
|
tags: ["power", "menu", "action", "show", modelData.key]
|
||||||
text: modelData.label
|
text: modelData.label
|
||||||
description: modelData.desc || ""
|
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)
|
checked: SettingsData.powerMenuActions.includes(modelData.key)
|
||||||
onToggled: checked => {
|
onToggled: checked => {
|
||||||
let actions = [...SettingsData.powerMenuActions];
|
let actions = [...SettingsData.powerMenuActions];
|
||||||
|
|||||||
@@ -749,16 +749,24 @@ Singleton {
|
|||||||
if (category === I18n.tr("All"))
|
if (category === I18n.tr("All"))
|
||||||
return visibleApps;
|
return visibleApps;
|
||||||
|
|
||||||
const pluginItems = getPluginItems(category, "");
|
|
||||||
if (pluginItems.length > 0)
|
|
||||||
return pluginItems;
|
|
||||||
|
|
||||||
return visibleApps.filter(app => {
|
return visibleApps.filter(app => {
|
||||||
const appCategories = getCategoriesForApp(app);
|
const appCategories = getCategoriesForApp(app);
|
||||||
return appCategories.includes(category);
|
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
|
// Plugin launcher support functions
|
||||||
function getPluginCategories() {
|
function getPluginCategories() {
|
||||||
if (typeof PluginService === "undefined") {
|
if (typeof PluginService === "undefined") {
|
||||||
@@ -778,17 +786,11 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPluginCategoryIcon(category) {
|
function getPluginCategoryIcon(category) {
|
||||||
if (typeof PluginService === "undefined")
|
const pluginId = getPluginIdForCategory(category);
|
||||||
|
if (!pluginId)
|
||||||
return null;
|
return null;
|
||||||
|
|
||||||
const launchers = PluginService.getLauncherPlugins();
|
return PluginService.getLauncherPlugins()[pluginId].icon || "extension";
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const plugin = launchers[pluginId];
|
|
||||||
if ((plugin.name || pluginId) === category) {
|
|
||||||
return plugin.icon || "extension";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAllPluginItems() {
|
function getAllPluginItems() {
|
||||||
@@ -809,18 +811,12 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getPluginItems(category, query) {
|
function getPluginItems(category, query) {
|
||||||
if (typeof PluginService === "undefined")
|
const pluginId = getPluginIdForCategory(category);
|
||||||
|
if (!pluginId)
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
const launchers = PluginService.getLauncherPlugins();
|
|
||||||
for (const pluginId in launchers) {
|
|
||||||
const plugin = launchers[pluginId];
|
|
||||||
if ((plugin.name || pluginId) === category) {
|
|
||||||
return getPluginItemsForPlugin(pluginId, query);
|
return getPluginItemsForPlugin(pluginId, query);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPluginItemsForPlugin(pluginId, query) {
|
function getPluginItemsForPlugin(pluginId, query) {
|
||||||
if (typeof PluginService === "undefined") {
|
if (typeof PluginService === "undefined") {
|
||||||
|
|||||||
@@ -23,6 +23,13 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: typeof PowerProfiles !== "undefined" ? PowerProfiles : null
|
||||||
|
function onHasPerformanceProfileChanged() {
|
||||||
|
root.applyPowerProfile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function applyPowerProfile() {
|
function applyPowerProfile() {
|
||||||
if (!batteryAvailable)
|
if (!batteryAvailable)
|
||||||
return;
|
return;
|
||||||
@@ -32,7 +39,7 @@ Singleton {
|
|||||||
const targetProfile = parseInt(profileValue);
|
const targetProfile = parseInt(profileValue);
|
||||||
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
if (isNaN(targetProfile) || PowerProfiles.profile === targetProfile)
|
||||||
return;
|
return;
|
||||||
PowerProfiles.profile = targetProfile;
|
PowerProfileWatcher.applyProfile(targetProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
readonly property string preferredBatteryOverride: Quickshell.env("DMS_PREFERRED_BATTERY")
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Singleton {
|
|||||||
id: root
|
id: root
|
||||||
readonly property var log: Log.scoped("DMSService")
|
readonly property var log: Log.scoped("DMSService")
|
||||||
|
|
||||||
property bool dmsAvailable: false
|
readonly property bool dmsAvailable: isConnected
|
||||||
property var capabilities: []
|
property var capabilities: []
|
||||||
property int apiVersion: 0
|
property int apiVersion: 0
|
||||||
property string cliVersion: ""
|
property string cliVersion: ""
|
||||||
@@ -21,7 +21,7 @@ Singleton {
|
|||||||
property var availableThemes: []
|
property var availableThemes: []
|
||||||
property var installedThemes: []
|
property var installedThemes: []
|
||||||
property bool isConnected: false
|
property bool isConnected: false
|
||||||
property bool isConnecting: false
|
readonly property bool isConnecting: requestSocket.connected && !requestSocket.linkUp
|
||||||
property bool subscribeConnected: false
|
property bool subscribeConnected: false
|
||||||
|
|
||||||
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
|
||||||
@@ -72,9 +72,10 @@ Singleton {
|
|||||||
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
|
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "wallpaper", "bluetooth", "bluetooth.pairing", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "sysupdate"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (socketPath && socketPath.length > 0) {
|
if (!socketPath || socketPath.length === 0)
|
||||||
|
return;
|
||||||
detectUpdateCommand();
|
detectUpdateCommand();
|
||||||
}
|
requestSocket.connected = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function detectUpdateCommand() {
|
function detectUpdateCommand() {
|
||||||
@@ -82,12 +83,6 @@ Singleton {
|
|||||||
checkAurHelper.running = true;
|
checkAurHelper.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startSocketConnection() {
|
|
||||||
if (socketPath && socketPath.length > 0) {
|
|
||||||
testProcess.running = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: checkAurHelper
|
id: checkAurHelper
|
||||||
command: ["sh", "-c", "command -v paru || command -v yay"]
|
command: ["sh", "-c", "command -v paru || command -v yay"]
|
||||||
@@ -105,7 +100,6 @@ Singleton {
|
|||||||
} else {
|
} else {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -114,7 +108,6 @@ Singleton {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +128,6 @@ Singleton {
|
|||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
}
|
}
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,53 +135,28 @@ Singleton {
|
|||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
updateCommand = "dms update";
|
updateCommand = "dms update";
|
||||||
checkingUpdateCommand = false;
|
checkingUpdateCommand = false;
|
||||||
startSocketConnection();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Process {
|
|
||||||
id: testProcess
|
|
||||||
command: ["test", "-S", root.socketPath]
|
|
||||||
|
|
||||||
onExited: exitCode => {
|
|
||||||
if (exitCode === 0) {
|
|
||||||
root.dmsAvailable = true;
|
|
||||||
connectSocket();
|
|
||||||
} else {
|
|
||||||
root.dmsAvailable = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectSocket() {
|
|
||||||
if (!dmsAvailable || isConnected || isConnecting) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
isConnecting = true;
|
|
||||||
requestSocket.connected = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DankSocket {
|
DankSocket {
|
||||||
id: requestSocket
|
id: requestSocket
|
||||||
path: root.socketPath
|
path: root.socketPath
|
||||||
connected: false
|
connected: false
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
onConnectionStateChanged: {
|
||||||
if (connected) {
|
if (linkUp) {
|
||||||
root.isConnected = true;
|
root.isConnected = true;
|
||||||
root.isConnecting = false;
|
|
||||||
root.connectionStateChanged();
|
root.connectionStateChanged();
|
||||||
subscribeSocket.connected = true;
|
subscribeSocket.connected = true;
|
||||||
} else {
|
return;
|
||||||
|
}
|
||||||
root.isConnected = false;
|
root.isConnected = false;
|
||||||
root.isConnecting = false;
|
|
||||||
root.apiVersion = 0;
|
root.apiVersion = 0;
|
||||||
root.capabilities = [];
|
root.capabilities = [];
|
||||||
|
root.failPendingRequests();
|
||||||
root.connectionStateChanged();
|
root.connectionStateChanged();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
onRead: line => {
|
onRead: line => {
|
||||||
@@ -219,11 +186,11 @@ Singleton {
|
|||||||
connected: false
|
connected: false
|
||||||
|
|
||||||
onConnectionStateChanged: {
|
onConnectionStateChanged: {
|
||||||
root.subscribeConnected = connected;
|
root.subscribeConnected = linkUp;
|
||||||
if (connected) {
|
if (!linkUp)
|
||||||
|
return;
|
||||||
sendSubscribeRequest();
|
sendSubscribeRequest();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
parser: SplitParser {
|
parser: SplitParser {
|
||||||
onRead: line => {
|
onRead: line => {
|
||||||
@@ -446,11 +413,21 @@ Singleton {
|
|||||||
|
|
||||||
function handleResponse(response) {
|
function handleResponse(response) {
|
||||||
const callback = pendingRequests[response.id];
|
const callback = pendingRequests[response.id];
|
||||||
|
if (!callback)
|
||||||
if (callback) {
|
return;
|
||||||
delete pendingRequests[response.id];
|
delete pendingRequests[response.id];
|
||||||
callback(response);
|
callback(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function failPendingRequests() {
|
||||||
|
const pending = pendingRequests;
|
||||||
|
pendingRequests = {};
|
||||||
|
clipboardRequestIds = {};
|
||||||
|
for (const id in pending) {
|
||||||
|
pending[id]({
|
||||||
|
"error": "not connected to DMS socket"
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ping(callback) {
|
function ping(callback) {
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ Singleton {
|
|||||||
// Chromium can report blank metadata between tracks
|
// Chromium can report blank metadata between tracks
|
||||||
property string stableTitle: ""
|
property string stableTitle: ""
|
||||||
property string stableArtist: ""
|
property string stableArtist: ""
|
||||||
|
property string stableAlbum: ""
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: root.activePlayer
|
||||||
@@ -59,6 +60,9 @@ Singleton {
|
|||||||
root._syncStableMeta();
|
root._syncStableMeta();
|
||||||
root._checkIdle();
|
root._checkIdle();
|
||||||
}
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
function onLengthChanged() {
|
function onLengthChanged() {
|
||||||
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
if (root.activePlayer && root.activePlayer.lengthSupported && root.activePlayer.length > 1) {
|
||||||
root.activePlayerStableLength = root.activePlayer.length;
|
root.activePlayerStableLength = root.activePlayer.length;
|
||||||
@@ -72,8 +76,10 @@ Singleton {
|
|||||||
|
|
||||||
onActivePlayerChanged: {
|
onActivePlayerChanged: {
|
||||||
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
activePlayerStableLength = (activePlayer && activePlayer.lengthSupported && activePlayer.length > 1) ? activePlayer.length : 0;
|
||||||
stableTitle = activePlayer?.trackTitle || "";
|
stableTitle = "";
|
||||||
stableArtist = activePlayer?.trackArtist || "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
_syncStableMeta();
|
||||||
_checkIdle();
|
_checkIdle();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,14 +88,24 @@ Singleton {
|
|||||||
if (!p) {
|
if (!p) {
|
||||||
stableTitle = "";
|
stableTitle = "";
|
||||||
stableArtist = "";
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isFirefoxYoutubeHoverPreview(p))
|
if (isFirefoxYoutubeHoverPreview(p))
|
||||||
return;
|
return;
|
||||||
if (p.trackTitle)
|
const metadataPlayer = bestMetadataPlayer(p);
|
||||||
stableTitle = p.trackTitle;
|
const nextTitle = displayTrackTitle(metadataPlayer);
|
||||||
if (p.trackArtist)
|
const trackChanged = nextTitle && stableTitle && nextTitle.toLowerCase() !== stableTitle.toLowerCase();
|
||||||
stableArtist = p.trackArtist;
|
if (trackChanged) {
|
||||||
|
stableArtist = "";
|
||||||
|
stableAlbum = "";
|
||||||
|
}
|
||||||
|
if (nextTitle)
|
||||||
|
stableTitle = nextTitle;
|
||||||
|
if (metadataPlayer.trackArtist)
|
||||||
|
stableArtist = metadataPlayer.trackArtist;
|
||||||
|
if (metadataPlayer.trackAlbum)
|
||||||
|
stableAlbum = metadataPlayer.trackAlbum;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
// Chromium reports stopped media w/blank metadata, resolve by checking idle status
|
||||||
@@ -101,6 +117,7 @@ Singleton {
|
|||||||
return;
|
return;
|
||||||
root.stableTitle = "";
|
root.stableTitle = "";
|
||||||
root.stableArtist = "";
|
root.stableArtist = "";
|
||||||
|
root.stableAlbum = "";
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -122,9 +139,30 @@ Singleton {
|
|||||||
delegate: Connections {
|
delegate: Connections {
|
||||||
required property MprisPlayer modelData
|
required property MprisPlayer modelData
|
||||||
target: modelData
|
target: modelData
|
||||||
|
ignoreUnknownSignals: true
|
||||||
function onIsPlayingChanged() {
|
function onIsPlayingChanged() {
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackTitleChanged() {
|
||||||
if (modelData.isPlaying)
|
if (modelData.isPlaying)
|
||||||
root._resolveActivePlayer();
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackArtistChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onTrackAlbumChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
|
}
|
||||||
|
function onMetadataChanged() {
|
||||||
|
if (modelData.isPlaying)
|
||||||
|
root._resolveActivePlayer();
|
||||||
|
root._syncStableMeta();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,9 +171,101 @@ Singleton {
|
|||||||
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
return player && player.playbackState === MprisPlaybackState.Stopped && !player.trackTitle && !player.trackArtist;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Known "<title> | <App>" suffixes stripped for matching only; display keeps the full title
|
||||||
|
readonly property var _appTitleSuffixes: ["youtube", "youtube music", "soundcloud", "spotify", "chrome", "chromium", "firefox", "brave", "vivaldi", "twitch"]
|
||||||
|
|
||||||
|
function _stripAppTitleSuffix(title: string): string {
|
||||||
|
const idx = title.lastIndexOf(" | ");
|
||||||
|
if (idx <= 0)
|
||||||
|
return title;
|
||||||
|
const suffix = title.substring(idx + 3).trim().toLowerCase();
|
||||||
|
return _appTitleSuffixes.indexOf(suffix) !== -1 ? title.substring(0, idx).trim() : title;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedTrackTitle(player: MprisPlayer): string {
|
||||||
|
return _stripAppTitleSuffix((player?.trackTitle || "").trim()).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayTrackTitle(player: MprisPlayer): string {
|
||||||
|
return (player?.trackTitle || "").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedTrackArtist(player: MprisPlayer): string {
|
||||||
|
return (player?.trackArtist || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Artist missing on either side: fall back to URL, then album, before trusting a title-only match
|
||||||
|
function isSameTrack(first: MprisPlayer, second: MprisPlayer): bool {
|
||||||
|
const firstTitle = normalizedTrackTitle(first);
|
||||||
|
if (!firstTitle || firstTitle !== normalizedTrackTitle(second))
|
||||||
|
return false;
|
||||||
|
const firstArtist = normalizedTrackArtist(first);
|
||||||
|
const secondArtist = normalizedTrackArtist(second);
|
||||||
|
if (firstArtist && secondArtist)
|
||||||
|
return firstArtist === secondArtist;
|
||||||
|
const firstUrl = (first?.metadata?.["xesam:url"] || "").toString();
|
||||||
|
const secondUrl = (second?.metadata?.["xesam:url"] || "").toString();
|
||||||
|
if (firstUrl && secondUrl)
|
||||||
|
return firstUrl === secondUrl;
|
||||||
|
const firstAlbum = (first?.trackAlbum || "").trim().toLowerCase();
|
||||||
|
const secondAlbum = (second?.trackAlbum || "").trim().toLowerCase();
|
||||||
|
if (firstAlbum && secondAlbum)
|
||||||
|
return firstAlbum === secondAlbum;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function metadataQuality(player: MprisPlayer): int {
|
||||||
|
if (!player)
|
||||||
|
return -1;
|
||||||
|
let quality = player.trackArtist ? 100 : 0;
|
||||||
|
quality += player.trackTitle ? 40 : 0;
|
||||||
|
quality += player.trackAlbum ? 20 : 0;
|
||||||
|
quality += player.trackArtUrl || player.metadata?.["mpris:artUrl"] ? 10 : 0;
|
||||||
|
quality += player.metadata?.["xesam:url"] ? 5 : 0;
|
||||||
|
return quality;
|
||||||
|
}
|
||||||
|
|
||||||
|
function equivalentPlayers(player: MprisPlayer): var {
|
||||||
|
if (!player)
|
||||||
|
return [];
|
||||||
|
return availablePlayers.filter(candidate => {
|
||||||
|
return candidate.playbackState !== MprisPlaybackState.Stopped && isSameTrack(player, candidate);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function bestMetadataPlayer(player: MprisPlayer): MprisPlayer {
|
||||||
|
const equivalents = equivalentPlayers(player);
|
||||||
|
if (equivalents.length === 0)
|
||||||
|
return player;
|
||||||
|
return equivalents.reduce((best, candidate) => {
|
||||||
|
return metadataQuality(candidate) > metadataQuality(best) ? candidate : best;
|
||||||
|
}, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _bestPlayingPlayer(): MprisPlayer {
|
||||||
|
const playing = availablePlayers.filter(player => player.isPlaying);
|
||||||
|
if (playing.length === 0)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
const controllable = playing.filter(player => player.canControl);
|
||||||
|
if (activePlayer?.isPlaying) {
|
||||||
|
if (activePlayer.canControl || controllable.length === 0)
|
||||||
|
return activePlayer;
|
||||||
|
// Playing but not controllable: only a same-track controllable peer may take over
|
||||||
|
const mirror = controllable.find(player => isSameTrack(activePlayer, player));
|
||||||
|
return mirror || activePlayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activePlayer?.canControl && activePlayer.playbackState === MprisPlaybackState.Paused) {
|
||||||
|
const onlyEquivalentMirrors = playing.every(player => isSameTrack(activePlayer, player));
|
||||||
|
if (onlyEquivalentMirrors)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return controllable[0] || playing[0];
|
||||||
|
}
|
||||||
|
|
||||||
function _resolveActivePlayer(): void {
|
function _resolveActivePlayer(): void {
|
||||||
// A playing player always wins; otherwise keep the selection stable w/idle
|
const playing = _bestPlayingPlayer();
|
||||||
const playing = availablePlayers.find(p => p.isPlaying);
|
|
||||||
if (playing) {
|
if (playing) {
|
||||||
if (activePlayer !== playing) {
|
if (activePlayer !== playing) {
|
||||||
activePlayer = playing;
|
activePlayer = playing;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ Singleton {
|
|||||||
property bool loginctlCommandAvailable: false
|
property bool loginctlCommandAvailable: false
|
||||||
property bool systemctlCommandAvailable: false
|
property bool systemctlCommandAvailable: false
|
||||||
property bool hibernateSupported: false
|
property bool hibernateSupported: false
|
||||||
|
readonly property bool softRebootSupported: systemctlCommandAvailable
|
||||||
property bool inhibitorAvailable: true
|
property bool inhibitorAvailable: true
|
||||||
property bool idleInhibited: false
|
property bool idleInhibited: false
|
||||||
property string inhibitReason: "Keep system awake"
|
property string inhibitReason: "Keep system awake"
|
||||||
@@ -217,6 +218,64 @@ Singleton {
|
|||||||
return envObj;
|
return envObj;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function splitShellArgs(str) {
|
||||||
|
const args = [];
|
||||||
|
let current = "";
|
||||||
|
let hasToken = false;
|
||||||
|
let quote = "";
|
||||||
|
let escaped = false;
|
||||||
|
for (const ch of str) {
|
||||||
|
if (escaped) {
|
||||||
|
current += ch;
|
||||||
|
escaped = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (quote) {
|
||||||
|
case "'":
|
||||||
|
if (ch === "'") {
|
||||||
|
quote = "";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
case "\"":
|
||||||
|
switch (ch) {
|
||||||
|
case "\"":
|
||||||
|
quote = "";
|
||||||
|
continue;
|
||||||
|
case "\\":
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (ch) {
|
||||||
|
case "\\":
|
||||||
|
escaped = true;
|
||||||
|
continue;
|
||||||
|
case "'":
|
||||||
|
case "\"":
|
||||||
|
quote = ch;
|
||||||
|
hasToken = true;
|
||||||
|
continue;
|
||||||
|
case " ":
|
||||||
|
case "\t":
|
||||||
|
case "\n":
|
||||||
|
if (!hasToken && current.length === 0)
|
||||||
|
continue;
|
||||||
|
args.push(current);
|
||||||
|
current = "";
|
||||||
|
hasToken = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
current += ch;
|
||||||
|
}
|
||||||
|
if (current.length > 0 || hasToken)
|
||||||
|
args.push(current);
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
function launchDesktopEntry(desktopEntry, useNvidia) {
|
function launchDesktopEntry(desktopEntry, useNvidia) {
|
||||||
if (!desktopEntry || !desktopEntry.command)
|
if (!desktopEntry || !desktopEntry.command)
|
||||||
return;
|
return;
|
||||||
@@ -230,8 +289,7 @@ Singleton {
|
|||||||
cmd = [nvidiaCommand].concat(cmd);
|
cmd = [nvidiaCommand].concat(cmd);
|
||||||
|
|
||||||
if (override?.extraFlags) {
|
if (override?.extraFlags) {
|
||||||
const extraArgs = override.extraFlags.trim().split(/\s+/).filter(arg => arg.length > 0);
|
cmd = cmd.concat(splitShellArgs(override.extraFlags));
|
||||||
cmd = cmd.concat(extraArgs);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
const userPrefix = SettingsData.launchPrefix?.trim() || "";
|
||||||
@@ -395,6 +453,10 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function softReboot() {
|
||||||
|
Quickshell.execDetached(["systemctl", "soft-reboot"]);
|
||||||
|
}
|
||||||
|
|
||||||
function poweroff() {
|
function poweroff() {
|
||||||
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
if (SettingsData.customPowerActionPowerOff.length === 0) {
|
||||||
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
Quickshell.execDetached(powerManagerCommand("poweroff"));
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ Singleton {
|
|||||||
return hash.toString(16).padStart(8, '0');
|
return hash.toString(16).padStart(8, '0');
|
||||||
}
|
}
|
||||||
|
|
||||||
function getArtworkUrl(player) {
|
function _directArtworkUrl(player) {
|
||||||
if (!player) return "";
|
if (!player) return "";
|
||||||
|
|
||||||
let artUrl = player.trackArtUrl || "";
|
let artUrl = player.trackArtUrl || "";
|
||||||
@@ -54,6 +54,17 @@ Singleton {
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getArtworkUrl(player) {
|
||||||
|
const directUrl = _directArtworkUrl(player);
|
||||||
|
if (directUrl !== "")
|
||||||
|
return directUrl;
|
||||||
|
|
||||||
|
const equivalent = MprisController.equivalentPlayers(player).find(candidate => {
|
||||||
|
return candidate !== player && _directArtworkUrl(candidate) !== "";
|
||||||
|
});
|
||||||
|
return _directArtworkUrl(equivalent);
|
||||||
|
}
|
||||||
|
|
||||||
function _commit(u, artKey, srcUrl) {
|
function _commit(u, artKey, srcUrl) {
|
||||||
resolvedArtUrl = u;
|
resolvedArtUrl = u;
|
||||||
_committedArtKey = u !== "" ? artKey : "";
|
_committedArtKey = u !== "" ? artKey : "";
|
||||||
@@ -171,12 +182,26 @@ Singleton {
|
|||||||
onActivePlayerChanged: _updateArtUrl()
|
onActivePlayerChanged: _updateArtUrl()
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: root.activePlayer
|
target: MprisController
|
||||||
|
function onAvailablePlayersChanged() {
|
||||||
|
root._updateArtUrl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Instantiator {
|
||||||
|
model: MprisController.availablePlayers
|
||||||
|
delegate: Connections {
|
||||||
|
required property MprisPlayer modelData
|
||||||
|
target: modelData
|
||||||
ignoreUnknownSignals: true
|
ignoreUnknownSignals: true
|
||||||
|
function onIsPlayingChanged() { root._updateArtUrl(); }
|
||||||
function onTrackTitleChanged() { root._updateArtUrl(); }
|
function onTrackTitleChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackArtistChanged() { root._updateArtUrl(); }
|
||||||
|
function onTrackAlbumChanged() { root._updateArtUrl(); }
|
||||||
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
function onTrackArtUrlChanged() { root._updateArtUrl(); }
|
||||||
function onMetadataChanged() { root._updateArtUrl(); }
|
function onMetadataChanged() { root._updateArtUrl(); }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function _trackKey() {
|
function _trackKey() {
|
||||||
const p = activePlayer;
|
const p = activePlayer;
|
||||||
@@ -201,8 +226,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
_pendingArtKey = key;
|
_pendingArtKey = key;
|
||||||
const url = getArtworkUrl(activePlayer);
|
const url = getArtworkUrl(activePlayer);
|
||||||
// Ignore Chrome's same-track thumbnail size updates.
|
// Ignore duplicate notifications, but let a richer peer replace same-track art
|
||||||
if (key !== "" && key === _committedArtKey)
|
if (key !== "" && key === _committedArtKey && url === _committedSrcUrl)
|
||||||
return;
|
return;
|
||||||
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
if (key !== "" && url !== "" && url === _committedSrcUrl) {
|
||||||
// Chrome can publish track metadata before its new artwork URL.
|
// Chrome can publish track metadata before its new artwork URL.
|
||||||
|
|||||||
Reference in New Issue
Block a user