1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-07 13:02:09 -04:00

Compare commits

..

9 Commits

Author SHA1 Message Date
purian23
1452c85946 Finalize m3 shadow & elevation 2026-03-01 00:43:46 -05:00
purian23
36951f82c6 Implement Auto-Bar location aware shadowing 2026-02-28 17:54:03 -05:00
purian23
1d25d18ca7 Refactor shadow levels to their own layer 2026-02-28 16:46:11 -05:00
purian23
771e59fcb8 Restore click to expand animation on notif cards 2026-02-28 16:09:52 -05:00
purian23
35593fa9d9 Fix some moar m3 changes w/shadows 2026-02-28 15:46:53 -05:00
purian23
88b7889447 Core m3 spec complete 2026-02-27 10:10:28 -05:00
purian23
3c5d8ba66a Moar M3 iteration stuffs 2026-02-26 23:02:49 -05:00
purian23
972bc8397d theme: Continue to iterate m3 specs 2026-02-26 16:18:59 -05:00
purian23
788c1fa9e6 feat: Implement Material Design 3 elevation and shadow effects
- Added global toggle for enabling M3 elevation & shadow effects
- Refactored various components to sync updated specs
2026-02-26 01:09:21 -05:00
110 changed files with 1251 additions and 6278 deletions

View File

@@ -28,12 +28,6 @@ packages:
outpkg: mocks_brightness
interfaces:
DBusConn:
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
config:
dir: "internal/mocks/geolocation"
outpkg: mocks_geolocation
interfaces:
Client:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"

View File

@@ -1,6 +1,6 @@
repos:
- repo: https://github.com/golangci/golangci-lint
rev: v2.10.1
rev: v2.9.0
hooks:
- id: golangci-lint-fmt
require_serial: true

View File

@@ -1,42 +0,0 @@
package geolocation
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
func NewClient() Client {
geoclueClient, err := newGeoClueClient()
if err != nil {
log.Warnf("GeoClue2 unavailable: %v", err)
return newSeededIpClient()
}
loc, _ := geoclueClient.GetLocation()
if loc.Latitude != 0 || loc.Longitude != 0 {
log.Info("Using GeoClue2 location")
return geoclueClient
}
log.Info("GeoClue2 has no fix yet, seeding with IP location")
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location seed failed: %v", err)
return geoclueClient
}
log.Info("Seeded GeoClue2 with IP location")
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
return geoclueClient
}
func newSeededIpClient() *IpClient {
client := newIpClient()
ipLoc, err := fetchIPLocation()
if err != nil {
log.Warnf("IP location also failed: %v", err)
return client
}
log.Info("Using IP location")
client.currLocation.Latitude = ipLoc.Latitude
client.currLocation.Longitude = ipLoc.Longitude
return client
}

View File

@@ -1,243 +0,0 @@
package geolocation
import (
"fmt"
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
)
const (
dbusGeoClueService = "org.freedesktop.GeoClue2"
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
dbusGeoClueInterface = dbusGeoClueService
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
)
type GeoClueClient struct {
currLocation *Location
locationMutex sync.RWMutex
dbusConn *dbus.Conn
clientPath dbus.ObjectPath
signals chan *dbus.Signal
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan Location]
}
func newGeoClueClient() (*GeoClueClient, error) {
dbusConn, err := dbus.ConnectSystemBus()
if err != nil {
return nil, fmt.Errorf("system bus connection failed: %w", err)
}
c := &GeoClueClient{
dbusConn: dbusConn,
stopChan: make(chan struct{}),
signals: make(chan *dbus.Signal, 256),
currLocation: &Location{
Latitude: 0.0,
Longitude: 0.0,
},
}
if err := c.setupClient(); err != nil {
dbusConn.Close()
return nil, err
}
if err := c.startSignalPump(); err != nil {
return nil, err
}
return c, nil
}
func (c *GeoClueClient) Close() {
close(c.stopChan)
c.sigWG.Wait()
if c.signals != nil {
c.dbusConn.RemoveSignal(c.signals)
close(c.signals)
}
c.subscribers.Range(func(key string, ch chan Location) bool {
close(ch)
c.subscribers.Delete(key)
return true
})
if c.dbusConn != nil {
c.dbusConn.Close()
}
}
func (c *GeoClueClient) Subscribe(id string) chan Location {
ch := make(chan Location, 64)
c.subscribers.Store(id, ch)
return ch
}
func (c *GeoClueClient) Unsubscribe(id string) {
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (c *GeoClueClient) setupClient() error {
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
}
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
return fmt.Errorf("failed to set desktop ID: %w", err)
}
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
return fmt.Errorf("failed to set time threshold: %w", err)
}
return nil
}
func (c *GeoClueClient) startSignalPump() error {
c.dbusConn.Signal(c.signals)
if err := c.dbusConn.AddMatchSignal(
dbus.WithMatchObjectPath(c.clientPath),
dbus.WithMatchInterface(dbusGeoClueClientInterface),
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
); err != nil {
return err
}
c.sigWG.Add(1)
go func() {
defer c.sigWG.Done()
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
clientObj.Call(dbusGeoClueClientTimeStart, 0)
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
for {
select {
case <-c.stopChan:
return
case sig, ok := <-c.signals:
if !ok {
return
}
if sig == nil {
continue
}
c.handleSignal(sig)
}
}
}()
return nil
}
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
switch sig.Name {
case dbusGeoClueClientLocationUpdated:
if len(sig.Body) != 2 {
return
}
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
if !ok {
return
}
if err := c.handleLocationUpdated(newLocationPath); err != nil {
log.Warn("GeoClue: Failed to handle location update: %v", err)
return
}
}
}
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
if err != nil {
return err
}
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
if err != nil {
return err
}
c.locationMutex.Lock()
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
c.locationMutex.Unlock()
c.notifySubscribers()
return nil
}
func (c *GeoClueClient) notifySubscribers() {
currentLocation, err := c.GetLocation()
if err != nil {
return
}
c.subscribers.Range(func(key string, ch chan Location) bool {
select {
case ch <- currentLocation:
default:
log.Warn("GeoClue: subscriber channel full, dropping update")
}
return true
})
}
func (c *GeoClueClient) SeedLocation(loc Location) {
c.locationMutex.Lock()
defer c.locationMutex.Unlock()
c.currLocation.Latitude = loc.Latitude
c.currLocation.Longitude = loc.Longitude
}
func (c *GeoClueClient) GetLocation() (Location, error) {
c.locationMutex.RLock()
defer c.locationMutex.RUnlock()
if c.currLocation == nil {
return Location{
Latitude: 0.0,
Longitude: 0.0,
}, nil
}
stateCopy := *c.currLocation
return stateCopy, nil
}

View File

@@ -1,91 +0,0 @@
package geolocation
import (
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
type IpClient struct {
currLocation *Location
}
type ipLocationResult struct {
Location
City string
}
type ipAPIResponse struct {
Status string `json:"status"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
City string `json:"city"`
}
func newIpClient() *IpClient {
return &IpClient{
currLocation: &Location{},
}
}
func (c *IpClient) Subscribe(id string) chan Location {
ch := make(chan Location, 1)
if location, err := c.GetLocation(); err == nil {
ch <- location
}
return ch
}
func (c *IpClient) Unsubscribe(id string) {}
func (c *IpClient) Close() {}
func (c *IpClient) GetLocation() (Location, error) {
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
return *c.currLocation, nil
}
result, err := fetchIPLocation()
if err != nil {
return Location{}, err
}
c.currLocation.Latitude = result.Latitude
c.currLocation.Longitude = result.Longitude
return *c.currLocation, nil
}
func fetchIPLocation() (ipLocationResult, error) {
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Get("http://ip-api.com/json/")
if err != nil {
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
}
var data ipAPIResponse
if err := json.Unmarshal(body, &data); err != nil {
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
}
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
}
return ipLocationResult{
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
City: data.City,
}, nil
}

View File

@@ -1,15 +0,0 @@
package geolocation
type Location struct {
Latitude float64
Longitude float64
}
type Client interface {
GetLocation() (Location, error)
Subscribe(id string) chan Location
Unsubscribe(id string)
Close()
}

View File

@@ -1,203 +0,0 @@
// Code generated by mockery v2.53.5. DO NOT EDIT.
package mocks_geolocation
import (
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
mock "github.com/stretchr/testify/mock"
)
// MockClient is an autogenerated mock type for the Client type
type MockClient struct {
mock.Mock
}
type MockClient_Expecter struct {
mock *mock.Mock
}
func (_m *MockClient) EXPECT() *MockClient_Expecter {
return &MockClient_Expecter{mock: &_m.Mock}
}
// Close provides a mock function with no fields
func (_m *MockClient) Close() {
_m.Called()
}
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
type MockClient_Close_Call struct {
*mock.Call
}
// Close is a helper method to define mock.On call
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
}
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
_c.Run(run)
return _c
}
// GetLocation provides a mock function with no fields
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetLocation")
}
var r0 geolocation.Location
var r1 error
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
return rf()
}
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
r0 = rf()
} else {
r0 = ret.Get(0).(geolocation.Location)
}
if rf, ok := ret.Get(1).(func() error); ok {
r1 = rf()
} else {
r1 = ret.Error(1)
}
return r0, r1
}
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
type MockClient_GetLocation_Call struct {
*mock.Call
}
// GetLocation is a helper method to define mock.On call
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
}
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
_c.Call.Return(_a0, _a1)
return _c
}
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
_c.Call.Return(run)
return _c
}
// Subscribe provides a mock function with given fields: id
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
ret := _m.Called(id)
if len(ret) == 0 {
panic("no return value specified for Subscribe")
}
var r0 chan geolocation.Location
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
r0 = rf(id)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(chan geolocation.Location)
}
}
return r0
}
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
type MockClient_Subscribe_Call struct {
*mock.Call
}
// Subscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
}
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
_c.Call.Return(run)
return _c
}
// Unsubscribe provides a mock function with given fields: id
func (_m *MockClient) Unsubscribe(id string) {
_m.Called(id)
}
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
type MockClient_Unsubscribe_Call struct {
*mock.Call
}
// Unsubscribe is a helper method to define mock.On call
// - id string
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
}
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
_c.Call.Return()
return _c
}
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
_c.Run(run)
return _c
}
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewMockClient(t interface {
mock.TestingT
Cleanup(func())
}) *MockClient {
mock := &MockClient{}
mock.Mock.Test(t)
t.Cleanup(func() { mock.AssertExpectations(t) })
return mock
}

View File

@@ -2,10 +2,8 @@ package cups
import (
"errors"
"fmt"
"net"
"net/url"
"os/exec"
"strings"
"time"
@@ -277,42 +275,13 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
return classes, nil
}
func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error {
args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd}
if information != "" {
args = append(args, "-D", information)
}
if location != "" {
args = append(args, "-L", location)
}
out, err := exec.Command("lpadmin", args...).CombinedOutput()
if err != nil {
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
func deletePrinterViaLpadmin(name string) error {
out, err := exec.Command("lpadmin", "-x", name).CombinedOutput()
if err != nil {
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
}
return nil
}
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
usedPkHelper := false
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
if isAuthError(err) && m.pkHelper != nil {
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort.
// lpadmin -E enables the printer, so no further setup needed.
if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil {
return err
}
m.RefreshState()
return nil
return err
}
usedPkHelper = true
} else if err != nil {
@@ -339,12 +308,6 @@ func (m *Manager) DeletePrinter(printerName string) error {
err := m.client.DeletePrinter(printerName)
if isAuthError(err) && m.pkHelper != nil {
err = m.pkHelper.PrinterDelete(printerName)
if err != nil {
// pkHelper failed, try lpadmin as last resort
if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil {
err = nil
}
}
}
if err == nil {
m.RefreshState()

View File

@@ -70,8 +70,6 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
handleRestartJob(conn, req, manager)
case "cups.holdJob":
handleHoldJob(conn, req, manager)
case "cups.testConnection":
handleTestConnection(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
@@ -466,22 +464,3 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
}
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
}
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
host, err := params.StringNonEmpty(req.Params, "host")
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
port := params.IntOpt(req.Params, "port", 631)
protocol := params.StringOpt(req.Params, "protocol", "ipp")
result, err := manager.TestRemotePrinter(host, port, protocol)
if err != nil {
models.RespondError(conn, req.ID, err.Error())
return
}
models.Respond(conn, req.ID, result)
}

View File

@@ -1,176 +0,0 @@
package cups
import (
"errors"
"fmt"
"net"
"strings"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
)
var validProtocols = map[string]bool{
"ipp": true,
"ipps": true,
"lpd": true,
"socket": true,
}
func validateTestConnectionParams(host string, port int, protocol string) error {
if host == "" {
return errors.New("host is required")
}
if strings.ContainsAny(host, " \t\n\r/\\") {
return errors.New("host contains invalid characters")
}
if port < 1 || port > 65535 {
return errors.New("port must be between 1 and 65535")
}
if protocol != "" && !validProtocols[protocol] {
return errors.New("protocol must be one of: ipp, ipps, lpd, socket")
}
return nil
}
const probeTimeout = 10 * time.Second
func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
// Fast fail: TCP reachability check
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
if err != nil {
return &RemotePrinterInfo{
Reachable: false,
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
}, nil
}
conn.Close()
// Create a temporary IPP client pointing at the remote host.
// The TCP dial above provides fast-fail for unreachable hosts.
// The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers.
client := ipp.NewIPPClient(host, port, "", "", useTLS)
// Try /ipp/print first (modern driverless printers), then / (legacy)
info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print")
if err != nil {
// If we got an auth error, the printer exists but requires credentials.
// Report it as reachable with the URI that triggered the auth challenge.
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
Info: "authentication required",
}, nil
}
info, err = probeIPPEndpoint(client, host, port, useTLS, "/")
}
if err != nil {
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/", proto, host, port),
Info: "authentication required",
}, nil
}
// TCP reachable but not an IPP printer
return &RemotePrinterInfo{
Reachable: true,
Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()),
}, nil
}
return info, nil
}
func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath)
httpProto := "http"
if useTLS {
httpProto = "https"
}
httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath)
req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1)
req.OperationAttributes[ipp.AttributePrinterURI] = printerURI
req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{
ipp.AttributePrinterName,
ipp.AttributePrinterMakeAndModel,
ipp.AttributePrinterState,
ipp.AttributePrinterInfo,
ipp.AttributePrinterUriSupported,
}
resp, err := client.SendRequest(httpURL, req, nil)
if err != nil {
return nil, err
}
if len(resp.PrinterAttributes) == 0 {
return nil, errors.New("no printer attributes returned")
}
attrs := resp.PrinterAttributes[0]
return &RemotePrinterInfo{
Reachable: true,
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
Name: getStringAttr(attrs, ipp.AttributePrinterName),
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
State: parsePrinterState(attrs),
URI: printerURI,
}, nil
}
// TestRemotePrinter validates inputs and probes a remote printer via IPP.
// For lpd/socket protocols, only TCP reachability is tested.
func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) {
if protocol == "" {
protocol = "ipp"
}
if err := validateTestConnectionParams(host, port, protocol); err != nil {
return nil, err
}
// For non-IPP protocols, only check TCP reachability
if protocol == "lpd" || protocol == "socket" {
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
if err != nil {
return &RemotePrinterInfo{
Reachable: false,
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
}, nil
}
conn.Close()
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d", protocol, host, port),
}, nil
}
useTLS := protocol == "ipps"
probeFn := m.probeRemoteFn
if probeFn == nil {
probeFn = probeRemotePrinter
}
return probeFn(host, port, useTLS)
}

View File

@@ -1,397 +0,0 @@
package cups
import (
"bytes"
"encoding/json"
"fmt"
"testing"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
"github.com/stretchr/testify/assert"
)
func TestValidateTestConnectionParams(t *testing.T) {
tests := []struct {
name string
host string
port int
protocol string
wantErr string
}{
{
name: "valid ipp",
host: "192.168.0.5",
port: 631,
protocol: "ipp",
wantErr: "",
},
{
name: "valid ipps",
host: "printer.local",
port: 443,
protocol: "ipps",
wantErr: "",
},
{
name: "valid lpd",
host: "10.0.0.1",
port: 515,
protocol: "lpd",
wantErr: "",
},
{
name: "valid socket",
host: "10.0.0.1",
port: 9100,
protocol: "socket",
wantErr: "",
},
{
name: "empty host",
host: "",
port: 631,
protocol: "ipp",
wantErr: "host is required",
},
{
name: "port too low",
host: "192.168.0.5",
port: 0,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "port too high",
host: "192.168.0.5",
port: 70000,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "invalid protocol",
host: "192.168.0.5",
port: 631,
protocol: "ftp",
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
},
{
name: "empty protocol treated as ipp",
host: "192.168.0.5",
port: 631,
protocol: "",
wantErr: "",
},
{
name: "host with slash",
host: "192.168.0.5/admin",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
{
name: "host with space",
host: "192.168.0.5 ",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
{
name: "host with newline",
host: "192.168.0.5\n",
port: 631,
protocol: "ipp",
wantErr: "host contains invalid characters",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateTestConnectionParams(tt.host, tt.port, tt.protocol)
if tt.wantErr == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, tt.wantErr)
}
})
}
}
func TestManager_TestRemotePrinter_Validation(t *testing.T) {
m := NewTestManager(nil, nil)
tests := []struct {
name string
host string
port int
protocol string
wantErr string
}{
{
name: "empty host returns error",
host: "",
port: 631,
protocol: "ipp",
wantErr: "host is required",
},
{
name: "invalid port returns error",
host: "192.168.0.5",
port: 0,
protocol: "ipp",
wantErr: "port must be between 1 and 65535",
},
{
name: "invalid protocol returns error",
host: "192.168.0.5",
port: 631,
protocol: "ftp",
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol)
assert.EqualError(t, err, tt.wantErr)
})
}
}
func TestManager_TestRemotePrinter_IPP(t *testing.T) {
tests := []struct {
name string
protocol string
probeRet *RemotePrinterInfo
probeErr error
wantTLS bool
wantReach bool
wantModel string
}{
{
name: "successful ipp probe",
protocol: "ipp",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
Name: "OfficeJet",
State: "idle",
URI: "ipp://192.168.0.5:631/ipp/print",
},
wantTLS: false,
wantReach: true,
wantModel: "HP OfficeJet 8010",
},
{
name: "successful ipps probe",
protocol: "ipps",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
URI: "ipps://192.168.0.5:631/ipp/print",
},
wantTLS: true,
wantReach: true,
wantModel: "HP OfficeJet 8010",
},
{
name: "unreachable host",
protocol: "ipp",
probeRet: &RemotePrinterInfo{
Reachable: false,
Error: "cannot reach 192.168.0.5:631: connection refused",
},
wantReach: false,
},
{
name: "empty protocol defaults to ipp",
protocol: "",
probeRet: &RemotePrinterInfo{
Reachable: true,
MakeModel: "Test Printer",
},
wantTLS: false,
wantReach: true,
wantModel: "Test Printer",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var capturedTLS bool
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
capturedTLS = useTLS
return tt.probeRet, tt.probeErr
}
result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol)
if tt.probeErr != nil {
assert.Error(t, err)
return
}
assert.NoError(t, err)
assert.Equal(t, tt.wantReach, result.Reachable)
assert.Equal(t, tt.wantModel, result.MakeModel)
assert.Equal(t, tt.wantTLS, capturedTLS)
})
}
}
func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
// Simulate what happens when the printer returns HTTP 401
return probeRemotePrinterWithAuthError(host, port, useTLS)
}
result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp")
assert.NoError(t, err)
assert.True(t, result.Reachable)
assert.Equal(t, "authentication required", result.Info)
assert.Contains(t, result.URI, "ipp://192.168.0.107:631")
}
// probeRemotePrinterWithAuthError simulates a probe where the printer
// returns HTTP 401 on both endpoints.
func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
// This simulates what probeRemotePrinter does when both endpoints
// return auth errors. We test the auth detection logic directly.
err := ipp.HTTPError{Code: 401}
if isAuthError(err) {
proto := "ipp"
if useTLS {
proto = "ipps"
}
return &RemotePrinterInfo{
Reachable: true,
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
Info: "authentication required",
}, nil
}
return nil, err
}
func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) {
m := NewTestManager(nil, nil)
probeCalled := false
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
probeCalled = true
return nil, nil
}
// These will fail at TCP dial (no real server), but the important
// thing is that probeRemoteFn is NOT called for lpd/socket.
m.TestRemotePrinter("192.168.0.5", 9100, "socket")
assert.False(t, probeCalled, "probe function should not be called for socket protocol")
m.TestRemotePrinter("192.168.0.5", 515, "lpd")
assert.False(t, probeCalled, "probe function should not be called for lpd protocol")
}
func TestHandleTestConnection_Success(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
return &RemotePrinterInfo{
Reachable: true,
MakeModel: "HP OfficeJet 8010",
Name: "OfficeJet",
State: "idle",
URI: "ipp://192.168.0.5:631/ipp/print",
}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{
"host": "192.168.0.5",
},
}
handleTestConnection(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel)
}
func TestHandleTestConnection_MissingHost(t *testing.T) {
m := NewTestManager(nil, nil)
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{},
}
handleTestConnection(conn, req, m)
var resp models.Response[any]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.Nil(t, resp.Result)
assert.NotNil(t, resp.Error)
}
func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
assert.Equal(t, 9631, port)
assert.True(t, useTLS)
return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{
"host": "192.168.0.5",
"port": float64(9631),
"protocol": "ipps",
},
}
handleTestConnection(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
}
func TestHandleRequest_TestConnection(t *testing.T) {
m := NewTestManager(nil, nil)
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
return &RemotePrinterInfo{Reachable: true}, nil
}
buf := &bytes.Buffer{}
conn := &mockConn{Buffer: buf}
req := models.Request{
ID: 1,
Method: "cups.testConnection",
Params: map[string]any{"host": "192.168.0.5"},
}
HandleRequest(conn, req, m)
var resp models.Response[RemotePrinterInfo]
err := json.NewDecoder(buf).Decode(&resp)
assert.NoError(t, err)
assert.NotNil(t, resp.Result)
assert.True(t, resp.Result.Reachable)
}

View File

@@ -55,16 +55,6 @@ type PPD struct {
Type string `json:"type"`
}
type RemotePrinterInfo struct {
Reachable bool `json:"reachable"`
MakeModel string `json:"makeModel"`
Name string `json:"name"`
Info string `json:"info"`
State string `json:"state"`
URI string `json:"uri"`
Error string `json:"error,omitempty"`
}
type PrinterClass struct {
Name string `json:"name"`
URI string `json:"uri"`
@@ -87,7 +77,6 @@ type Manager struct {
notifierWg sync.WaitGroup
lastNotifiedState *CUPSState
baseURL string
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
}
type SubscriptionManagerInterface interface {

View File

@@ -1,61 +0,0 @@
package location
import (
"encoding/json"
"fmt"
"net"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
)
type LocationEvent struct {
Type string `json:"type"`
Data State `json:"data"`
}
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
switch req.Method {
case "location.getState":
handleGetState(conn, req, manager)
case "location.subscribe":
handleSubscribe(conn, req, manager)
default:
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
}
}
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
models.Respond(conn, req.ID, manager.GetState())
}
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
clientID := fmt.Sprintf("client-%p", conn)
stateChan := manager.Subscribe(clientID)
defer manager.Unsubscribe(clientID)
initialState := manager.GetState()
event := LocationEvent{
Type: "state_changed",
Data: initialState,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
ID: req.ID,
Result: &event,
}); err != nil {
return
}
for state := range stateChan {
event := LocationEvent{
Type: "state_changed",
Data: state,
}
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
Result: &event,
}); err != nil {
return
}
}
}

View File

@@ -1,175 +0,0 @@
package location
import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
)
func NewManager(client geolocation.Client) (*Manager, error) {
currLocation, err := client.GetLocation()
if err != nil {
log.Warnf("Failed to get initial location: %v", err)
}
m := &Manager{
client: client,
dirty: make(chan struct{}),
stopChan: make(chan struct{}),
state: &State{
Latitude: currLocation.Latitude,
Longitude: currLocation.Longitude,
},
}
if err := m.startSignalPump(); err != nil {
return nil, err
}
m.notifierWg.Add(1)
go m.notifier()
return m, nil
}
func (m *Manager) Close() {
close(m.stopChan)
m.notifierWg.Wait()
m.sigWG.Wait()
m.subscribers.Range(func(key string, ch chan State) bool {
close(ch)
m.subscribers.Delete(key)
return true
})
}
func (m *Manager) Subscribe(id string) chan State {
ch := make(chan State, 64)
m.subscribers.Store(id, ch)
return ch
}
func (m *Manager) Unsubscribe(id string) {
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
close(ch)
}
}
func (m *Manager) startSignalPump() error {
m.sigWG.Add(1)
go func() {
defer m.sigWG.Done()
subscription := m.client.Subscribe("locationManager")
defer m.client.Unsubscribe("locationManager")
for {
select {
case <-m.stopChan:
return
case location, ok := <-subscription:
if !ok {
return
}
m.handleLocationChange(location)
}
}
}()
return nil
}
func (m *Manager) handleLocationChange(location geolocation.Location) {
m.stateMutex.Lock()
defer m.stateMutex.Unlock()
m.state.Latitude = location.Latitude
m.state.Longitude = location.Longitude
m.notifySubscribers()
}
func (m *Manager) notifySubscribers() {
select {
case m.dirty <- struct{}{}:
default:
}
}
func (m *Manager) GetState() State {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
if m.state == nil {
return State{
Latitude: 0.0,
Longitude: 0.0,
}
}
stateCopy := *m.state
return stateCopy
}
func (m *Manager) notifier() {
defer m.notifierWg.Done()
const minGap = 200 * time.Millisecond
timer := time.NewTimer(minGap)
timer.Stop()
var pending bool
for {
select {
case <-m.stopChan:
timer.Stop()
return
case <-m.dirty:
if pending {
continue
}
pending = true
timer.Reset(minGap)
case <-timer.C:
if !pending {
continue
}
currentState := m.GetState()
if m.lastNotified != nil && !stateChanged(m.lastNotified, &currentState) {
pending = false
continue
}
m.subscribers.Range(func(key string, ch chan State) bool {
select {
case ch <- currentState:
default:
log.Warn("Location: subscriber channel full, dropping update")
}
return true
})
stateCopy := currentState
m.lastNotified = &stateCopy
pending = false
}
}
}
func stateChanged(old, new *State) bool {
if old == nil || new == nil {
return true
}
if old.Latitude != new.Latitude {
return true
}
if old.Longitude != new.Longitude {
return true
}
return false
}

View File

@@ -1,28 +0,0 @@
package location
import (
"sync"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
type State struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
type Manager struct {
state *State
stateMutex sync.RWMutex
client geolocation.Client
stopChan chan struct{}
sigWG sync.WaitGroup
subscribers syncmap.Map[string, chan State]
dirty chan struct{}
notifierWg sync.WaitGroup
lastNotified *State
}

View File

@@ -5,6 +5,5 @@ const (
dbusPath = "/org/freedesktop/login1"
dbusManagerInterface = "org.freedesktop.login1.Manager"
dbusSessionInterface = "org.freedesktop.login1.Session"
dbusUserInterface = "org.freedesktop.login1.User"
dbusPropsInterface = "org.freedesktop.DBus.Properties"
)

View File

@@ -17,8 +17,15 @@ func NewManager() (*Manager, error) {
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
}
sessionID := os.Getenv("XDG_SESSION_ID")
if sessionID == "" {
sessionID = "self"
}
m := &Manager{
state: &SessionState{},
state: &SessionState{
SessionID: sessionID,
},
stateMutex: sync.RWMutex{},
stopChan: make(chan struct{}),
@@ -53,13 +60,12 @@ func (m *Manager) initialize() error {
m.initializeFallbackDelay()
sessionID, sessionPath, err := m.discoverSession()
sessionPath, err := m.getSession(m.state.SessionID)
if err != nil {
return fmt.Errorf("failed to get session path: %w", err)
}
m.stateMutex.Lock()
m.state.SessionID = sessionID
m.state.SessionPath = string(sessionPath)
m.sessionPath = sessionPath
m.stateMutex.Unlock()
@@ -73,41 +79,6 @@ func (m *Manager) initialize() error {
return nil
}
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
// 1. Explicit XDG_SESSION_ID
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
if path, err := m.getSession(id); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
return id, path, nil
}
}
// 2. PID-based lookup (works when caller is inside a session cgroup)
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
return id, path, nil
}
// 3. User's primary display session (handles UWSM and similar)
if id, path, err := m.getUserDisplaySession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
return id, path, nil
}
// 4. Score all sessions for current UID
if id, path, err := m.findBestSession(); err == nil {
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
return id, path, nil
}
// 5. Last resort: "self"
path, err := m.getSession("self")
if err != nil {
return "", "", fmt.Errorf("%w", err)
}
return "self", path, nil
}
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
var out dbus.ObjectPath
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
@@ -117,166 +88,6 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
return out, nil
}
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
var path dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
return "", "", err
}
sessionObj := m.conn.Object(dbusDest, path)
var id dbus.Variant
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
return "", "", err
}
return id.Value().(string), path, nil
}
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
uid := uint32(os.Getuid())
var userPath dbus.ObjectPath
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
return "", "", err
}
userObj := m.conn.Object(dbusDest, userPath)
var display dbus.Variant
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
return "", "", err
}
pair, ok := display.Value().([]any)
if !ok || len(pair) < 2 {
return "", "", fmt.Errorf("unexpected Display format")
}
sessionID, _ := pair[0].(string)
sessionPath, _ := pair[1].(dbus.ObjectPath)
if sessionID == "" || sessionPath == "" {
return "", "", fmt.Errorf("empty Display session")
}
return sessionID, sessionPath, nil
}
type sessionCandidate struct {
id string
path dbus.ObjectPath
}
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
var raw [][]any
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
return "", "", err
}
uid := uint32(os.Getuid())
var candidates []sessionCandidate
for _, entry := range raw {
if len(entry) < 5 {
continue
}
entryUID, _ := entry[1].(uint32)
if entryUID != uid {
continue
}
id, _ := entry[0].(string)
path, _ := entry[4].(dbus.ObjectPath)
if id != "" && path != "" {
candidates = append(candidates, sessionCandidate{id: id, path: path})
}
}
if len(candidates) == 0 {
return "", "", fmt.Errorf("no sessions for uid %d", uid)
}
bestScore := -1
var best sessionCandidate
for _, c := range candidates {
score := m.scoreSession(c.path)
if score > bestScore {
bestScore = score
best = c
}
}
if bestScore < 0 {
return "", "", fmt.Errorf("no viable session found")
}
return best.id, best.path, nil
}
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
obj := m.conn.Object(dbusDest, path)
var props map[string]dbus.Variant
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
return -1
}
getStr := func(key string) string {
if v, ok := props[key]; ok {
if s, ok := v.Value().(string); ok {
return s
}
}
return ""
}
getBool := func(key string) bool {
if v, ok := props[key]; ok {
if b, ok := v.Value().(bool); ok {
return b
}
}
return false
}
getUint32 := func(key string) uint32 {
if v, ok := props[key]; ok {
if u, ok := v.Value().(uint32); ok {
return u
}
}
return 0
}
class := getStr("Class")
if class != "user" {
return -1
}
if getBool("Remote") {
return -1
}
score := 0
if getBool("Active") {
score += 100
}
switch getStr("Type") {
case "wayland", "x11":
score += 80
case "tty":
score += 10
}
if v, ok := props["Seat"]; ok {
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
if seat, ok := seatArr[0].(string); ok && seat != "" {
score += 40
if seat == "seat0" {
score += 10
}
}
}
}
if getUint32("VTNr") > 0 {
score += 20
}
return score
}
func (m *Manager) refreshSessionBinding() error {
if m.managerObj == nil || m.conn == nil {
return fmt.Errorf("manager not fully initialized")

View File

@@ -15,7 +15,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -193,15 +192,6 @@ func RouteRequest(conn net.Conn, req models.Request) {
return
}
if strings.HasPrefix(req.Method, "location.") {
if locationManager == nil {
models.RespondError(conn, req.ID, "location manager not initialized")
return
}
location.HandleRequest(conn, req, locationManager)
return
}
switch req.Method {
case "ping":
models.Respond(conn, req.ID, "pong")

View File

@@ -14,7 +14,6 @@ import (
"syscall"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
@@ -26,7 +25,6 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
@@ -72,7 +70,6 @@ var clipboardManager *clipboard.Manager
var dbusManager *serverDbus.Manager
var wlContext *wlcontext.SharedContext
var themeModeManager *thememode.Manager
var locationManager *location.Manager
const dbusClientID = "dms-dbus-client"
@@ -191,7 +188,7 @@ func InitializeFreedeskManager() error {
return nil
}
func InitializeWaylandManager(geoClient geolocation.Client) error {
func InitializeWaylandManager() error {
log.Info("Attempting to initialize Wayland gamma control...")
if wlContext == nil {
@@ -204,7 +201,7 @@ func InitializeWaylandManager(geoClient geolocation.Client) error {
}
config := wayland.DefaultConfig()
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
manager, err := wayland.NewManager(wlContext.Display(), config)
if err != nil {
log.Errorf("Failed to initialize wayland manager: %v", err)
return err
@@ -385,27 +382,14 @@ func InitializeDbusManager() error {
return nil
}
func InitializeThemeModeManager(geoClient geolocation.Client) error {
manager := thememode.NewManager(geoClient)
func InitializeThemeModeManager() error {
manager := thememode.NewManager()
themeModeManager = manager
log.Info("Theme mode automation manager initialized")
return nil
}
func InitializeLocationManager(geoClient geolocation.Client) error {
manager, err := location.NewManager(geoClient)
if err != nil {
log.Warnf("Failed to initialize location manager: %v", err)
return err
}
locationManager = manager
log.Info("Location manager initialized")
return nil
}
func handleConnection(conn net.Conn) {
defer conn.Close()
@@ -553,10 +537,6 @@ func getServerInfo() ServerInfo {
caps = append(caps, "theme.auto")
}
if locationManager != nil {
caps = append(caps, "location")
}
if dbusManager != nil {
caps = append(caps, "dbus")
}
@@ -1327,9 +1307,6 @@ func cleanupManagers() {
if wlContext != nil {
wlContext.Close()
}
if locationManager != nil {
locationManager.Close()
}
}
func Start(printDocs bool) error {
@@ -1511,9 +1488,6 @@ func Start(printDocs bool) error {
log.Info(" clipboard.getConfig - Get clipboard configuration")
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
log.Info("Location:")
log.Info(" location.getState - Get current location state")
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
log.Info("")
}
log.Info("Initializing managers...")
@@ -1545,9 +1519,6 @@ func Start(printDocs bool) error {
loginctlReady := make(chan struct{})
freedesktopReady := make(chan struct{})
geoClient := geolocation.NewClient()
defer geoClient.Close()
go func() {
defer close(loginctlReady)
if err := InitializeLoginctlManager(); err != nil {
@@ -1592,7 +1563,7 @@ func Start(printDocs bool) error {
}
}()
if err := InitializeWaylandManager(geoClient); err != nil {
if err := InitializeWaylandManager(); err != nil {
log.Warnf("Wayland manager unavailable: %v", err)
}
@@ -1624,7 +1595,7 @@ func Start(printDocs bool) error {
log.Debugf("WlrOutput manager unavailable: %v", err)
}
if err := InitializeThemeModeManager(geoClient); err != nil {
if err := InitializeThemeModeManager(); err != nil {
log.Warnf("Theme mode manager unavailable: %v", err)
} else {
notifyCapabilityChange()
@@ -1637,12 +1608,6 @@ func Start(printDocs bool) error {
}()
}
if err := InitializeLocationManager(geoClient); err != nil {
log.Warnf("Location manager unavailable: %v", err)
} else {
notifyCapabilityChange()
}
fatalErrChan := make(chan error, 1)
if wlrOutputManager != nil {
go func() {

View File

@@ -5,7 +5,6 @@ import (
"sync"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
@@ -33,14 +32,12 @@ type Manager struct {
cachedIPLat *float64
cachedIPLon *float64
geoClient geolocation.Client
stopChan chan struct{}
updateTrigger chan struct{}
wg sync.WaitGroup
}
func NewManager(geoClient geolocation.Client) *Manager {
func NewManager() *Manager {
m := &Manager{
config: Config{
Enabled: false,
@@ -54,7 +51,6 @@ func NewManager(geoClient geolocation.Client) *Manager {
},
stopChan: make(chan struct{}),
updateTrigger: make(chan struct{}, 1),
geoClient: geoClient,
}
m.updateState(time.Now())
@@ -331,17 +327,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation()
lat, lon, err := wayland.FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
return lat, lon
}
func statesEqual(a, b *State) bool {

View File

@@ -13,14 +13,13 @@ import (
"golang.org/x/sys/unix"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
)
const animKelvinStep = 25
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
if err := config.Validate(); err != nil {
return nil, err
}
@@ -41,7 +40,6 @@ func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, c
updateTrigger: make(chan struct{}, 1),
dirty: make(chan struct{}, 1),
dbusSignal: make(chan *dbus.Signal, 16),
geoClient: geoClient,
}
if err := m.setupRegistry(); err != nil {
@@ -439,16 +437,15 @@ func (m *Manager) getLocation() (*float64, *float64) {
}
m.locationMutex.RUnlock()
location, err := m.geoClient.GetLocation()
lat, lon, err := FetchIPLocation()
if err != nil {
return nil, nil
}
m.locationMutex.Lock()
m.cachedIPLat = &location.Latitude
m.cachedIPLon = &location.Longitude
m.cachedIPLat = lat
m.cachedIPLon = lon
m.locationMutex.Unlock()
return m.cachedIPLat, m.cachedIPLon
return lat, lon
}
return nil, nil
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/stretchr/testify/assert"
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
)
@@ -391,20 +390,18 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
func TestNewManager_GetRegistryError(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
mockDisplay.EXPECT().Context().Return(nil)
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
config := DefaultConfig()
_, err := NewManager(mockDisplay, mockGeoclient, config)
_, err := NewManager(mockDisplay, config)
assert.Error(t, err)
assert.Contains(t, err.Error(), "get registry")
}
func TestNewManager_InvalidConfig(t *testing.T) {
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
mockGeoclient := mocks_geolocation.NewMockClient(t)
config := Config{
LowTemp: 500,
@@ -412,6 +409,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
Gamma: 1.0,
}
_, err := NewManager(mockDisplay, mockGeoclient, config)
_, err := NewManager(mockDisplay, config)
assert.Error(t, err)
}

View File

@@ -6,7 +6,6 @@ import (
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
"github.com/godbus/dbus/v5"
@@ -98,8 +97,6 @@ type Manager struct {
dbusConn *dbus.Conn
dbusSignal chan *dbus.Signal
geoClient geolocation.Client
lastAppliedTemp int
lastAppliedGamma float64
}

View File

@@ -2,6 +2,7 @@
config,
lib,
pkgs,
dmsPkgs,
...
}:
let
@@ -9,7 +10,7 @@ let
in
{
packages = [
cfg.package
dmsPkgs.dms-shell
]
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
++ lib.optionals cfg.enableVPN [

View File

@@ -8,7 +8,6 @@
let
inherit (lib) types;
cfg = config.programs.dank-material-shell.greeter;
cfgDms = config.programs.dank-material-shell;
inherit (config.services.greetd.settings.default_session) user;
@@ -30,13 +29,13 @@ let
lib.escapeShellArgs (
[
"sh"
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
"--cache-dir"
cacheDir
"--command"
cfg.compositor.name
"-p"
"${cfg.package}/share/quickshell/dms"
"${dmsPkgs.dms-shell}/share/quickshell/dms"
]
++ lib.optionals (cfg.compositor.customConfig != "") [
"-C"
@@ -66,21 +65,6 @@ in
options.programs.dank-material-shell.greeter = {
enable = lib.mkEnableOption "DankMaterialShell greeter";
package = lib.mkOption {
type = types.package;
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
defaultText = lib.literalExpression ''
if config.programs.dank-material-shell.enable
then config.programs.dank-material-shell.package
else built from source;
'';
description = ''
The DankMaterialShell package to use for the greeter.
Defaults to the package from `programs.dank-material-shell` if it is enabled,
otherwise defaults to building from source.
'';
};
compositor.name = lib.mkOption {
type = types.enum [
"niri"

View File

@@ -2,6 +2,7 @@
config,
pkgs,
lib,
dmsPkgs,
...
}@args:
let
@@ -12,6 +13,7 @@ let
config
pkgs
lib
dmsPkgs
;
};
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
@@ -94,7 +96,7 @@ in
};
Service = {
ExecStart = lib.getExe cfg.package + " run --session";
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
Restart = "on-failure";
};

View File

@@ -2,6 +2,7 @@
config,
pkgs,
lib,
dmsPkgs,
...
}@args:
let
@@ -11,6 +12,7 @@ let
config
pkgs
lib
dmsPkgs
;
};
in
@@ -34,7 +36,7 @@ in
restartIfChanged = cfg.systemd.restartIfChanged;
serviceConfig = {
ExecStart = lib.getExe cfg.package + " run --session";
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
Restart = "on-failure";
};
};
@@ -48,7 +50,6 @@ in
services.power-profiles-daemon.enable = lib.mkDefault true;
services.accounts-daemon.enable = lib.mkDefault true;
services.geoclue2.enable = lib.mkDefault true;
security.polkit.enable = lib.mkDefault true;
};
}

View File

@@ -26,9 +26,6 @@ in
options.programs.dank-material-shell = {
enable = lib.mkEnableOption "DankMaterialShell";
package = lib.mkPackageOption dmsPkgs "dms-shell" {
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
};
systemd = {
enable = lib.mkEnableOption "DankMaterialShell systemd startup";

View File

@@ -1,174 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
// AnimVariants — Central tuning for animation and Motion Effects variants
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
Singleton {
id: root
readonly property list<real> variantEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standardDecel;
case 2:
return Anims.expressiveFastSpatial;
default:
return Anims.expressiveDefaultSpatial;
}
}
readonly property list<real> variantExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
switch (SettingsData.animationVariant) {
case 1:
return Anims.standard;
case 2:
return Anims.emphasized;
default:
return Anims.emphasized;
}
}
// Modal-specific entry curve
readonly property list<real> variantModalEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
}
return variantEnterCurve;
}
readonly property list<real> variantModalExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
// Popout-specific entry curve
readonly property list<real> variantPopoutEnterCurve: {
if (typeof SettingsData === "undefined")
return Anims.expressiveDefaultSpatial;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.standardDecel;
if (SettingsData.animationVariant === 2)
return Anims.expressiveFastSpatial;
return Anims.standardDecel;
}
return variantEnterCurve;
}
readonly property list<real> variantPopoutExitCurve: {
if (typeof SettingsData === "undefined")
return Anims.emphasized;
if (isDirectionalEffect) {
if (SettingsData.animationVariant === 1)
return Anims.emphasizedAccel;
if (SettingsData.animationVariant === 2)
return Anims.emphasizedAccel;
}
return variantExitCurve;
}
readonly property real variantEnterDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.9;
case 2:
return 1.08;
default:
return 1.0;
}
}
readonly property real variantExitDurationFactor: {
if (typeof SettingsData === "undefined")
return 1.0;
switch (SettingsData.animationVariant) {
case 1:
return 0.85;
case 2:
return 0.92;
default:
return 1.0;
}
}
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
readonly property real variantOpacityDurationScale: {
if (typeof SettingsData === "undefined")
return 1.0;
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
}
function variantDuration(baseDuration, entering) {
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
return Math.max(0, Math.round(baseDuration * factor));
}
function variantExitCleanupPadding() {
if (typeof SettingsData === "undefined")
return 50;
switch (SettingsData.motionEffect) {
case 1:
return 8;
case 2:
return 24;
default:
return 50;
}
}
function variantCloseInterval(baseDuration) {
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
}
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
readonly property real effectScaleCollapsed: {
if (typeof SettingsData === "undefined")
return 0.96;
switch (SettingsData.motionEffect) {
case 1:
return 1.0;
case 2:
return 0.88;
default:
return 0.96;
}
}
readonly property real effectAnimOffset: {
if (typeof SettingsData === "undefined")
return 16;
switch (SettingsData.motionEffect) {
case 1:
return 144;
case 2:
return 56;
default:
return 16;
}
}
}

View File

@@ -22,9 +22,4 @@ Singleton {
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
// Used by AnimVariants for variant/effect logic
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
}

View File

@@ -1,6 +1,5 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
@@ -9,9 +8,7 @@ import Quickshell.Io
Singleton {
id: root
property string _resolvedLocale: "en"
readonly property string _rawLocale: SessionData.locale === "" ? Qt.locale().name : SessionData.locale
readonly property string _rawLocale: Qt.locale().name
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
readonly property var _candidates: {
const fullUnderscore = _rawLocale;
@@ -24,10 +21,7 @@ Singleton {
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
readonly property alias folder: dir.folder
property var presentLocales: ({
"en": Qt.locale("en")
})
property string currentLocale: "en"
property var translations: ({})
property bool translationsLoaded: false
@@ -40,10 +34,8 @@ Singleton {
showDirs: false
showDotAndDotDot: false
onStatusChanged: if (status === FolderListModel.Ready) {
root._loadPresentLocales();
root._pickTranslation();
}
onStatusChanged: if (status === FolderListModel.Ready)
root._pickTranslation()
}
FileView {
@@ -54,54 +46,41 @@ Singleton {
try {
root.translations = JSON.parse(text());
root.translationsLoaded = true;
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
console.info(`I18n: Loaded translations for '${root.currentLocale}' ` + `(${Object.keys(root.translations).length} contexts)`);
} catch (e) {
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e, "- falling back to English");
root._fallbackToEnglish();
}
}
onLoadFailed: error => {
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` + "falling back to English");
root._fallbackToEnglish();
}
}
function locale() {
if (SessionData.timeLocale)
return Qt.locale(SessionData.timeLocale);
return Qt.locale();
}
function _loadPresentLocales() {
if (Object.keys(presentLocales).length > 1) {
return; // already loaded
}
function _pickTranslation() {
const present = new Set();
for (let i = 0; i < dir.count; i++) {
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
if (name && name.endsWith(".json")) {
const shortName = name.slice(0, -5);
presentLocales[shortName] = Qt.locale(shortName);
present.add(name.slice(0, -5));
}
}
}
function _pickTranslation() {
for (let i = 0; i < _candidates.length; i++) {
const cand = _candidates[i];
if (presentLocales[cand] === undefined)
continue;
_resolvedLocale = cand;
useLocale(cand, cand.startsWith("en") ? "" : translationsFolder + "/" + cand + ".json");
return;
if (present.has(cand)) {
_useLocale(cand, dir.folder + "/" + cand + ".json");
return;
}
}
_resolvedLocale = "en";
_fallbackToEnglish();
}
function useLocale(localeTag, fileUrl) {
_resolvedLocale = localeTag || "en";
function _useLocale(localeTag, fileUrl) {
currentLocale = localeTag;
_selectedPath = fileUrl;
translationsLoaded = false;
translations = ({});
@@ -109,6 +88,7 @@ Singleton {
}
function _fallbackToEnglish() {
currentLocale = "en";
_selectedPath = "";
translationsLoaded = false;
translations = ({});

View File

@@ -71,40 +71,15 @@ Singleton {
return appId;
}
function resolveIconPath(iconName: string): string {
if (!iconName) return "";
const moddedId = moddedAppId(iconName);
if (moddedId !== iconName) {
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
return toFileUrl(expandTilde(moddedId));
if (moddedId.startsWith("file://"))
return moddedId;
return Quickshell.iconPath(moddedId, true);
}
return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
}
function resolveIconUrl(iconName: string): string {
if (!iconName) return "";
const moddedId = moddedAppId(iconName);
if (moddedId !== iconName) {
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
return toFileUrl(expandTilde(moddedId));
if (moddedId.startsWith("file://"))
return moddedId;
return "image://icon/" + moddedId;
}
return "image://icon/" + iconName;
}
function getAppIcon(appId: string, desktopEntry: var): string {
if (appId === "org.quickshell") {
return Qt.resolvedUrl("../assets/danklogo.svg");
}
const moddedId = moddedAppId(appId);
if (moddedId !== appId)
return resolveIconPath(appId);
if (moddedId !== appId) {
return Quickshell.iconPath(moddedId, true);
}
if (desktopEntry && desktopEntry.icon) {
return Quickshell.iconPath(desktopEntry.icon, true);

View File

@@ -21,9 +21,7 @@ Singleton {
property bool _isReadOnly: false
property bool _hasUnsavedChanges: false
property var _loadedSessionSnapshot: null
readonly property var _hooks: ({
"updateLocale": updateLocale
})
readonly property var _hooks: ({})
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
readonly property string _stateDir: Paths.strip(_stateUrl)
@@ -128,9 +126,6 @@ Singleton {
property var hiddenOutputDeviceNames: []
property var hiddenInputDeviceNames: []
property string locale: ""
property string timeLocale: ""
property string launcherLastMode: "all"
property string appDrawerLastMode: "apps"
property string niriOverviewLastMode: "apps"
@@ -1109,14 +1104,6 @@ Singleton {
saveSettings();
}
function updateLocale() {
if (!locale) {
I18n._pickTranslation();
return;
}
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
}
function setLauncherLastMode(mode) {
launcherLastMode = mode;
saveSettings();

View File

@@ -37,18 +37,6 @@ Singleton {
Custom
}
enum AnimationVariant {
Material,
Fluent,
Dynamic
}
enum AnimationEffect {
Standard, // 0 — M3: scale-in, rises from below
Directional, // 1 — pure large slide, no scale
Depth // 2 — medium slide with deep depth scale pop
}
enum SuspendBehavior {
Suspend,
Hibernate,
@@ -161,7 +149,6 @@ Singleton {
property int mangoLayoutRadiusOverride: -1
property int mangoLayoutBorderSize: -1
property int firstDayOfWeek: -1
property bool use24HourClock: true
property bool showSeconds: false
property bool padHours12Hour: false
@@ -178,12 +165,6 @@ Singleton {
property int modalCustomAnimationDuration: 150
property bool enableRippleEffects: true
onEnableRippleEffectsChanged: saveSettings()
property int animationVariant: SettingsData.AnimationVariant.Material
onAnimationVariantChanged: saveSettings()
property int motionEffect: SettingsData.AnimationEffect.Standard
onMotionEffectChanged: saveSettings()
property int directionalAnimationMode: 0
onDirectionalAnimationModeChanged: saveSettings()
property bool m3ElevationEnabled: true
onM3ElevationEnabledChanged: saveSettings()
property int m3ElevationIntensity: 12
@@ -531,15 +512,9 @@ Singleton {
property bool enableFprint: false
property int maxFprintTries: 15
property bool fprintdAvailable: false
property bool enableU2f: false
property string u2fMode: "or"
property bool u2fAvailable: false
property string lockScreenActiveMonitor: "all"
property string lockScreenInactiveColor: "#000000"
property int lockScreenNotificationMode: 0
property bool lockScreenVideoEnabled: false
property string lockScreenVideoPath: ""
property bool lockScreenVideoCycling: false
property bool hideBrightnessSlider: false
property int notificationTimeoutLow: 5000
@@ -1025,7 +1000,6 @@ Singleton {
loadSettings();
initializeListModels();
Processes.detectFprintd();
Processes.detectU2f();
Processes.checkPluginSettings();
}
}
@@ -1173,7 +1147,7 @@ Singleton {
"updateCompositorLayout": updateCompositorLayout,
"applyStoredIconTheme": applyStoredIconTheme,
"updateBarConfigs": updateBarConfigs,
"updateCompositorCursor": updateCompositorCursor,
"updateCompositorCursor": updateCompositorCursor
})
function set(key, value) {

View File

@@ -960,24 +960,6 @@ Singleton {
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
}
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
readonly property var animationPresetDurations: {
"none": 0,
"short": 250,

View File

@@ -18,10 +18,6 @@ Singleton {
fprintdDetectionProcess.running = true;
}
function detectU2f() {
u2fDetectionProcess.running = true;
}
function checkPluginSettings() {
pluginSettingsCheckProcess.running = true;
}
@@ -61,16 +57,6 @@ Singleton {
}
}
property var u2fDetectionProcess: Process {
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
running: false
onExited: function (exitCode) {
if (!settingsRoot)
return;
settingsRoot.u2fAvailable = (exitCode === 0);
}
}
property var pluginSettingsCheckProcess: Process {
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
running: false

View File

@@ -79,9 +79,6 @@ var SPEC = {
hiddenOutputDeviceNames: { def: [] },
hiddenInputDeviceNames: { def: [] },
locale: { def: "", onChange: "updateLocale" },
timeLocale: { def: "" },
launcherLastMode: { def: "all" },
appDrawerLastMode: { def: "apps" },
niriOverviewLastMode: { def: "apps" }

View File

@@ -32,7 +32,6 @@ var SPEC = {
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
firstDayOfWeek: { def: -1 },
use24HourClock: { def: true },
showSeconds: { def: false },
padHours12Hour: { def: false },
@@ -47,9 +46,6 @@ var SPEC = {
modalAnimationSpeed: { def: 1 },
modalCustomAnimationDuration: { def: 150 },
enableRippleEffects: { def: true },
animationVariant: { def: 0 },
motionEffect: { def: 0 },
directionalAnimationMode: { def: 0 },
m3ElevationEnabled: { def: true },
m3ElevationIntensity: { def: 12 },
m3ElevationOpacity: { def: 30 },
@@ -330,15 +326,9 @@ var SPEC = {
enableFprint: { def: false },
maxFprintTries: { def: 15 },
fprintdAvailable: { def: false, persist: false },
enableU2f: { def: false },
u2fMode: { def: "or" },
u2fAvailable: { def: false, persist: false },
lockScreenActiveMonitor: { def: "all" },
lockScreenInactiveColor: { def: "#000000" },
lockScreenNotificationMode: { def: 0 },
lockScreenVideoEnabled: { def: false },
lockScreenVideoPath: { def: "" },
lockScreenVideoCycling: { def: false },
hideBrightnessSlider: { def: false },
notificationTimeoutLow: { def: 5000 },

View File

@@ -9,9 +9,6 @@ function parse(root, jsonObj) {
for (var k in SPEC) {
if (k === "pluginSettings") continue;
// Runtime-only keys are never in the JSON; resetting them here
// would wipe values set by detection processes on every reload.
if (SPEC[k].persist === false) continue;
if (!(k in jsonObj)) {
root[k] = SPEC[k].def;
}

View File

@@ -21,37 +21,11 @@ Item {
required property var workspaceRenameModalLoader
required property var windowRuleModalLoader
function getPreferredBar(refPropertyName) {
function getFirstBar() {
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
return null;
const focusedScreenName = BarWidgetService.getFocusedScreenName();
const loaders = Array.from({
length: root.dankBarRepeater.count
}, (_, i) => root.dankBarRepeater.itemAt(i));
let currentBar = null;
for (const loader of loaders) {
const instances = loader?.item?.barVariants?.instances || [];
for (const bar of instances) {
if (!bar)
continue;
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
const hasRef = !refPropertyName || !!bar[refPropertyName];
if (hasRef) {
currentBar = bar;
if (onFocusedScreen)
break;
}
}
}
return currentBar;
const firstLoader = root.dankBarRepeater.itemAt(0);
return firstLoader ? firstLoader.item : null;
}
IpcHandler {
@@ -123,9 +97,9 @@ Item {
IpcHandler {
function open(): string {
const bar = root.getPreferredBar("controlCenterButtonRef");
const bar = root.getFirstBar();
if (bar) {
bar.triggerControlCenter();
bar.triggerControlCenterOnFocusedScreen();
return "CONTROL_CENTER_OPEN_SUCCESS";
}
return "CONTROL_CENTER_OPEN_FAILED";
@@ -140,14 +114,9 @@ Item {
}
function toggle(): string {
if (root.controlCenterLoader.item?.shouldBeVisible) {
root.controlCenterLoader.item.close();
return "CONTROL_CENTER_TOGGLE_SUCCESS";
}
const bar = root.getPreferredBar("controlCenterButtonRef");
const bar = root.getFirstBar();
if (bar) {
bar.triggerControlCenter();
bar.triggerControlCenterOnFocusedScreen();
return "CONTROL_CENTER_TOGGLE_SUCCESS";
}
return "CONTROL_CENTER_TOGGLE_FAILED";
@@ -162,37 +131,27 @@ Item {
IpcHandler {
function open(tab: string): string {
const bar = root.getPreferredBar("clockButtonRef");
if (!bar)
return "DASH_OPEN_FAILED";
const dash = root.dankDashPopoutLoader.item;
const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
if (!onSameScreen) {
bar.triggerWallpaperBrowser();
root.dankDashPopoutLoader.active = true;
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS";
}
if (!root.dankDashPopoutLoader.item)
return "DASH_OPEN_FAILED";
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS";
return "DASH_OPEN_FAILED";
}
function close(): string {
@@ -204,14 +163,8 @@ Item {
}
function toggle(tab: string): string {
if (root.dankDashPopoutLoader.item?.dashVisible) {
root.dankDashPopoutLoader.item.dashVisible = false;
return "DASH_TOGGLE_SUCCESS";
}
const bar = root.getPreferredBar("clockButtonRef");
if (bar) {
bar.triggerWallpaperBrowser();
const bar = root.getFirstBar();
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
@@ -568,9 +521,8 @@ Item {
IpcHandler {
function wallpaper(): string {
const bar = root.getPreferredBar("clockButtonRef");
if (bar) {
bar.triggerWallpaperBrowser();
const bar = root.getFirstBar();
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
return "SUCCESS: Toggled wallpaper browser";
}
return "ERROR: Failed to toggle wallpaper browser";

View File

@@ -86,7 +86,7 @@ Item {
anchors.bottom: parent.bottom
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
anchors.bottomMargin: modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0
clip: true
DankListView {
@@ -112,7 +112,14 @@ Item {
if (index < 0 || index >= count) {
return;
}
positionViewAtIndex(index, ListView.Contain);
const itemHeight = ClipboardConstants.itemHeight + spacing;
const itemY = index * itemHeight;
const itemBottom = itemY + itemHeight;
if (itemY < contentY) {
contentY = itemY;
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height;
}
}
onCurrentIndexChanged: {
@@ -171,7 +178,14 @@ Item {
if (index < 0 || index >= count) {
return;
}
positionViewAtIndex(index, ListView.Contain);
const itemHeight = ClipboardConstants.itemHeight + spacing;
const itemY = index * itemHeight;
const itemBottom = itemY + itemHeight;
if (itemY < contentY) {
contentY = itemY;
} else if (itemBottom > contentY + height) {
contentY = itemBottom - height;
}
}
onCurrentIndexChanged: {

View File

@@ -31,13 +31,13 @@ Item {
sourceSize.height: 128
function tryLoadImage() {
if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
if (loadQueued || entryType !== "image" || cachedImageData) {
return;
}
thumbnailImage.loadQueued = true;
loadQueued = true;
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
modal.activeImageLoads++;
thumbnailImage.loadImage();
loadImage();
} else {
retryTimer.restart();
}
@@ -47,7 +47,7 @@ Item {
DMSService.sendRequest("clipboard.getEntry", {
"id": entry.id
}, function (response) {
thumbnailImage.loadQueued = false;
loadQueued = false;
if (modal.activeImageLoads > 0) {
modal.activeImageLoads--;
}
@@ -57,7 +57,7 @@ Item {
}
const data = response.result?.data;
if (data) {
thumbnailImage.cachedImageData = data;
cachedImageData = data;
}
});
}

View File

@@ -26,10 +26,10 @@ Item {
property bool closeOnBackgroundClick: true
property string animationType: "scale"
property int animationDuration: Theme.modalAnimationDuration
property real animationScaleCollapsed: Theme.effectScaleCollapsed
property real animationOffset: Theme.effectAnimOffset
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
property list<real> animationExitCurve: Theme.variantModalExitCurve
property real animationScaleCollapsed: 0.96
property real animationOffset: Theme.spacingL
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
property color backgroundColor: Theme.surfaceContainer
property color borderColor: Theme.outlineMedium
property real borderWidth: 0
@@ -44,13 +44,11 @@ Item {
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
property bool useOverlayLayer: false
property real frozenMotionOffsetX: 0
property real frozenMotionOffsetY: 0
readonly property alias contentWindow: contentWindow
readonly property alias clickCatcher: clickCatcher
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
readonly property bool useSingleWindow: CompositorService.isHyprland
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
signal opened
signal dialogClosed
@@ -60,34 +58,19 @@ Item {
function open() {
closeTimer.stop();
animationsEnabled = false;
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
const focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
contentWindow.screen = focusedScreen;
if (!useSingleWindow)
clickCatcher.screen = focusedScreen;
}
if (Theme.isDirectionalEffect || root.useBackground) {
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
}
ModalManager.openModal(root);
Qt.callLater(() => {
animationsEnabled = true;
shouldBeVisible = true;
if (!useSingleWindow && !clickCatcher.visible)
clickCatcher.visible = true;
if (!contentWindow.visible)
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
});
shouldBeVisible = true;
if (!useSingleWindow)
clickCatcher.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
}
function close() {
@@ -148,7 +131,7 @@ Item {
Timer {
id: closeTimer
interval: Theme.variantCloseInterval(animationDuration)
interval: animationDuration + 50
onTriggered: {
if (shouldBeVisible)
return;
@@ -162,17 +145,7 @@ Item {
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowMotionPadding: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
return 0; // Wayland native overlap mask
if (animationType === "slide")
return 30;
if (Theme.isDirectionalEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
if (Theme.isDepthEffect)
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
return Math.max(0, animationOffset);
}
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
@@ -232,26 +205,9 @@ Item {
MouseArea {
anchors.fill: parent
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: root.backgroundClicked()
}
Rectangle {
anchors.fill: parent
z: -1
color: "black"
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
}
}
PanelWindow {
@@ -294,12 +250,9 @@ Item {
bottom: root.useSingleWindow
}
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
WlrLayershell.margins {
left: actualMarginLeft
top: actualMarginTop
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
right: 0
bottom: 0
}
@@ -329,14 +282,13 @@ Item {
anchors.fill: parent
z: -1
color: "black"
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: opacity > 0
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.useBackground
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
NumberAnimation {
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
easing.type: Easing.BezierSpline
enabled: root.animationsEnabled
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -344,8 +296,8 @@ Item {
Item {
id: modalContainer
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
x: root.useSingleWindow ? root.alignedX : shadowBuffer
y: root.useSingleWindow ? root.alignedY : shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
@@ -361,117 +313,45 @@ Item {
}
readonly property bool slide: root.animationType === "slide"
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
readonly property real customDistLeft: customAnchorX
readonly property real customDistRight: root.screenWidth - customAnchorX
readonly property real customDistTop: customAnchorY
readonly property real customDistBottom: root.screenHeight - customAnchorY
readonly property real offsetX: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return 15;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -directionalTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return directionalTravel;
return 0;
default:
return 0;
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return 0;
case "custom":
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
return -depthTravel;
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
return depthTravel;
return 0;
default:
return 0;
}
}
return 0;
}
readonly property real offsetY: {
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
return 0;
if (slide && !directionalEffect && !depthEffect)
return -30;
if (directionalEffect) {
switch (root.positioning) {
case "top-right":
return -Math.max(directionalTravel * 0.65, 96);
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -directionalTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return directionalTravel;
return 0;
default:
// Default to sliding down from top when centered
return -Math.max(directionalTravel, root.screenHeight * 0.24);
}
}
if (depthEffect) {
switch (root.positioning) {
case "top-right":
return -depthTravel * 0.75;
case "custom":
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
return -depthTravel;
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
return depthTravel;
return depthTravel * 0.45;
default:
return -depthTravel;
}
}
return root.animationOffset;
}
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
property real animX: 0
property real animY: 0
property real scaleValue: root.animationScaleCollapsed
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
}
}
Behavior on animX {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on animY {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled
NumberAnimation {
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
easing.type: Easing.BezierSpline
DankAnim {
duration: root.animationDuration
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
@@ -487,14 +367,15 @@ Item {
id: animatedContent
anchors.fill: parent
clip: false
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
transformOrigin: Item.Center
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
enabled: root.animationsEnabled
NumberAnimation {
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}

View File

@@ -51,15 +51,6 @@ Item {
}
}
onSearchModeChanged: {
if (searchMode === "apps") {
_loadAppCategories();
} else {
appCategory = "";
appCategories = [];
}
}
Connections {
target: SettingsData
function onSortAppsAlphabeticallyChanged() {
@@ -74,12 +65,8 @@ Item {
if (!active)
return;
_clearModeCache();
if (searchMode === "apps") {
_loadAppCategories();
if (!searchQuery && searchMode === "all")
performSearch();
} else if (!searchQuery && searchMode === "all") {
performSearch();
}
}
}
@@ -184,8 +171,6 @@ Item {
property string activePluginName: ""
property var activePluginCategories: []
property string activePluginCategory: ""
property string appCategory: ""
property var appCategories: []
function getSectionViewMode(sectionId) {
if (sectionId === "browse_plugins")
@@ -379,8 +364,6 @@ Item {
activePluginName = "";
activePluginCategories = [];
activePluginCategory = "";
appCategory = "";
appCategories = [];
pluginFilter = "";
collapsedSections = {};
_clearModeCache();
@@ -425,19 +408,6 @@ Item {
performSearch();
}
function setAppCategory(category) {
if (appCategory === category)
return;
appCategory = category;
_queryDrivenSearch = true;
_clearModeCache();
performSearch();
}
function _loadAppCategories() {
appCategories = AppSearchService.getAllCategories();
}
function setFileSearchType(type) {
if (fileSearchType === type)
return;
@@ -622,9 +592,8 @@ Item {
}
if (searchMode === "apps") {
var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
var cachedSections = AppSearchService.getCachedDefaultSections();
if (cachedSections && !searchQuery && !isCategoryFiltered) {
if (cachedSections && !searchQuery) {
var modeCache = _getCachedModeData("apps");
if (modeCache) {
_applyHighlights(modeCache.sections, "");
@@ -654,23 +623,9 @@ Item {
return;
}
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]));
}
} else {
var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) {
allItems.push(apps[i]);
}
var apps = searchApps(searchQuery);
for (var i = 0; i < apps.length; i++) {
allItems.push(apps[i]);
}
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);

View File

@@ -14,7 +14,6 @@ Item {
property bool spotlightOpen: false
property bool keyboardActive: false
property bool contentVisible: false
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
property var spotlightContent: launcherContentLoader.item
property bool openedFromOverview: false
property bool isClosing: false
@@ -24,14 +23,8 @@ Item {
property string _pendingMode: ""
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
// Animation state — matches DankPopout/DankModal pattern
property bool animationsEnabled: true
property bool _motionActive: false
property real _frozenMotionX: 0
property real _frozenMotionY: 0
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
readonly property var effectiveScreen: contentWindow.screen
readonly property var effectiveScreen: launcherWindow.screen
readonly property real screenWidth: effectiveScreen?.width ?? 1920
readonly property real screenHeight: effectiveScreen?.height ?? 1080
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
@@ -85,34 +78,6 @@ Item {
}
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
// Shadow padding for the content window (render padding only, no motion padding)
readonly property var shadowLevel: Theme.elevationLevel3
readonly property real shadowFallbackOffset: 6
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
readonly property real alignedX: Theme.snap(modalX, dpr)
readonly property real alignedY: Theme.snap(modalY, dpr)
// For directional/depth: window extends from screen top (content slides within)
// For standard: small window tightly around the modal + shadow padding
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
// Content window geometry
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
readonly property real _cwWidth: alignedWidth + shadowPad * 2
readonly property real _cwHeight: {
if (Theme.isDirectionalEffect)
return screenHeight + shadowPad;
if (Theme.isDepthEffect)
return alignedY + alignedHeight + shadowPad;
return alignedHeight + shadowPad * 2;
}
// Where the content container sits inside the content window
readonly property real _ccX: shadowPad
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
signal dialogClosed
function _ensureContentLoadedAndInitialize(query, mode) {
@@ -132,8 +97,7 @@ Item {
if (!spotlightContent)
return;
contentVisible = true;
// NOTE: forceActiveFocus() is deliberately NOT called here.
// It is deferred to after animation starts to avoid compositor IPC stalls.
spotlightContent.searchField.forceActiveFocus();
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query;
@@ -166,59 +130,40 @@ Item {
}
}
function _openCommon(query, mode) {
function show() {
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
// Disable animations so the snap is instant
animationsEnabled = false;
// Freeze the collapsed offsets (they depend on height which could change)
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen) {
backgroundWindow.screen = focusedScreen;
contentWindow.screen = focusedScreen;
}
if (focusedScreen)
launcherWindow.screen = focusedScreen;
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
_motionActive = false;
// Make windows visible but do NOT request keyboard focus yet
ModalManager.openModal(root);
spotlightOpen = true;
backgroundWindow.visible = true;
contentWindow.visible = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
// Load content and initialize (but no forceActiveFocus — that's deferred)
_ensureContentLoadedAndInitialize(query || "", mode || "");
// Frame 1: enable animations and trigger enter motion
Qt.callLater(() => {
root.animationsEnabled = true;
root._motionActive = true;
// Frame 2: request keyboard focus + activate search field
// Double-deferred to avoid compositor IPC competing with animation frames
Qt.callLater(() => {
root.keyboardActive = true;
if (root.spotlightContent && root.spotlightContent.searchField)
root.spotlightContent.searchField.forceActiveFocus();
});
});
}
function show() {
_openCommon("", "");
_ensureContentLoadedAndInitialize("", "");
}
function showWithQuery(query) {
_openCommon(query, "");
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize(query, "");
}
function hide() {
@@ -226,17 +171,13 @@ Item {
return;
openedFromOverview = false;
isClosing = true;
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
if (!Theme.isDirectionalEffect)
contentVisible = false;
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
_motionActive = false;
contentVisible = false;
keyboardActive = false;
spotlightOpen = false;
focusGrab.active = false;
ModalManager.closeModal(root);
closeCleanupTimer.start();
}
@@ -245,7 +186,21 @@ Item {
}
function showWithMode(mode) {
_openCommon("", mode);
closeCleanupTimer.stop();
isClosing = false;
openedFromOverview = false;
var focusedScreen = CompositorService.getFocusedScreen();
if (focusedScreen)
launcherWindow.screen = focusedScreen;
spotlightOpen = true;
keyboardActive = true;
ModalManager.openModal(root);
if (useHyprlandFocusGrab)
focusGrab.active = true;
_ensureContentLoadedAndInitialize("", mode);
}
function toggleWithMode(mode) {
@@ -266,13 +221,10 @@ Item {
Timer {
id: closeCleanupTimer
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
interval: Theme.modalAnimationDuration + 50
repeat: false
onTriggered: {
isClosing = false;
contentVisible = false;
contentWindow.visible = false;
backgroundWindow.visible = false;
if (root.unloadContentOnClose)
launcherContentLoader.active = false;
dialogClosed();
@@ -290,7 +242,7 @@ Item {
HyprlandFocusGrab {
id: focusGrab
windows: [contentWindow]
windows: [launcherWindow]
active: false
onCleared: {
@@ -315,7 +267,7 @@ Item {
if (Quickshell.screens.length === 0)
return;
const screen = contentWindow.screen;
const screen = launcherWindow.screen;
const screenName = screen?.name;
let needsReset = !screen || !screenName;
@@ -337,31 +289,35 @@ Item {
return;
root._windowEnabled = false;
backgroundWindow.screen = newScreen;
contentWindow.screen = newScreen;
launcherWindow.screen = newScreen;
Qt.callLater(() => {
root._windowEnabled = true;
});
}
}
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
PanelWindow {
id: backgroundWindow
visible: false
id: launcherWindow
visible: root._windowEnabled && (spotlightOpen || isClosing)
color: "transparent"
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "dms:spotlight:bg"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
WlrLayershell.margins {
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
}
}
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
top: true
@@ -371,11 +327,11 @@ Item {
}
mask: Region {
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
item: spotlightOpen ? fullScreenMask : null
}
Item {
id: bgFullScreenMask
id: fullScreenMask
anchors.fill: parent
}
@@ -383,14 +339,13 @@ Item {
id: backgroundDarken
anchors.fill: parent
color: "black"
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: launcherMotionVisible || opacity > 0
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
visible: contentVisible || opacity > 0
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
@@ -398,240 +353,88 @@ Item {
MouseArea {
anchors.fill: parent
enabled: spotlightOpen
onClicked: root.hide()
}
}
onClicked: mouse => {
var contentX = modalContainer.x;
var contentY = modalContainer.y;
var contentW = modalContainer.width;
var contentH = modalContainer.height;
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
WlrLayershell.namespace: "dms:spotlight"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "background":
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
return WlrLayershell.Top;
case "overlay":
return WlrLayershell.Overlay;
default:
return WlrLayershell.Top;
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
root.hide();
}
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
anchors {
left: true
top: true
}
WlrLayershell.margins {
left: root._cwMarginLeft
top: root._cwMarginTop
}
implicitWidth: root._cwWidth
implicitHeight: root._cwHeight
mask: Region {
item: contentInputMask
}
Item {
id: contentInputMask
visible: false
x: contentContainer.x + contentWrapper.x
y: contentContainer.y + contentWrapper.y
width: root.alignedWidth
height: root.alignedHeight
}
id: modalContainer
x: root.modalX
y: root.modalY
width: root.modalWidth
height: root.modalHeight
visible: contentVisible || opacity > 0
Item {
id: contentContainer
opacity: contentVisible ? 1 : 0
scale: contentVisible ? 1 : 0.96
transformOrigin: Item.Center
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
x: root._ccX
y: root._ccY
width: root.alignedWidth
height: root.alignedHeight
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
readonly property bool dockTop: dockEdge === 0
readonly property bool dockBottom: dockEdge === 1
readonly property bool dockLeft: dockEdge === 2
readonly property bool dockRight: dockEdge === 3
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: {
if (directionalEffect) {
if (dockLeft)
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
if (dockRight)
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
}
if (depthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
readonly property real collapsedMotionY: {
if (directionalEffect) {
if (dockTop)
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
if (dockBottom)
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
return 0;
}
if (depthEffect)
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
}
// animX/animY are Behavior-animated — DankPopout pattern
property real animX: 0
property real animY: 0
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
Component.onCompleted: {
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
Connections {
target: root
function on_MotionActiveChanged() {
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
}
}
Behavior on animX {
enabled: root.animationsEnabled
Behavior on opacity {
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on animY {
enabled: root.animationsEnabled
Behavior on scale {
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
duration: Theme.modalAnimationDuration
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on scaleValue {
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
DankAnim {
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
ElevationShadow {
id: launcherShadowLayer
anchors.fill: parent
level: Theme.elevationLevel3
fallbackOffset: 6
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
}
Item {
id: directionalClipMask
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
readonly property real clipOversize: 2000
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
clip: shouldClip
FocusScope {
anchors.fill: parent
focus: keyboardActive
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
Item {
id: aligner
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
width: contentContainer.width
height: contentContainer.height
// Shadow mirrors contentWrapper position/scale/opacity
ElevationShadow {
id: launcherShadowLayer
width: parent.width
height: parent.height
opacity: contentWrapper.opacity
scale: contentWrapper.scale
x: contentWrapper.x
y: contentWrapper.y
level: root.shadowLevel
fallbackOffset: root.shadowFallbackOffset
targetColor: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
targetRadius: root.cornerRadius
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
// contentWrapper moves inside static contentContainer — DankPopout pattern
Item {
id: contentWrapper
width: parent.width
height: parent.height
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
visible: opacity > 0
scale: contentContainer.scaleValue
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity {
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
MouseArea {
anchors.fill: parent
onPressed: mouse => mouse.accepted = true
}
FocusScope {
anchors.fill: parent
focus: keyboardActive
Loader {
id: launcherContentLoader
anchors.fill: parent
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
asynchronous: false
sourceComponent: LauncherContent {
focus: true
parentModal: root
}
onLoaded: {
if (root._pendingInitialize) {
root._initializeAndShow(root._pendingQuery, root._pendingMode);
root._pendingInitialize = false;
}
}
}
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
} // contentWrapper
} // aligner
} // directionalClipMask
} // contentContainer
} // PanelWindow
Keys.onEscapePressed: event => {
root.hide();
event.accepted = true;
}
}
}
}
}

View File

@@ -86,7 +86,7 @@ FocusScope {
Controller {
id: controller
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
active: root.parentModal?.spotlightOpen ?? true
viewModeContext: root.viewModeContext
onItemExecuted: {
@@ -462,7 +462,7 @@ FocusScope {
showClearButton: true
textColor: Theme.surfaceText
font.pixelSize: Theme.fontSizeLarge
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
placeholderText: ""
ignoreUpDownKeys: true
ignoreTabKeys: true
@@ -496,9 +496,8 @@ FocusScope {
Row {
id: categoryRow
width: parent.width
readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
height: showPluginCategories ? 36 : 0
visible: showPluginCategories
height: controller.activePluginCategories.length > 0 ? 36 : 0
visible: controller.activePluginCategories.length > 0
spacing: Theme.spacingS
clip: true
@@ -512,7 +511,6 @@ FocusScope {
DankDropdown {
id: categoryDropdown
visible: categoryRow.showPluginCategories
width: Math.min(200, parent.width)
compactMode: true
dropdownWidth: 200
@@ -696,13 +694,7 @@ FocusScope {
Item {
width: parent.width
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
opacity: {
if (!root.parentModal)
return 1;
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
return 1;
return root.parentModal.isClosing ? 0 : 1;
}
opacity: root.parentModal?.isClosing ? 0 : 1
ResultsList {
id: resultsList
@@ -797,7 +789,7 @@ FocusScope {
Image {
width: 40
height: 40
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
sourceSize.width: 40
sourceSize.height: 40
fillMode: Image.PreserveAspectFit

View File

@@ -1,9 +1,7 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Services
import qs.Widgets
Rectangle {
@@ -37,190 +35,21 @@ Rectangle {
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
// Whether the apps category picker should replace the plain title
readonly property bool hasAppCategories: root.section?.id === "apps" && (root.controller?.appCategories?.length ?? 0) > 0
DankIcon {
anchors.verticalCenter: parent.verticalCenter
// Hide section icon when the category chip already shows one
visible: !leftContent.hasAppCategories
name: root.section?.icon ?? "folder"
size: 16
color: Theme.surfaceVariantText
}
// Plain title — hidden when the category chip is shown
StyledText {
anchors.verticalCenter: parent.verticalCenter
visible: !leftContent.hasAppCategories
text: root.section?.title ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceVariantText
}
// Compact inline category chip — only visible on the apps section
Item {
id: categoryChip
visible: leftContent.hasAppCategories
anchors.verticalCenter: parent.verticalCenter
// Size to content with a fixed-min width so it doesn't jump around
width: chipRow.implicitWidth + Theme.spacingM * 2
height: 24
readonly property string currentCategory: root.controller?.appCategory || (root.controller?.appCategories?.length > 0 ? root.controller.appCategories[0] : "")
readonly property var iconMap: {
const cats = root.controller?.appCategories ?? [];
const m = {};
cats.forEach(c => { m[c] = AppSearchService.getCategoryIcon(c); });
return m;
}
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: chipArea.containsMouse || categoryPopup.visible ? Theme.surfaceContainerHigh : "transparent"
border.color: categoryPopup.visible ? Theme.primary : Theme.outlineMedium
border.width: categoryPopup.visible ? 2 : 1
}
Row {
id: chipRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: categoryChip.iconMap[categoryChip.currentCategory] ?? "apps"
size: 14
color: Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: categoryChip.currentCategory
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
}
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: categoryPopup.visible ? "expand_less" : "expand_more"
size: 14
color: Theme.surfaceVariantText
}
}
MouseArea {
id: chipArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (categoryPopup.visible) {
categoryPopup.close();
} else {
const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0);
categoryPopup.x = pos.x;
categoryPopup.y = pos.y + categoryChip.height + 4;
categoryPopup.open();
}
}
}
Popup {
id: categoryPopup
parent: Overlay.overlay
width: Math.max(categoryChip.width, 180)
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle { color: "transparent" }
contentItem: Rectangle {
radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
border.color: Theme.primary
border.width: 2
ElevationShadow {
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: parent.radius
targetColor: parent.color
borderColor: parent.border.color
borderWidth: parent.border.width
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
}
ListView {
id: categoryList
anchors.fill: parent
anchors.margins: Theme.spacingS
model: root.controller?.appCategories ?? []
spacing: 2
clip: true
interactive: contentHeight > height
implicitHeight: contentHeight
delegate: Rectangle {
id: catDelegate
required property string modelData
required property int index
width: categoryList.width
height: 32
radius: Theme.cornerRadius
readonly property bool isCurrent: categoryChip.currentCategory === modelData
color: isCurrent ? Theme.primaryHover : catArea.containsMouse ? Theme.primaryHoverLight : "transparent"
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
anchors.verticalCenter: parent.verticalCenter
name: categoryChip.iconMap[catDelegate.modelData] ?? "apps"
size: 16
color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: catDelegate.modelData
font.pixelSize: Theme.fontSizeMedium
color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
font.weight: catDelegate.isCurrent ? Font.Medium : Font.Normal
}
}
MouseArea {
id: catArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (root.controller)
root.controller.setAppCategory(catDelegate.modelData);
categoryPopup.close();
}
}
}
}
}
// Size to list content, cap at 10 visible items
height: Math.min((root.controller?.appCategories?.length ?? 0) * 34, 10 * 34) + Theme.spacingS * 2 + 4
}
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.section?.items?.length ?? 0

View File

@@ -225,13 +225,7 @@ Item {
}
StyledText {
text: {
if (root.errorCount === 0)
return I18n.tr("All checks passed", "greeter doctor page success");
return root.errorCount === 1
? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
: I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
}
text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success")
font.pixelSize: Theme.fontSizeMedium
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
}

View File

@@ -470,22 +470,7 @@ FocusScope {
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
}
}
Loader {
id: localeLoader
anchors.fill: parent
active: root.currentIndex === 30
visible: active
focus: active
sourceComponent: LocaleTab {}
onActiveChanged: {
if (active && item)
Qt.callLater(() => item.forceActiveFocus());
Qt.callLater(() => item.forceActiveFocus());
}
}
}

View File

@@ -246,12 +246,6 @@ Rectangle {
"icon": "headphones",
"tabIndex": 29
},
{
"id": "locale",
"text": I18n.tr("Locale"),
"icon": "language",
"tabIndex": 30
},
{
"id": "clipboard",
"text": I18n.tr("Clipboard"),

View File

@@ -106,7 +106,7 @@ DankPopout {
QtObject {
id: modalAdapter
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
property bool isClosing: appDrawerPopout.isClosing
property bool isClosing: false
function hide() {
appDrawerPopout.close();

View File

@@ -87,6 +87,10 @@ Variants {
Component.onCompleted: {
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
if (!source) {
root._renderSettling = false;
}
isInitialized = true;
}
@@ -109,7 +113,7 @@ Variants {
Timer {
id: renderSettleTimer
interval: 1000
interval: 100
onTriggered: root._renderSettling = false
}

View File

@@ -271,8 +271,8 @@ Item {
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
}
font.pixelSize: Theme.fontSizeSmall
color: root.accentColor
@@ -324,8 +324,8 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
}
font.pixelSize: digitalRoot.smallSize
color: Theme.withAlpha(root.accentColor, 0.7)
@@ -528,7 +528,7 @@ Item {
StyledText {
visible: stackedRoot.hasDate
anchors.horizontalCenter: parent.horizontalCenter
text: systemClock.date?.toLocaleDateString(I18n.locale(), "MMM dd") ?? ""
text: systemClock.date?.toLocaleDateString(Qt.locale(), "MMM dd") ?? ""
font.pixelSize: stackedRoot.smallSize * 0.7
color: Theme.withAlpha(root.accentColor, 0.7)
}

View File

@@ -126,11 +126,9 @@ DankPopout {
z: 5000
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.shortDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
duration: 200
easing.type: Easing.OutCubic
}
}
}

View File

@@ -1117,7 +1117,6 @@ Item {
if (!notificationCenterLoader.item) {
return;
}
notificationCenterLoader.item.triggerScreen = barWindow.screen;
const effectiveBarConfig = topBarContent.barConfig;
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
if (notificationCenterLoader.item.setBarContext) {

View File

@@ -566,9 +566,8 @@ PanelWindow {
readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null
readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
function sectionRect(section, isCenter, _dep) {
function sectionRect(section, isCenter) {
if (!section)
return {
"x": 0,
@@ -597,7 +596,7 @@ PanelWindow {
item: clickThroughEnabled ? null : inputMask
Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false) : {
"x": 0,
"y": 0,
"w": 0,
@@ -610,7 +609,7 @@ PanelWindow {
}
Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true) : {
"x": 0,
"y": 0,
"w": 0,
@@ -623,7 +622,7 @@ PanelWindow {
}
Region {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false) : {
"x": 0,
"y": 0,
"w": 0,
@@ -634,14 +633,6 @@ PanelWindow {
width: r.w
height: r.h
}
Region {
readonly property bool active: barWindow.clickThroughEnabled && !inputMask.showing
x: active ? inputMask.x : 0
y: active ? inputMask.y : 0
width: active ? inputMask.width : 0
height: active ? inputMask.height : 0
}
}
Item {
@@ -654,7 +645,7 @@ PanelWindow {
Timer {
id: revealHold
interval: barWindow.clickThroughEnabled ? Math.max((barConfig?.autoHideDelay ?? 250) * 6, 1500) : (barConfig?.autoHideDelay ?? 250)
interval: barConfig?.autoHideDelay ?? 250
repeat: false
onTriggered: {
if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) {
@@ -712,6 +703,7 @@ PanelWindow {
Connections {
function onBarConfigChanged() {
topBarCore.autoHide = barConfig?.autoHide ?? false;
revealHold.interval = barConfig?.autoHideDelay ?? 250;
}
target: rootWindow

View File

@@ -273,7 +273,7 @@ PanelWindow {
IconImage {
anchors.fill: parent
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready

View File

@@ -129,7 +129,7 @@ BasePill {
StyledText {
text: {
const locale = I18n.locale();
const locale = Qt.locale();
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
@@ -144,7 +144,7 @@ BasePill {
StyledText {
text: {
const locale = I18n.locale();
const locale = Qt.locale();
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
@@ -165,7 +165,7 @@ BasePill {
StyledText {
text: {
const locale = I18n.locale();
const locale = Qt.locale();
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
@@ -180,7 +180,7 @@ BasePill {
StyledText {
text: {
const locale = I18n.locale();
const locale = Qt.locale();
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
@@ -311,9 +311,9 @@ BasePill {
id: dateText
text: {
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
return systemClock?.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat);
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat);
}
return systemClock?.date?.toLocaleDateString(I18n.locale(), "ddd d");
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d");
}
font.pixelSize: clockRow.fontSize
color: Theme.widgetTextColor

View File

@@ -16,6 +16,7 @@ DankPopout {
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
triggerWidth: 80
screen: triggerScreen
shouldBeVisible: dashVisible
property bool __focusArmed: false
property bool __contentReady: false

View File

@@ -44,43 +44,6 @@ Item {
property int __volumeHoverCount: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
function panelMotionX(panelWidth, active) {
if (active)
return 0;
if (directionalEffect) {
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
return isRightEdge ? -travel : travel;
}
if (depthEffect) {
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
return isRightEdge ? -travel : travel;
}
return 0;
}
function panelMotionY(panelType, panelHeight, active) {
if (active)
return 0;
if (directionalEffect) {
if (panelType === 2)
return panelHeight * 0.08;
if (panelType === 3)
return -panelHeight * 0.08;
return 0;
}
if (depthEffect) {
if (panelType === 2)
return panelHeight * 0.04;
if (panelType === 3)
return -panelHeight * 0.04;
return 0;
}
return 0;
}
function volumeAreaEntered() {
__volumeHoverCount++;
panelEntered();
@@ -99,47 +62,30 @@ Item {
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
opacity: dropdownType === 1 ? 1 : 0
scale: dropdownType === 1 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
@@ -251,50 +197,33 @@ Item {
Rectangle {
id: audioDevicesPanel
visible: dropdownType === 2 && activePlayer !== null
visible: dropdownType === 2
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
opacity: dropdownType === 2 ? 1 : 0
scale: dropdownType === 2 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
@@ -425,47 +354,30 @@ Item {
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
opacity: dropdownType === 3 ? 1 : 0
scale: dropdownType === 3 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
DankAnim {
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
easing.type: Easing.BezierSpline
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}

View File

@@ -14,15 +14,8 @@ Rectangle {
signal closeDash
function weekStartQt() {
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
return Qt.locale().firstDayOfWeek;
}
return SettingsData.firstDayOfWeek;
}
function weekStartJs() {
return weekStartQt() % 7;
return Qt.locale().firstDayOfWeek % 7;
}
function startOfWeek(dateObj) {
@@ -186,7 +179,7 @@ Rectangle {
StyledText {
width: parent.width - 56
height: 28
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
@@ -230,10 +223,11 @@ Rectangle {
Repeater {
model: {
const days = [];
const qtFirst = weekStartQt();
const loc = Qt.locale();
const qtFirst = loc.firstDayOfWeek;
for (let i = 0; i < 7; ++i) {
const qtDay = ((qtFirst - 1 + i) % 7) + 1;
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
days.push(loc.dayName(qtDay, Locale.ShortFormat));
}
return days;
}

View File

@@ -99,7 +99,7 @@ Card {
}
StyledText {
text: systemClock?.date?.toLocaleDateString(I18n.locale(), "MMM dd")
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
font.pixelSize: Theme.fontSizeSmall
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
anchors.horizontalCenter: parent.horizontalCenter

View File

@@ -447,8 +447,9 @@ Variants {
height: {
if (dock.isVertical) {
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5);
if (!dock.reveal)
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5);
return Math.min(dockBackground.height + 8 + dock.borderThickness, maxDockHeight);
}
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
}
@@ -456,7 +457,8 @@ Variants {
if (dock.isVertical) {
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
}
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
if (!dock.reveal)
return Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5);
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
}
anchors {

View File

@@ -329,7 +329,7 @@ PanelWindow {
IconImage {
anchors.fill: parent
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
smooth: true
asynchronous: true
visible: status === Image.Ready

View File

@@ -218,7 +218,7 @@ Item {
property string fullTimeStr: {
const format = GreetdSettings.getEffectiveTimeFormat();
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
}
property var timeParts: fullTimeStr.split(':')
property string hours: timeParts[0] || ""
@@ -328,9 +328,9 @@ Item {
anchors.topMargin: 4
text: {
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
}
font.pixelSize: Theme.fontSizeXLarge
color: "white"

View File

@@ -333,9 +333,9 @@ Item {
visible: SettingsData.lockScreenShowDate
text: {
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat);
}
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
}
font.pixelSize: Theme.fontSizeXLarge
color: "white"
@@ -687,24 +687,14 @@ Item {
anchors.centerIn: parent
name: {
if (pam.u2fPending)
return "passkey";
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return "fingerprint_off";
if (pam.fprint.active)
return "fingerprint";
if (pam.u2f.active)
return "passkey";
return "lock";
}
size: 20
color: {
if (pam.fprint.tries >= SettingsData.maxFprintTries)
return Theme.error;
if (pam.u2fState !== "")
return Theme.tertiary;
return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
}
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
opacity: pam.passwd.active ? 0 : 1
Behavior on opacity {
@@ -755,7 +745,8 @@ Item {
}
}
onAccepted: {
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
if (!demoMode && !pam.passwd.active) {
console.log("Enter pressed, starting PAM authentication");
pam.passwd.start();
}
}
@@ -765,11 +756,6 @@ Item {
}
if (event.key === Qt.Key_Escape) {
if (pam.u2fPending) {
pam.cancelU2fPending();
event.accepted = true;
return;
}
clear();
}
@@ -834,11 +820,6 @@ Item {
if (root.unlocking) {
return "Unlocking...";
}
if (pam.u2fPending) {
if (pam.u2fState === "insert")
return "Insert your security key...";
return "Touch your security key...";
}
if (pam.passwd.active) {
return "Authenticating...";
}
@@ -913,7 +894,7 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard"
buttonSize: 32
visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
visible: !demoMode && !pam.passwd.active && !root.unlocking
enabled: visible
onClicked: {
if (keyboardController.isKeyboardActive) {
@@ -1014,10 +995,11 @@ Item {
anchors.verticalCenter: parent.verticalCenter
iconName: "keyboard_return"
buttonSize: 36
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
enabled: !demoMode
onClicked: {
if (!demoMode && !pam.u2fPending) {
if (!demoMode) {
console.log("Enter button clicked, starting PAM authentication");
pam.passwd.start();
}
}
@@ -1043,12 +1025,6 @@ Item {
Layout.fillWidth: true
Layout.preferredHeight: 20
text: {
if (pam.u2fState === "insert" && !pam.u2fPending) {
return "Insert your security key...";
}
if (pam.u2fState === "waiting" && !pam.u2fPending) {
return "Touch your security key...";
}
if (root.pamState === "error") {
return "Authentication error - try again";
}
@@ -1060,10 +1036,10 @@ Item {
}
return "";
}
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
color: Theme.error
font.pixelSize: Theme.fontSizeSmall
horizontalAlignment: Text.AlignHCenter
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0
opacity: root.pamState !== "" ? 1 : 0
Behavior on opacity {
NumberAnimation {
@@ -1631,14 +1607,6 @@ Item {
root.passwordBuffer = "";
}
}
onU2fPendingChanged: {
if (u2fPending) {
passwordField.text = "";
root.passwordBuffer = "";
if (keyboardController.isKeyboardActive)
keyboardController.hide();
}
}
}
Binding {

View File

@@ -2,9 +2,8 @@ pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Wayland
import qs.Common
FocusScope {
Rectangle {
id: root
required property WlSessionLock lock
@@ -15,17 +14,7 @@ FocusScope {
signal passwordChanged(string newPassword)
signal unlockRequested
Keys.onPressed: event => {
if (videoScreensaver.active && videoScreensaver.inputEnabled) {
videoScreensaver.dismiss();
event.accepted = true;
}
}
Rectangle {
anchors.fill: parent
color: "transparent"
}
color: "transparent"
LockScreenContent {
id: lockContent
@@ -34,38 +23,17 @@ FocusScope {
demoMode: false
passwordBuffer: root.sharedPasswordBuffer
screenName: root.screenName
enabled: !videoScreensaver.active
focus: !videoScreensaver.active
opacity: videoScreensaver.active ? 0 : 1
onUnlockRequested: root.unlockRequested()
onPasswordBufferChanged: {
if (root.sharedPasswordBuffer !== passwordBuffer) {
root.passwordChanged(passwordBuffer);
}
}
Behavior on opacity {
NumberAnimation {
duration: 200
}
}
}
VideoScreensaver {
id: videoScreensaver
anchors.fill: parent
screenName: root.screenName
}
Component.onCompleted: forceActiveFocus()
onIsLockedChanged: {
if (isLocked) {
forceActiveFocus();
lockContent.resetLockState();
if (SettingsData.lockScreenVideoEnabled) {
videoScreensaver.start();
}
return;
}
lockContent.unlocking = false;

View File

@@ -14,51 +14,14 @@ Scope {
readonly property alias passwd: passwd
readonly property alias fprint: fprint
readonly property alias u2f: u2f
property string lockMessage
property string state
property string fprintState
property string u2fState
property bool u2fPending: false
property string buffer
signal flashMsg
signal unlockRequested
function completeUnlock(): void {
if (!unlockInProgress) {
unlockInProgress = true;
passwd.abort();
fprint.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
u2fPending = false;
u2fState = "";
unlockRequested();
}
}
function proceedAfterPrimaryAuth(): void {
if (SettingsData.enableU2f && SettingsData.u2fMode === "and" && u2f.available) {
u2f.startForSecondFactor();
} else {
completeUnlock();
}
}
function cancelU2fPending(): void {
if (!u2fPending)
return;
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
u2fPending = false;
u2fState = "";
fprint.checkAvail();
}
FileView {
id: dankshellConfigWatcher
@@ -67,9 +30,9 @@ Scope {
}
FileView {
id: u2fConfigWatcher
id: loginConfigWatcher
path: "/etc/pam.d/dankshell-u2f"
path: "/etc/pam.d/login"
printErrors: false
}
@@ -77,7 +40,7 @@ Scope {
id: passwd
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: {
if (message.startsWith("The account is locked"))
@@ -96,8 +59,9 @@ Scope {
onCompleted: res => {
if (res === PamResult.Success) {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
fprint.abort();
root.proceedAfterPrimaryAuth();
root.unlockRequested();
}
return;
}
@@ -141,8 +105,9 @@ Scope {
if (res === PamResult.Success) {
if (!root.unlockInProgress) {
root.unlockInProgress = true;
passwd.abort();
root.proceedAfterPrimaryAuth();
root.unlockRequested();
}
return;
}
@@ -170,74 +135,6 @@ Scope {
}
}
PamContext {
id: u2f
property bool available
function checkAvail(): void {
if (!available || !SettingsData.enableU2f || !root.lockSecured) {
abort();
return;
}
if (SettingsData.u2fMode === "or") {
start();
}
}
function startForSecondFactor(): void {
if (!available || !SettingsData.enableU2f) {
root.completeUnlock();
return;
}
abort();
root.u2fPending = true;
root.u2fState = "";
u2fPendingTimeout.restart();
start();
}
config: u2fConfigWatcher.loaded ? "dankshell-u2f" : "u2f"
configDirectory: u2fConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
onMessageChanged: {
if (message.toLowerCase().includes("touch"))
root.u2fState = "waiting";
}
onCompleted: res => {
if (!available || root.unlockInProgress)
return;
if (res === PamResult.Success) {
root.completeUnlock();
return;
}
if (res === PamResult.Error || res === PamResult.MaxTries || res === PamResult.Failed) {
abort();
if (root.u2fPending) {
if (root.u2fState === "waiting") {
// AND mode: device was found but auth failed → back to password
root.u2fPending = false;
root.u2fState = "";
fprint.checkAvail();
} else {
// AND mode: no device found → keep pending, show "Insert...", retry
root.u2fState = "insert";
u2fErrorRetry.restart();
}
} else {
// OR mode: prompt to insert key, silently retry
root.u2fState = "insert";
u2fErrorRetry.restart();
}
}
}
}
Process {
id: availProc
@@ -248,16 +145,6 @@ Scope {
}
}
Process {
id: u2fAvailProc
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
onExited: code => {
u2f.available = code === 0;
u2f.checkAvail();
}
}
Timer {
id: errorRetry
@@ -265,20 +152,6 @@ Scope {
onTriggered: fprint.start()
}
Timer {
id: u2fErrorRetry
interval: 800
onTriggered: u2f.start()
}
Timer {
id: u2fPendingTimeout
interval: 30000
onTriggered: root.cancelU2fPending()
}
Timer {
id: stateReset
@@ -302,22 +175,13 @@ Scope {
onLockSecuredChanged: {
if (lockSecured) {
availProc.running = true;
u2fAvailProc.running = true;
root.state = "";
root.fprintState = "";
root.u2fState = "";
root.u2fPending = false;
root.lockMessage = "";
root.unlockInProgress = false;
} else {
fprint.abort();
passwd.abort();
u2f.abort();
errorRetry.running = false;
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
root.unlockInProgress = false;
}
}
@@ -328,20 +192,5 @@ Scope {
function onEnableFprintChanged(): void {
fprint.checkAvail();
}
function onEnableU2fChanged(): void {
u2f.checkAvail();
}
function onU2fModeChanged(): void {
if (root.lockSecured) {
u2f.abort();
u2fErrorRetry.running = false;
u2fPendingTimeout.running = false;
root.u2fPending = false;
root.u2fState = "";
u2f.checkAvail();
}
}
}
}

View File

@@ -1,200 +0,0 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Services
Item {
id: root
required property string screenName
property bool active: false
property string videoSource: ""
property bool inputEnabled: false
property point lastMousePos: Qt.point(-1, -1)
property bool mouseInitialized: false
property var videoPlayer: null
signal dismissed
visible: active
z: 1000
Rectangle {
id: background
anchors.fill: parent
color: "black"
visible: root.active
}
Timer {
id: inputEnableTimer
interval: 500
onTriggered: root.inputEnabled = true
}
Process {
id: videoPicker
property string result: ""
property string folder: ""
command: ["sh", "-c", "find '" + folder + "' -maxdepth 1 -type f \\( " + "-iname '*.mp4' -o -iname '*.mkv' -o -iname '*.webm' -o " + "-iname '*.mov' -o -iname '*.avi' -o -iname '*.m4v' " + "\\) 2>/dev/null | shuf -n1"]
stdout: SplitParser {
onRead: data => {
const path = data.trim();
if (path) {
videoPicker.result = path;
root.videoSource = "file://" + path;
}
}
}
onExited: exitCode => {
if (exitCode !== 0 || !videoPicker.result) {
console.warn("VideoScreensaver: no video found in folder");
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("No video found in folder"));
root.dismiss();
}
}
}
Process {
id: fileChecker
command: ["test", "-d", SettingsData.lockScreenVideoPath]
onExited: exitCode => {
const isDir = exitCode === 0;
const videoPath = SettingsData.lockScreenVideoPath;
if (isDir) {
videoPicker.folder = videoPath;
videoPicker.running = true;
} else if (SettingsData.lockScreenVideoCycling) {
const parentFolder = videoPath.substring(0, videoPath.lastIndexOf('/'));
videoPicker.folder = parentFolder;
videoPicker.running = true;
} else {
root.videoSource = "file://" + videoPath;
}
}
}
function createVideoPlayer() {
if (videoPlayer)
return true;
try {
videoPlayer = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
Video {
anchors.fill: parent
fillMode: VideoOutput.PreserveAspectCrop
loops: MediaPlayer.Infinite
volume: 0
}
`, background, "VideoScreensaver.VideoPlayer");
videoPlayer.errorOccurred.connect((error, errorString) => {
console.warn("VideoScreensaver: playback error:", errorString);
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("Playback error: ") + errorString);
root.dismiss();
});
return true;
} catch (e) {
console.warn("VideoScreensaver: Failed to create video player:", e);
return false;
}
}
function destroyVideoPlayer() {
if (videoPlayer) {
videoPlayer.stop();
videoPlayer.destroy();
videoPlayer = null;
}
}
function start() {
if (!SettingsData.lockScreenVideoEnabled || !SettingsData.lockScreenVideoPath)
return;
if (!MultimediaService.available) {
ToastService.showError(I18n.tr("Video Screensaver"), I18n.tr("QtMultimedia is not available"));
return;
}
if (!createVideoPlayer())
return;
videoPicker.result = "";
videoPicker.folder = "";
inputEnabled = false;
mouseInitialized = false;
lastMousePos = Qt.point(-1, -1);
active = true;
inputEnableTimer.start();
fileChecker.running = true;
}
function dismiss() {
if (!active)
return;
destroyVideoPlayer();
inputEnabled = false;
active = false;
videoSource = "";
dismissed();
}
onVideoSourceChanged: {
if (videoSource && active && videoPlayer) {
videoPlayer.source = videoSource;
videoPlayer.play();
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
enabled: root.active && root.inputEnabled
hoverEnabled: true
propagateComposedEvents: false
onPositionChanged: mouse => {
if (!root.mouseInitialized) {
root.lastMousePos = Qt.point(mouse.x, mouse.y);
root.mouseInitialized = true;
return;
}
var dx = Math.abs(mouse.x - root.lastMousePos.x);
var dy = Math.abs(mouse.y - root.lastMousePos.y);
if (dx > 5 || dy > 5) {
root.dismiss();
}
}
onClicked: root.dismiss()
onPressed: root.dismiss()
onWheel: root.dismiss()
}
Connections {
target: IdleService
function onLockRequested() {
if (SettingsData.lockScreenVideoEnabled && !root.active) {
root.start();
}
}
function onFadeToLockRequested() {
if (SettingsData.lockScreenVideoEnabled && !root.active) {
IdleService.cancelFadeToLock();
root.start();
}
}
}
}

View File

@@ -137,12 +137,12 @@ Rectangle {
return "";
const appIcon = historyItem.appIcon;
if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
return iconFromImage ? "image://icon/" + iconFromImage : "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
return "";
return Paths.resolveIconPath(appIcon);
return Quickshell.iconPath(appIcon, true);
}
hasImage: hasNotificationImage

View File

@@ -215,12 +215,12 @@ Rectangle {
return "";
const appIcon = notificationGroup?.latestNotification?.appIcon;
if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
return iconFromImage ? "image://icon/" + iconFromImage : "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
return "";
return Paths.resolveIconPath(appIcon);
return Quickshell.iconPath(appIcon, true);
}
hasImage: hasNotificationImage
@@ -552,12 +552,12 @@ Rectangle {
return "";
const appIcon = modelData?.appIcon;
if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
return iconFromImage ? "image://icon/" + iconFromImage : "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
return "";
return Paths.resolveIconPath(appIcon);
return Quickshell.iconPath(appIcon, true);
}
fallbackIcon: {

View File

@@ -39,9 +39,11 @@ DankPopout {
}
}
popupWidth: 400
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
popupHeight: stablePopupHeight
positioning: ""
animationScaleCollapsed: 0.94
animationOffset: 0
suspendShadowWhileResizing: false
screen: triggerScreen

View File

@@ -34,51 +34,51 @@ Rectangle {
readonly property var timeoutOptions: [
{
"text": I18n.tr("Never"),
"text": "Never",
"value": 0
},
{
"text": I18n.tr("1 second"),
"text": "1 second",
"value": 1000
},
{
"text": I18n.tr("3 seconds"),
"text": "3 seconds",
"value": 3000
},
{
"text": I18n.tr("5 seconds"),
"text": "5 seconds",
"value": 5000
},
{
"text": I18n.tr("8 seconds"),
"text": "8 seconds",
"value": 8000
},
{
"text": I18n.tr("10 seconds"),
"text": "10 seconds",
"value": 10000
},
{
"text": I18n.tr("15 seconds"),
"text": "15 seconds",
"value": 15000
},
{
"text": I18n.tr("30 seconds"),
"text": "30 seconds",
"value": 30000
},
{
"text": I18n.tr("1 minute"),
"text": "1 minute",
"value": 60000
},
{
"text": I18n.tr("2 minutes"),
"text": "2 minutes",
"value": 120000
},
{
"text": I18n.tr("5 minutes"),
"text": "5 minutes",
"value": 300000
},
{
"text": I18n.tr("10 minutes"),
"text": "10 minutes",
"value": 600000
}
]

View File

@@ -24,29 +24,6 @@ PanelWindow {
property real _lastReportedAlignedHeight: -1
property real _storedTopMargin: 0
property real _storedBottomMargin: 0
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real entryTravel: {
const base = Math.abs(Theme.effectAnimOffset);
if (directionalEffect) {
if (isCenterPosition)
return Math.max(base, Math.round(content.height * 1.1));
return Math.max(base, Math.round(content.width * 0.95));
}
if (depthEffect)
return Math.max(base, 44);
return base;
}
readonly property real exitTravel: {
if (directionalEffect) {
if (isCenterPosition)
return content.height + entryTravel;
return content.width + entryTravel;
}
if (depthEffect)
return Math.round(entryTravel * 1.35);
return Anims.slidePx;
}
readonly property string clearText: I18n.tr("Dismiss")
property bool descriptionExpanded: false
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
@@ -160,9 +137,9 @@ PanelWindow {
enabled: !exiting && !_isDestroying
NumberAnimation {
id: implicitHeightAnim
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.emphasized
}
}
@@ -542,12 +519,12 @@ PanelWindow {
return "";
const appIcon = notificationData.appIcon;
if (!appIcon)
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
return iconFromImage ? "image://icon/" + iconFromImage : "";
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
return appIcon;
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
return "";
return Paths.resolveIconPath(appIcon);
return Quickshell.iconPath(appIcon, true);
}
hasImage: hasNotificationImage
@@ -934,9 +911,9 @@ PanelWindow {
if (isCenterPosition)
return 0;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel;
return isLeft ? -Anims.slidePx : Anims.slidePx;
}
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
}
]
}
@@ -948,16 +925,16 @@ PanelWindow {
property: isCenterPosition ? "y" : "x"
from: {
if (isTopCenter)
return -entryTravel;
return -Anims.slidePx;
if (isBottomCenter)
return entryTravel;
return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -entryTravel : entryTravel;
return isLeft ? -Anims.slidePx : Anims.slidePx;
}
to: 0
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
duration: Theme.notificationEnterDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutEnterCurve
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
onStopped: {
if (!win.exiting && !win._isDestroying) {
if (isCenterPosition) {
@@ -982,35 +959,35 @@ PanelWindow {
from: 0
to: {
if (isTopCenter)
return -exitTravel;
return -Anims.slidePx;
if (isBottomCenter)
return exitTravel;
return Anims.slidePx;
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
return isLeft ? -exitTravel : exitTravel;
return isLeft ? -Anims.slidePx : Anims.slidePx;
}
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
NumberAnimation {
target: content
property: "opacity"
from: 1
to: Theme.isDirectionalEffect ? 1 : 0
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
to: 0
duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.standardAccel
}
NumberAnimation {
target: content
property: "scale"
from: 1
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
to: 0.98
duration: Theme.notificationExitDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantPopoutExitCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
}
}

View File

@@ -27,11 +27,11 @@ DankOSD {
let icon = "music_note";
switch (player.playbackState) {
case MprisPlaybackState.Playing:
icon = "pause";
icon = "play_arrow";
break;
case MprisPlaybackState.Paused:
case MprisPlaybackState.Stopped:
icon = "play_arrow";
icon = "pause";
break;
}
if (icon === _displayIcon)

View File

@@ -311,9 +311,7 @@ Item {
const prefs = cfg?.screenPreferences || ["all"];
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"))
return I18n.tr("All displays");
return prefs.length === 1
? I18n.tr("%1 display").arg(prefs.length)
: I18n.tr("%1 displays").arg(prefs.length);
return I18n.tr("%1 display(s)").replace("%1", prefs.length);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText

View File

@@ -344,11 +344,7 @@ Item {
return I18n.tr("%1 exists but is not included in config. Custom keybinds will not work until this is fixed.").arg(bindsFile);
if (warningBox.showWarning) {
const count = warningBox.status.overriddenBy;
return I18n.ntr(
"%1 DMS bind may be overridden by config binds that come after the include.",
"%1 DMS binds may be overridden by config binds that come after the include.",
count
).arg(count);
return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count);
}
return "";
}

View File

@@ -897,7 +897,7 @@ Item {
Image {
width: 24
height: 24
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
@@ -1008,7 +1008,7 @@ Item {
Image {
width: 24
height: 24
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
@@ -1154,7 +1154,7 @@ Item {
Image {
width: 24
height: 24
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
sourceSize.width: 24
sourceSize.height: 24
fillMode: Image.PreserveAspectFit
@@ -1170,7 +1170,7 @@ Item {
spacing: 2
StyledText {
text: modelData.name || I18n.tr("Unknown App")
text: modelData.name || "Unknown App"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
@@ -1179,7 +1179,7 @@ Item {
StyledText {
text: {
if (!modelData.lastUsed)
return I18n.tr("Never used");
return "Never used";
var date = new Date(modelData.lastUsed);
var now = new Date();
var diffMs = now - date;
@@ -1189,17 +1189,11 @@ Item {
if (diffMins < 1)
return I18n.tr("Last launched just now");
if (diffMins < 60)
return diffMins === 1
? I18n.tr("Last launched %1 minute ago").arg(diffMins)
: I18n.tr("Last launched %1 minutes ago").arg(diffMins);
return I18n.tr("Last launched %1 minute%2 ago").arg(diffMins).arg(diffMins === 1 ? "" : "s");
if (diffHours < 24)
return diffHours === 1
? I18n.tr("Last launched %1 hour ago").arg(diffHours)
: I18n.tr("Last launched %1 hours ago").arg(diffHours);
return I18n.tr("Last launched %1 hour%2 ago").arg(diffHours).arg(diffHours === 1 ? "" : "s");
if (diffDays < 7)
return diffDays === 1
? I18n.tr("Last launched %1 day ago").arg(diffDays)
: I18n.tr("Last launched %1 days ago").arg(diffDays);
return I18n.tr("Last launched %1 day%2 ago").arg(diffDays).arg(diffDays === 1 ? "" : "s");
return I18n.tr("Last launched %1").arg(date.toLocaleDateString());
}
font.pixelSize: Theme.fontSizeSmall

View File

@@ -1,91 +0,0 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.Modules.Settings.Widgets
Item {
id: localeTab
readonly property string _systemDefaultLabel: I18n.tr("System Default")
function _localeDisplayName(localeCode) {
if (!I18n.presentLocales[localeCode])
return;
const nativeName = I18n.presentLocales[localeCode].nativeLanguageName;
return nativeName[0].toUpperCase() + nativeName.slice(1);
}
function _allLocaleOptions() {
return [_systemDefaultLabel].concat(Object.keys(I18n.presentLocales).map(_localeDisplayName));
}
function _codeForDisplayName(displayName) {
if (displayName === _systemDefaultLabel)
return "";
for (const code of Object.keys(I18n.presentLocales)) {
if (_localeDisplayName(code) === displayName)
return code;
}
return "";
}
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: mainColumn.height + Theme.spacingXL
contentWidth: width
Column {
id: mainColumn
topPadding: 4
width: Math.min(550, parent.width - Theme.spacingL * 2)
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "locale"
tags: ["locale", "language", "country"]
title: I18n.tr("Locale Settings")
iconName: "language"
SettingsDropdownRow {
id: localeDropdown
tab: "locale"
tags: ["locale", "language", "country"]
settingKey: "locale"
text: I18n.tr("Current Locale")
description: I18n.tr("Change the locale used by the DMS interface.")
options: localeTab._allLocaleOptions()
enableFuzzySearch: true
Component.onCompleted: {
currentValue = SessionData.locale ? localeTab._localeDisplayName(SessionData.locale) : localeTab._systemDefaultLabel;
}
onValueChanged: value => {
SessionData.set("locale", localeTab._codeForDisplayName(value));
}
}
SettingsDropdownRow {
id: timeLocaleDropdown
tab: "locale"
tags: ["locale", "time", "date", "format", "region"]
settingKey: "timeLocale"
text: I18n.tr("Time & Date Locale")
description: I18n.tr("Change the locale used for date and time formatting, independent of the interface language.")
options: localeTab._allLocaleOptions()
enableFuzzySearch: true
Component.onCompleted: {
currentValue = SessionData.timeLocale ? localeTab._localeDisplayName(SessionData.timeLocale) : localeTab._systemDefaultLabel;
}
onValueChanged: value => {
SessionData.set("timeLocale", localeTab._codeForDisplayName(value));
}
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Modals.FileBrowser
import qs.Services
import qs.Widgets
import qs.Modules.Settings.Widgets
@@ -9,16 +8,6 @@ import qs.Modules.Settings.Widgets
Item {
id: root
FileBrowserModal {
id: videoBrowserModal
browserTitle: I18n.tr("Select Video or Folder")
browserIcon: "movie"
browserType: "video"
showHiddenFiles: false
fileExtensions: ["*.mp4", "*.mkv", "*.webm", "*.mov", "*.avi", "*.m4v"]
onFileSelected: path => SettingsData.set("lockScreenVideoPath", path)
}
DankFlickable {
anchors.fill: parent
clip: true
@@ -172,120 +161,11 @@ Item {
settingKey: "enableFprint"
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
text: I18n.tr("Enable fingerprint authentication")
description: SettingsData.fprintdAvailable ? I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)") : I18n.tr("Not enrolled", "fingerprint not detected status")
descriptionColor: SettingsData.fprintdAvailable ? Theme.surfaceVariantText : Theme.warning
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
checked: SettingsData.enableFprint
enabled: SettingsData.fprintdAvailable
visible: SettingsData.fprintdAvailable
onToggled: checked => SettingsData.set("enableFprint", checked)
}
SettingsToggleRow {
settingKey: "enableU2f"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "fido", "authentication", "hardware"]
text: I18n.tr("Enable security key authentication", "Enable FIDO2/U2F hardware security key for lock screen")
description: SettingsData.u2fAvailable ? I18n.tr("Use a FIDO2/U2F security key (e.g. YubiKey) for lock screen authentication (requires enrolled keys)", "lock screen U2F security key setting") : I18n.tr("Not enrolled", "security key not detected status")
descriptionColor: SettingsData.u2fAvailable ? Theme.surfaceVariantText : Theme.warning
checked: SettingsData.enableU2f
enabled: SettingsData.u2fAvailable
onToggled: checked => SettingsData.set("enableU2f", checked)
}
SettingsDropdownRow {
settingKey: "u2fMode"
tags: ["lock", "screen", "u2f", "yubikey", "security", "key", "mode", "factor", "second"]
text: I18n.tr("Security key mode", "lock screen U2F security key mode setting")
description: I18n.tr("'Alternative' lets the key unlock on its own. 'Second factor' requires password or fingerprint first, then the key.", "lock screen U2F security key mode setting")
visible: SettingsData.u2fAvailable && SettingsData.enableU2f
options: [I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method"), I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint")]
currentValue: SettingsData.u2fMode === "and" ? I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint") : I18n.tr("Alternative (OR)", "U2F mode option: key works as standalone unlock method")
onValueChanged: value => {
if (value === I18n.tr("Second Factor (AND)", "U2F mode option: key required after password or fingerprint"))
SettingsData.set("u2fMode", "and");
else
SettingsData.set("u2fMode", "or");
}
}
}
SettingsCard {
width: parent.width
iconName: "movie"
title: I18n.tr("Video Screensaver")
settingKey: "videoScreensaver"
StyledText {
visible: !MultimediaService.available
text: I18n.tr("QtMultimedia is not available - video screensaver requires qt multimedia services")
font.pixelSize: Theme.fontSizeSmall
color: Theme.warning
width: parent.width
wrapMode: Text.WordWrap
}
SettingsToggleRow {
settingKey: "lockScreenVideoEnabled"
tags: ["lock", "screen", "video", "screensaver", "animation", "movie"]
text: I18n.tr("Enable Video Screensaver")
description: I18n.tr("Play a video when the screen locks.")
enabled: MultimediaService.available
checked: SettingsData.lockScreenVideoEnabled
onToggled: checked => SettingsData.set("lockScreenVideoEnabled", checked)
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
StyledText {
text: I18n.tr("Video Path")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
StyledText {
text: I18n.tr("Path to a video file or folder containing videos")
font.pixelSize: Theme.fontSizeXSmall
color: Theme.outlineVariant
wrapMode: Text.WordWrap
width: parent.width
}
Row {
width: parent.width
spacing: Theme.spacingS
DankTextField {
id: videoPathField
width: parent.width - browseVideoButton.width - Theme.spacingS
placeholderText: I18n.tr("/path/to/videos")
text: SettingsData.lockScreenVideoPath
backgroundColor: Theme.surfaceContainerHighest
onTextChanged: {
if (text !== SettingsData.lockScreenVideoPath) {
SettingsData.set("lockScreenVideoPath", text);
}
}
}
DankButton {
id: browseVideoButton
text: I18n.tr("Browse")
onClicked: videoBrowserModal.open()
}
}
}
SettingsToggleRow {
settingKey: "lockScreenVideoCycling"
tags: ["lock", "screen", "video", "screensaver", "cycling", "random", "shuffle"]
text: I18n.tr("Automatic Cycling")
description: I18n.tr("Pick a different random video each time from the same folder")
visible: SettingsData.lockScreenVideoEnabled && MultimediaService.available
enabled: MultimediaService.available
checked: SettingsData.lockScreenVideoCycling
onToggled: checked => SettingsData.set("lockScreenVideoCycling", checked)
}
}
SettingsCard {

View File

@@ -340,9 +340,7 @@ Item {
if (devices.length === 0)
return I18n.tr("No adapters");
if (connected === 0)
return devices.length === 1
? I18n.tr("%1 adapter, none connected").arg(devices.length)
: I18n.tr("%1 adapters, none connected").arg(devices.length);
return I18n.tr("%1 adapter(s), none connected").arg(devices.length);
return I18n.tr("%1 connected").arg(connected);
}
font.pixelSize: Theme.fontSizeSmall

View File

@@ -14,12 +14,6 @@ Item {
LayoutMirroring.childrenInherit: true
property bool showAddPrinter: false
property bool manualEntryMode: false
property string manualHost: ""
property string manualPort: "631"
property string manualProtocol: "ipp"
property bool testingConnection: false
property var testConnectionResult: null
property string newPrinterName: ""
property string selectedDeviceUri: ""
property var selectedDevice: null
@@ -29,12 +23,6 @@ Item {
property var suggestedPPDs: []
function resetAddPrinterForm() {
manualEntryMode = false;
manualHost = "";
manualPort = "631";
manualProtocol = "ipp";
testingConnection = false;
testConnectionResult = null;
newPrinterName = "";
selectedDeviceUri = "";
selectedDevice = null;
@@ -44,45 +32,6 @@ Item {
suggestedPPDs = [];
}
Connections {
target: CupsService
function onPpdsChanged() {
if (printerTab.manualEntryMode && printerTab.testConnectionResult?.success)
printerTab.selectDriverlessPPD();
}
}
function selectDriverlessPPD() {
if (printerTab.selectedPpd || CupsService.ppds.length === 0)
return;
const probeModel = printerTab.testConnectionResult?.data?.makeModel || "";
let suggested = [];
// Try to find a model-specific PPD match
if (probeModel) {
const normalizedModel = probeModel.toLowerCase().replace(/[^a-z0-9]/g, "");
const modelMatches = CupsService.ppds.filter(p => {
const normalizedPPD = (p.makeModel || "").toLowerCase().replace(/[^a-z0-9]/g, "");
return normalizedPPD.includes(normalizedModel) || normalizedModel.includes(normalizedPPD);
});
if (modelMatches.length > 0)
suggested = suggested.concat(modelMatches);
}
// Always include driverless as an option
const driverless = CupsService.ppds.filter(p => p.name === "driverless" || p.name === "everywhere");
for (const d of driverless) {
if (!suggested.find(s => s.name === d.name))
suggested.push(d);
}
if (suggested.length > 0) {
printerTab.selectedPpd = suggested[0].name;
printerTab.suggestedPPDs = suggested;
}
}
function selectDevice(device) {
if (!device)
return;
@@ -327,93 +276,9 @@ Item {
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
}
Row {
width: parent.width
spacing: Theme.spacingS
Rectangle {
width: discoverRow.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius
color: !printerTab.manualEntryMode ? Theme.primary : (discoverArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
Row {
id: discoverRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "search"
size: 16
color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
}
StyledText {
text: I18n.tr("Discover Devices", "Toggle button to scan for printers via mDNS/Avahi")
font.pixelSize: Theme.fontSizeSmall
color: !printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: discoverArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
printerTab.manualEntryMode = false;
printerTab.testConnectionResult = null;
printerTab.testingConnection = false;
}
}
}
Rectangle {
width: manualRow.width + Theme.spacingM * 2
height: 32
radius: Theme.cornerRadius
color: printerTab.manualEntryMode ? Theme.primary : (manualArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight)
Row {
id: manualRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "edit"
size: 16
color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
}
StyledText {
text: I18n.tr("Add by Address", "Toggle button to manually add a printer by IP or hostname")
font.pixelSize: Theme.fontSizeSmall
color: printerTab.manualEntryMode ? Theme.onPrimary : Theme.surfaceText
font.weight: Font.Medium
}
}
MouseArea {
id: manualArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
printerTab.manualEntryMode = true;
printerTab.selectedDevice = null;
printerTab.selectedDeviceUri = "";
if (CupsService.ppds.length === 0)
CupsService.getPPDs();
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: !printerTab.manualEntryMode
Row {
width: parent.width
@@ -486,202 +351,6 @@ Item {
elide: Text.ElideRight
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
visible: printerTab.manualEntryMode
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Host", "Label for printer IP address or hostname input field")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: 80
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
width: parent.width - 80 - Theme.spacingS
placeholderText: I18n.tr("IP address or hostname", "Placeholder text for manual printer address input")
text: printerTab.manualHost
onTextEdited: {
printerTab.manualHost = text;
printerTab.testConnectionResult = null;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Port", "Label for printer port number input field")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: 80
anchors.verticalCenter: parent.verticalCenter
}
DankTextField {
width: 80
placeholderText: "631"
text: printerTab.manualPort
onTextEdited: {
printerTab.manualPort = text;
printerTab.testConnectionResult = null;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
StyledText {
text: I18n.tr("Protocol", "Label for printer protocol selector, e.g. ipp, ipps, lpd, socket")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: 80
anchors.verticalCenter: parent.verticalCenter
}
DankDropdown {
id: protocolDropdown
dropdownWidth: 120
popupWidth: 120
currentValue: printerTab.manualProtocol
options: ["ipp", "ipps", "lpd", "socket"]
onValueChanged: value => {
printerTab.manualProtocol = value;
printerTab.testConnectionResult = null;
}
}
}
Row {
width: parent.width
spacing: Theme.spacingS
Item {
width: 80
height: 1
}
DankButton {
text: printerTab.testingConnection ? I18n.tr("Testing...", "Button state while testing printer connection") : I18n.tr("Test Connection", "Button to test connection to a printer by IP address")
iconName: printerTab.testingConnection ? "sync" : "lan"
buttonHeight: 36
enabled: printerTab.manualHost.length > 0 && !printerTab.testingConnection
onClicked: {
printerTab.testingConnection = true;
printerTab.testConnectionResult = null;
const port = parseInt(printerTab.manualPort) || 631;
CupsService.testConnection(printerTab.manualHost, port, printerTab.manualProtocol, response => {
printerTab.testingConnection = false;
if (response.error) {
printerTab.testConnectionResult = {
"success": false,
"error": response.error
};
} else if (response.result) {
printerTab.testConnectionResult = {
"success": response.result.reachable === true,
"data": response.result
};
if (response.result.reachable) {
if (response.result.uri)
printerTab.selectedDeviceUri = response.result.uri;
if (response.result.name && !printerTab.newPrinterName)
printerTab.newPrinterName = response.result.name.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").substring(0, 32) || "Printer";
// Load PPDs if not loaded yet, then select driverless
if (CupsService.ppds.length === 0) {
CupsService.getPPDs();
}
selectDriverlessPPD();
}
}
});
}
}
}
Column {
width: parent.width
spacing: Theme.spacingXS
visible: printerTab.testConnectionResult !== null
Row {
spacing: Theme.spacingS
Item {
width: 80
height: 1
}
Rectangle {
width: 8
height: 8
radius: 4
anchors.verticalCenter: parent.verticalCenter
color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error
}
StyledText {
text: printerTab.testConnectionResult?.success ? I18n.tr("Printer reachable", "Status message when test connection to printer succeeds") : I18n.tr("Connection failed", "Status message when test connection to printer fails")
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: printerTab.testConnectionResult?.success ? Theme.success : Theme.error
}
}
Row {
spacing: Theme.spacingS
visible: printerTab.testConnectionResult?.success && (printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info)
Item {
width: 80
height: 1
}
StyledText {
text: printerTab.testConnectionResult?.data?.makeModel || printerTab.testConnectionResult?.data?.info || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}
}
Row {
spacing: Theme.spacingS
visible: !printerTab.testConnectionResult?.success && printerTab.testConnectionResult?.data?.error
Item {
width: 80
height: 1
}
StyledText {
text: printerTab.testConnectionResult?.data?.error || printerTab.testConnectionResult?.error || ""
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.parent.width - 80 - Theme.spacingS
wrapMode: Text.WordWrap
}
}
}
}
Column {
width: parent.width
spacing: Theme.spacingS
Row {
width: parent.width
@@ -879,7 +548,7 @@ Item {
const count = CupsService.printerNames.length;
if (count === 0)
return I18n.tr("No printers configured");
return I18n.ntr("%1 printer", "%1 printers", count).arg(count);
return I18n.tr("%1 printer(s)").arg(count);
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
@@ -1029,7 +698,7 @@ Item {
}
StyledText {
text: I18n.ntr("%1 job", "%1 jobs", printerData?.jobs?.length ?? 0).arg(printerData?.jobs?.length ?? 0)
text: I18n.tr("%1 job(s)").arg(printerData?.jobs?.length ?? 0)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: (printerData?.jobs?.length ?? 0) > 0
@@ -1576,7 +1245,7 @@ Item {
}
StyledText {
text: I18n.ntr("%1 class", "%1 classes", CupsService.printerClasses.length).arg(CupsService.printerClasses.length)
text: I18n.tr("%1 class(es)").arg(CupsService.printerClasses.length)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
width: parent.width
@@ -1641,7 +1310,7 @@ Item {
}
StyledText {
text: I18n.ntr("%1 printer", "%1 printers", modelData.members?.length ?? 0).arg(modelData.members?.length ?? 0)
text: I18n.tr("%1 printer(s)").arg(modelData.members?.length ?? 0)
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
}

View File

@@ -2100,7 +2100,6 @@ Item {
tags: ["modal", "darken", "background", "overlay"]
title: I18n.tr("Modal Background")
settingKey: "modalBackground"
iconName: "layers"
SettingsToggleRow {
tab: "theme"
@@ -2118,7 +2117,7 @@ Item {
tags: ["applications", "portal", "dark", "terminal"]
title: I18n.tr("Applications")
settingKey: "applications"
iconName: "apps"
iconName: "terminal"
SettingsToggleRow {
tab: "theme"
@@ -2335,7 +2334,6 @@ Item {
tags: ["icon", "theme", "system"]
title: I18n.tr("Icon Theme")
settingKey: "iconTheme"
iconName: "interests"
SettingsDropdownRow {
tab: "theme"
@@ -2674,7 +2672,7 @@ Item {
tags: ["system", "app", "theming", "gtk", "qt"]
title: I18n.tr("System App Theming")
settingKey: "systemAppTheming"
iconName: "brush"
iconName: "extension"
visible: Theme.matugenAvailable
Row {

View File

@@ -9,22 +9,6 @@ import qs.Modules.Settings.Widgets
Item {
id: root
readonly property string _systemDefaultLabel: I18n.tr("System Default")
function weekStartQt() {
if (SettingsData.firstDayOfWeek < 0 || SettingsData.firstDayOfWeek >= 7)
return Qt.locale().firstDayOfWeek;
return SettingsData.firstDayOfWeek;
}
function weekStartJs() {
return weekStartQt() % 7;
}
function _dayNames() {
return Array(7).fill(0).map((_, i) => new Date(Date.UTC(2026, 2, 1 + i, 0, 0, 0)).toLocaleDateString(I18n.locale(), "dddd")).map(d => d[0].toUpperCase() + d.slice(1));
}
DankFlickable {
anchors.fill: parent
clip: true
@@ -85,39 +69,12 @@ Item {
settingKey: "dateFormat"
iconName: "calendar_today"
SettingsDropdownRow {
tab: "time"
tags: ["first", "day", "week"]
settingKey: "firstDayOfWeek"
text: I18n.tr("First Day of Week")
options: [root._systemDefaultLabel].concat(root._dayNames())
currentValue: {
if (SettingsData.firstDayOfWeek < 0 || SettingsData.firstDayOfWeek >= 7)
return root._systemDefaultLabel;
return root._dayNames()[root.weekStartJs()];
}
onValueChanged: value => {
if (value === root._systemDefaultLabel) {
SettingsData.set("firstDayOfWeek", -1);
return;
}
SettingsData.set("firstDayOfWeek", root._dayNames().indexOf(value));
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
SettingsDropdownRow {
tab: "time"
tags: ["date", "format", "topbar"]
settingKey: "clockDateFormat"
text: I18n.tr("Top Bar Format")
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(I18n.locale(), "ddd d"))
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
options: [I18n.tr("System Default", "date format option"), I18n.tr("Day Date", "date format option"), I18n.tr("Day Month Date", "date format option"), I18n.tr("Month Date", "date format option"), I18n.tr("Numeric (M/D)", "date format option"), I18n.tr("Numeric (D/M)", "date format option"), I18n.tr("Full with Year", "date format option"), I18n.tr("ISO Date", "date format option"), I18n.tr("Full Day & Month", "date format option"), I18n.tr("Custom...", "date format option")]
currentValue: {
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0)
@@ -204,7 +161,7 @@ Item {
tags: ["date", "format", "lock", "screen"]
settingKey: "lockDateFormat"
text: I18n.tr("Lock Screen Format")
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(I18n.locale(), Locale.LongFormat))
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
options: [I18n.tr("System Default", "date format option"), I18n.tr("Day Date", "date format option"), I18n.tr("Day Month Date", "date format option"), I18n.tr("Month Date", "date format option"), I18n.tr("Numeric (M/D)", "date format option"), I18n.tr("Numeric (D/M)", "date format option"), I18n.tr("Full with Year", "date format option"), I18n.tr("ISO Date", "date format option"), I18n.tr("Full Day & Month", "date format option"), I18n.tr("Custom...", "date format option")]
currentValue: {
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0)

View File

@@ -55,180 +55,6 @@ Item {
anchors.horizontalCenter: parent.horizontalCenter
spacing: Theme.spacingXL
SettingsCard {
tab: "typography"
tags: ["animation", "variant", "style", "slide", "fluent", "dynamic", "motion"]
title: I18n.tr("Animation Style")
settingKey: "animationVariant"
iconName: "auto_awesome_motion"
Item {
width: parent.width
height: animVariantGroup.implicitHeight
clip: true
DankButtonGroup {
id: animVariantGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Material"), I18n.tr("Fluent"), I18n.tr("Dynamic")]
selectionMode: "single"
currentIndex: SettingsData.animationVariant
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("animationVariant", index);
}
Connections {
target: SettingsData
function onAnimationVariantChanged() {
animVariantGroup.currentIndex = SettingsData.animationVariant;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: variantDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: variantDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.animationVariant) {
case 1:
return I18n.tr("Fluent: Smooth cubic deceleration in, quick snap out — clean, elegant curves.");
case 2:
return I18n.tr("Dynamic: Spring bezier with overshoot — entry briefly exceeds its target then settles. Expressive and alive.");
default:
return I18n.tr("Material: Material Design 3 Expressive bezier curves. The DMS default feel.");
}
}
}
}
}
SettingsCard {
tab: "typography"
tags: ["animation", "motion", "effect", "slide", "directional", "depth", "spring", "physics"]
title: I18n.tr("Motion Effects")
settingKey: "motionEffect"
iconName: "motion_photos_on"
Item {
width: parent.width
height: motionEffectGroup.implicitHeight
clip: true
DankButtonGroup {
id: motionEffectGroup
anchors.horizontalCenter: parent.horizontalCenter
buttonPadding: parent.width < 480 ? Theme.spacingS : Theme.spacingL
minButtonWidth: parent.width < 480 ? 64 : 96
textSize: parent.width < 480 ? Theme.fontSizeSmall : Theme.fontSizeMedium
model: [I18n.tr("Standard"), I18n.tr("Directional"), I18n.tr("Depth")]
selectionMode: "single"
currentIndex: SettingsData.motionEffect
onSelectionChanged: (index, selected) => {
if (!selected)
return;
SettingsData.set("motionEffect", index);
}
Connections {
target: SettingsData
function onMotionEffectChanged() {
motionEffectGroup.currentIndex = SettingsData.motionEffect;
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
}
Item {
width: parent.width
height: motionEffectDescription.implicitHeight + Theme.spacingS * 2
StyledText {
id: motionEffectDescription
x: Theme.spacingM
y: Theme.spacingS
width: parent.width - Theme.spacingM * 2
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
wrapMode: Text.WordWrap
text: {
switch (SettingsData.motionEffect) {
case 1:
return I18n.tr("Directional: Panels glide in from a larger distance at full size — no scale change, pure clean motion.");
case 2:
return I18n.tr("Depth: Panels scale up from small as they slide in — a dramatic pop-forward depth effect.");
default:
return I18n.tr("Standard: Classic Material Design 3 — panels rise from below with a subtle scale. The DMS default.");
}
}
}
}
Rectangle {
width: parent.width
height: 1
color: Theme.outline
opacity: 0.15
visible: SettingsData.motionEffect === 1
}
SettingsDropdownRow {
visible: SettingsData.motionEffect === 1
tab: "typography"
tags: ["animation", "directional", "behavior", "overlap", "sticky", "roll"]
settingKey: "directionalAnimationMode"
text: I18n.tr("Directional Behavior")
description: I18n.tr("How the popout emerges from the DankBar")
options: [I18n.tr("Overlap"), I18n.tr("Slide"), I18n.tr("Roll")]
currentValue: {
switch (SettingsData.directionalAnimationMode) {
case 1:
return I18n.tr("Slide");
case 2:
return I18n.tr("Roll");
default:
return I18n.tr("Overlap");
}
}
onValueChanged: value => {
if (value === I18n.tr("Slide"))
SettingsData.set("directionalAnimationMode", 1);
else if (value === I18n.tr("Roll"))
SettingsData.set("directionalAnimationMode", 2);
else
SettingsData.set("directionalAnimationMode", 0);
}
}
}
SettingsCard {
tab: "typography"
tags: ["font", "family", "text", "typography"]

View File

@@ -135,7 +135,7 @@ Variants {
Timer {
id: renderSettleTimer
interval: 1000
interval: 100
onTriggered: root._renderSettling = false
}

View File

@@ -121,9 +121,9 @@ Scope {
Behavior on opacity {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
@@ -154,69 +154,45 @@ Scope {
id: scaleTransform
origin.x: contentContainer.width / 2
origin.y: contentContainer.height / 2
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
xScale: overviewScope.overviewOpen ? 1 : 0.96
yScale: overviewScope.overviewOpen ? 1 : 0.96
Behavior on xScale {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
Behavior on yScale {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
Translate {
id: motionTransform
x: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return 0;
if (Theme.isDepthEffect)
return Theme.effectAnimOffset * 0.25;
return 0;
}
y: {
if (overviewScope.overviewOpen)
return 0;
if (Theme.isDirectionalEffect)
return -Math.max(contentContainer.height * 0.8, Theme.effectAnimOffset * 1.1);
if (Theme.isDepthEffect)
return Math.max(Theme.effectAnimOffset * 0.85, 28);
return Theme.effectAnimOffset;
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
x: 0
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}
}
Behavior on opacity {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
}
}

View File

@@ -202,18 +202,8 @@ Scope {
Item {
id: spotlightContainer
readonly property bool directionalEffect: Theme.isDirectionalEffect
readonly property bool depthEffect: Theme.isDepthEffect
readonly property real collapsedMotionX: depthEffect ? Theme.effectAnimOffset * 0.25 : 0
readonly property real collapsedMotionY: {
if (directionalEffect)
return Math.max(height * 0.85, Theme.effectAnimOffset * 1.1);
if (depthEffect)
return Math.max(Theme.effectAnimOffset * 0.8, 30);
return 0;
}
x: Theme.snap((parent.width - width) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionX), overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2 + (overlayWindow.shouldShowSpotlight ? 0 : collapsedMotionY), overlayWindow.dpr)
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
readonly property int baseWidth: {
switch (SettingsData.dankLauncherV2Size) {
@@ -244,8 +234,8 @@ Scope {
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
visible: overlayWindow.shouldShowSpotlight || animatingOut
enabled: overlayWindow.shouldShowSpotlight
@@ -255,11 +245,10 @@ Scope {
Behavior on scale {
id: scaleAnimation
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
onRunningChanged: {
if (running || !spotlightContainer.animatingOut)
return;
@@ -269,27 +258,10 @@ Scope {
}
Behavior on opacity {
enabled: !Theme.isDirectionalEffect
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
duration: Theme.expressiveDurations.fast
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
easing.type: Easing.BezierSpline
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
}
}

View File

@@ -62,30 +62,30 @@ Item {
Behavior on x {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on y {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
@@ -124,16 +124,16 @@ Item {
Behavior on width {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
Behavior on height {
NumberAnimation {
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.variantModalEnterCurve
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}

View File

@@ -128,7 +128,7 @@ DesktopPluginComponent {
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
@@ -163,7 +163,7 @@ DesktopPluginComponent {
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: timeText.bottom
anchors.topMargin: Theme.spacingXS
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
font.pixelSize: digitalRoot.dateFontSize
color: Theme.surfaceText
}

View File

@@ -687,12 +687,6 @@ Singleton {
appCategories.forEach(cat => categories.add(cat));
}
// Include categories from core apps (e.g. DMS Settings)
for (const app of coreApps) {
const appCategories = getCategoriesForApp(app);
appCategories.forEach(cat => categories.add(cat));
}
const pluginCategories = getPluginCategories();
pluginCategories.forEach(cat => categories.add(cat));

View File

@@ -7,7 +7,6 @@ import Quickshell
import Quickshell.Io
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
Singleton {
id: root
@@ -15,7 +14,7 @@ Singleton {
readonly property PwNode sink: Pipewire.defaultAudioSink
readonly property PwNode source: Pipewire.defaultAudioSource
readonly property bool soundsAvailable: MultimediaService.available
property bool soundsAvailable: false
property bool gsettingsAvailable: false
property var availableSoundThemes: []
property string currentSoundTheme: ""
@@ -313,6 +312,24 @@ EOFCONFIG
}
}
function detectSoundsAvailability() {
try {
const testObj = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
Item {}
`, root, "AudioService.TestComponent");
if (testObj) {
testObj.destroy();
}
soundsAvailable = true;
return true;
} catch (e) {
soundsAvailable = false;
return false;
}
}
function checkGsettings() {
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
gsettingsAvailable = (exitCode === 0);
@@ -1011,7 +1028,10 @@ EOFCONFIG
}
Component.onCompleted: {
if (soundsAvailable) {
if (!detectSoundsAvailability()) {
console.warn("AudioService: QtMultimedia not available - sound effects disabled");
} else {
console.info("AudioService: Sound effects enabled");
checkGsettings();
Qt.callLater(createSoundPlayers);
}

View File

@@ -56,8 +56,8 @@ Singleton {
}
readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging)
// Is the system plugged in (Is not running on battery)
readonly property bool isPluggedIn: !UPower.onBattery
// Is the system plugged in (none of the batteries are discharging or empty)
readonly property bool isPluggedIn: batteryAvailable && batteries.every(b => b.state !== UPowerDeviceState.Discharging)
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
onIsPluggedInChanged: {

View File

@@ -157,12 +157,12 @@ Singleton {
// Parse start and end dates using detected format
let startDate, endDate
if (event['start-date']) {
startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.khalDateFormat)
startDate = Date.fromLocaleString(Qt.locale(), event['start-date'], root.khalDateFormat)
} else {
startDate = new Date()
}
if (event['end-date']) {
endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.khalDateFormat)
endDate = Date.fromLocaleString(Qt.locale(), event['end-date'], root.khalDateFormat)
} else {
endDate = new Date(startDate)
}

View File

@@ -479,21 +479,6 @@ Singleton {
});
}
function testConnection(host, port, protocol, callback) {
if (!cupsAvailable)
return;
const params = {
"host": host,
"port": port,
"protocol": protocol
};
DMSService.sendRequest("cups.testConnection", params, response => {
if (callback)
callback(response);
});
}
function createPrinter(name, deviceURI, ppd, options) {
if (!cupsAvailable)
return;

View File

@@ -61,13 +61,12 @@ Singleton {
signal appPickerRequested(var data)
signal screensaverStateUpdate(var data)
signal clipboardStateUpdate(var data)
signal locationStateUpdate(var data)
property bool capsLockState: false
property bool screensaverInhibited: false
property var screensaverInhibitors: []
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location"]
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard"]
Component.onCompleted: {
if (socketPath && socketPath.length > 0) {
@@ -285,7 +284,7 @@ Singleton {
function removeSubscription(service) {
if (activeSubscriptions.includes("all")) {
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser", "location"];
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser"];
const filtered = allServices.filter(s => s !== service);
subscribe(filtered);
} else {
@@ -307,7 +306,7 @@ Singleton {
excludeServices = [excludeServices];
}
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus", "location"];
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
const filtered = allServices.filter(s => !excludeServices.includes(s));
subscribe(filtered);
}
@@ -396,8 +395,6 @@ Singleton {
dbusSignalReceived(data.subscriptionId || "", data);
} else if (service === "clipboard") {
clipboardStateUpdate(data);
} else if (service === "location") {
locationStateUpdate(data);
}
}

View File

@@ -1,55 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
readonly property bool locationAvailable: DMSService.isConnected && (DMSService.capabilities.length === 0 || DMSService.capabilities.includes("location"))
readonly property bool valid: latitude !== 0 || longitude !== 0
property var latitude: 0.0
property var longitude: 0.0
signal locationChanged(var data)
onLocationAvailableChanged: {
if (locationAvailable && !valid)
getState();
}
Connections {
target: DMSService
function onLocationStateUpdate(data) {
if (!locationAvailable)
return;
handleStateUpdate(data);
}
}
function handleStateUpdate(data) {
const lat = data.latitude;
const lon = data.longitude;
if (lat === 0 && lon === 0)
return;
root.latitude = lat;
root.longitude = lon;
root.locationChanged(data);
}
function getState() {
if (!locationAvailable)
return;
DMSService.sendRequest("location.getState", null, response => {
if (response.result)
handleStateUpdate(response.result);
});
}
}

View File

@@ -1,35 +0,0 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
Singleton {
id: root
property bool available: false
function detectAvailability() {
try {
const testObj = Qt.createQmlObject(`
import QtQuick
import QtMultimedia
Item {}
`, root, "MultimediaService.TestComponent");
if (testObj) {
testObj.destroy();
}
available = true;
return true;
} catch (e) {
available = false;
return false;
}
}
Component.onCompleted: {
if (!detectAvailability()) {
console.warn("MultimediaService: QtMultimedia not available");
}
}
}

View File

@@ -19,7 +19,6 @@ Singleton {
readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json"
readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images"
property bool historyLoaded: false
property int historyEntryCounter: 0
property list<NotifWrapper> notificationQueue: []
property list<NotifWrapper> visibleNotifications: []
@@ -74,12 +73,6 @@ Singleton {
onTriggered: root.performSaveHistory()
}
function _makeHistoryEntryId(sourceId, timestamp) {
historyEntryCounter += 1;
const safeSource = sourceId && sourceId !== "" ? sourceId : "notification";
return safeSource + "_" + (timestamp || Date.now()) + "_" + historyEntryCounter;
}
function getImageCachePath(wrapper) {
const ts = wrapper.time ? wrapper.time.getTime() : Date.now();
const id = wrapper.notification?.id?.toString() || "0";
@@ -87,13 +80,12 @@ Singleton {
}
function updateHistoryImage(wrapperId, imagePath) {
const idx = historyList.findIndex(n => n.sourceNotificationId === wrapperId || n.id === wrapperId);
const idx = historyList.findIndex(n => n.id === wrapperId);
if (idx < 0)
return;
const item = historyList[idx];
const updated = {
id: item.id,
sourceNotificationId: item.sourceNotificationId || item.id,
summary: item.summary,
body: item.body,
htmlBody: item.htmlBody,
@@ -121,11 +113,8 @@ Singleton {
} else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) {
persistableImage = imageUrl;
}
const sourceNotificationId = wrapper.notification?.id?.toString() || "";
const timestamp = wrapper.time.getTime();
const data = {
id: _makeHistoryEntryId(sourceNotificationId, timestamp),
sourceNotificationId: sourceNotificationId,
id: wrapper.notification?.id?.toString() || Date.now().toString(),
summary: wrapper.summary || "",
body: wrapper.body || "",
htmlBody: wrapper.htmlBody || wrapper.body || "",
@@ -133,7 +122,7 @@ Singleton {
appIcon: wrapper.appIcon || "",
image: persistableImage,
urgency: urg,
timestamp: timestamp,
timestamp: wrapper.time.getTime(),
desktopEntry: wrapper.desktopEntry || ""
};
let newList = [data, ...historyList];
@@ -163,8 +152,6 @@ Singleton {
const now = Date.now();
const maxAgeMs = maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : 0;
const loaded = [];
const seenIds = {};
let needsRewrite = false;
for (const item of historyAdapter.notifications || []) {
if (maxAgeMs > 0 && (now - item.timestamp) > maxAgeMs)
@@ -175,18 +162,8 @@ Singleton {
if (htmlBody) {
htmlBody = htmlBody.replace(/<img\b[^>]*>/gi, "");
}
const sourceNotificationId = (item.sourceNotificationId || item.id || "").toString();
let historyId = (item.id || "").toString();
if (!historyId || seenIds[historyId]) {
historyId = _makeHistoryEntryId(sourceNotificationId, item.timestamp || now);
needsRewrite = true;
}
if (!item.sourceNotificationId)
needsRewrite = true;
seenIds[historyId] = true;
loaded.push({
id: historyId,
sourceNotificationId: sourceNotificationId,
id: item.id || "",
summary: item.summary || "",
body: body,
htmlBody: htmlBody,
@@ -200,7 +177,7 @@ Singleton {
}
historyList = loaded;
historyLoaded = true;
if ((maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length) || needsRewrite)
if (maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length)
saveHistory();
} catch (e) {
console.warn("NotificationService: load history failed:", e);
@@ -275,7 +252,7 @@ Singleton {
if (daysDiff === 0)
return timeStr;
try {
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
const weekday = date.toLocaleDateString(localeName, {
weekday: "long"
});
@@ -718,7 +695,7 @@ Singleton {
}
try {
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
const weekday = time.toLocaleDateString(localeName, {
weekday: "long"
});

View File

@@ -44,26 +44,24 @@ Singleton {
}
}
readonly property var archBasedPMSettings: function(requiresSudo) {
return {
"listUpdatesSettings": {
"params": ["-Qu"],
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
},
"upgradeSettings": {
"params": ["-Syu"],
"requiresSudo": requiresSudo
},
"parserSettings": {
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": match[2],
"newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}`
};
}
readonly property var archBasedPMSettings: {
"listUpdatesSettings": {
"params": ["-Qu"],
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
},
"upgradeSettings": {
"params": ["-Syu"],
"requiresSudo": false
},
"parserSettings": {
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
"entryProducer": function (match) {
return {
"name": match[1],
"currentVersion": match[2],
"newVersion": match[3],
"description": `${match[1]} ${match[2]} ${match[3]}`
};
}
}
}
@@ -94,9 +92,8 @@ Singleton {
"checkupdates": archBasedUCSettings
}
readonly property var packageManagerParams: {
"yay": archBasedPMSettings(false),
"paru": archBasedPMSettings(false),
"pacman": archBasedPMSettings(true),
"yay": archBasedPMSettings,
"paru": archBasedPMSettings,
"dnf": fedoraBasedPMSettings
}
readonly property list<string> supportedDistributions: ["arch", "artix", "cachyos", "manjaro", "endeavouros", "fedora"]
@@ -185,7 +182,7 @@ Singleton {
Process {
id: pkgManagerDetection
command: ["sh", "-c", "which paru || which yay || which pacman || which dnf"]
command: ["sh", "-c", "which paru || which yay || which dnf"]
onExited: exitCode => {
if (exitCode === 0) {
@@ -262,7 +259,7 @@ Singleton {
const terminal = Quickshell.env("TERMINAL") || "xterm";
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
const updateCommand = `${SettingsData.updaterCustomCommand} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`;
const termClass = SettingsData.updaterTerminalAdditionalParams;
var finalCommand = [terminal];
@@ -277,7 +274,7 @@ Singleton {
} else {
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ");
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : "";
const updateCommand = `${sudo} ${pkgManager} ${params} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`;
updater.command = [terminal, "-e", "sh", "-c", updateCommand];
}

Some files were not shown because too many files have changed in this diff Show More