mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-12 15:29:43 -04:00
Compare commits
35 Commits
1452c85946
..
anims
| Author | SHA1 | Date | |
|---|---|---|---|
| 62bf9c6efe | |||
| 61a77bd186 | |||
| e04c919d78 | |||
| 246b6c44b0 | |||
| 847ddf7d38 | |||
| 16e8199f9e | |||
| 7d1519f546 | |||
| 1bf66ee482 | |||
| 39a43f4de5 | |||
| 971a511edb | |||
| 0f8e0bc2b4 | |||
| 537c44e354 | |||
| db53a9a719 | |||
| f4a10de790 | |||
| 8c9fe84d02 | |||
| f0fcc77bdb | |||
| cf4c4b7d69 | |||
| 7bb8499353 | |||
| ee1a2bc7de | |||
| 20d383d4ab | |||
| 9cb0d8baf2 | |||
| 362ded3bc9 | |||
| 654f2ec7ad | |||
| 3600e034b8 | |||
| d7c501e175 | |||
| b9e9da579f | |||
| 7bea6b4a62 | |||
| ab211266a6 | |||
| 4da22a4345 | |||
| fbc1ff62c7 | |||
| 1fe72e1a66 | |||
| f82d7610e3 | |||
| bd6ad53875 | |||
| 5d09acca4c | |||
| b4e7c4a4cd |
@@ -28,6 +28,12 @@ packages:
|
|||||||
outpkg: mocks_brightness
|
outpkg: mocks_brightness
|
||||||
interfaces:
|
interfaces:
|
||||||
DBusConn:
|
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:
|
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||||
config:
|
config:
|
||||||
dir: "internal/mocks/network"
|
dir: "internal/mocks/network"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/golangci/golangci-lint
|
- repo: https://github.com/golangci/golangci-lint
|
||||||
rev: v2.9.0
|
rev: v2.10.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: golangci-lint-fmt
|
- id: golangci-lint-fmt
|
||||||
require_serial: true
|
require_serial: true
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,243 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package geolocation
|
||||||
|
|
||||||
|
type Location struct {
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
GetLocation() (Location, error)
|
||||||
|
|
||||||
|
Subscribe(id string) chan Location
|
||||||
|
Unsubscribe(id string)
|
||||||
|
|
||||||
|
Close()
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@ package cups
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -275,13 +277,42 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
|
|||||||
return classes, nil
|
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 {
|
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
|
||||||
usedPkHelper := false
|
usedPkHelper := false
|
||||||
|
|
||||||
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
||||||
if isAuthError(err) && m.pkHelper != nil {
|
if isAuthError(err) && m.pkHelper != nil {
|
||||||
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
|
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
|
||||||
return err
|
// 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
|
||||||
}
|
}
|
||||||
usedPkHelper = true
|
usedPkHelper = true
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
@@ -308,6 +339,12 @@ func (m *Manager) DeletePrinter(printerName string) error {
|
|||||||
err := m.client.DeletePrinter(printerName)
|
err := m.client.DeletePrinter(printerName)
|
||||||
if isAuthError(err) && m.pkHelper != nil {
|
if isAuthError(err) && m.pkHelper != nil {
|
||||||
err = m.pkHelper.PrinterDelete(printerName)
|
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 {
|
if err == nil {
|
||||||
m.RefreshState()
|
m.RefreshState()
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
handleRestartJob(conn, req, manager)
|
handleRestartJob(conn, req, manager)
|
||||||
case "cups.holdJob":
|
case "cups.holdJob":
|
||||||
handleHoldJob(conn, req, manager)
|
handleHoldJob(conn, req, manager)
|
||||||
|
case "cups.testConnection":
|
||||||
|
handleTestConnection(conn, req, manager)
|
||||||
default:
|
default:
|
||||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||||
}
|
}
|
||||||
@@ -464,3 +466,22 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
|||||||
}
|
}
|
||||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
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)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -55,6 +55,16 @@ type PPD struct {
|
|||||||
Type string `json:"type"`
|
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 {
|
type PrinterClass struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
URI string `json:"uri"`
|
URI string `json:"uri"`
|
||||||
@@ -77,6 +87,7 @@ type Manager struct {
|
|||||||
notifierWg sync.WaitGroup
|
notifierWg sync.WaitGroup
|
||||||
lastNotifiedState *CUPSState
|
lastNotifiedState *CUPSState
|
||||||
baseURL string
|
baseURL string
|
||||||
|
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SubscriptionManagerInterface interface {
|
type SubscriptionManagerInterface interface {
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
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, ¤tState) {
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -5,5 +5,6 @@ const (
|
|||||||
dbusPath = "/org/freedesktop/login1"
|
dbusPath = "/org/freedesktop/login1"
|
||||||
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
||||||
dbusSessionInterface = "org.freedesktop.login1.Session"
|
dbusSessionInterface = "org.freedesktop.login1.Session"
|
||||||
|
dbusUserInterface = "org.freedesktop.login1.User"
|
||||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -17,15 +17,8 @@ func NewManager() (*Manager, error) {
|
|||||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sessionID := os.Getenv("XDG_SESSION_ID")
|
|
||||||
if sessionID == "" {
|
|
||||||
sessionID = "self"
|
|
||||||
}
|
|
||||||
|
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
state: &SessionState{
|
state: &SessionState{},
|
||||||
SessionID: sessionID,
|
|
||||||
},
|
|
||||||
stateMutex: sync.RWMutex{},
|
stateMutex: sync.RWMutex{},
|
||||||
|
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
@@ -60,12 +53,13 @@ func (m *Manager) initialize() error {
|
|||||||
|
|
||||||
m.initializeFallbackDelay()
|
m.initializeFallbackDelay()
|
||||||
|
|
||||||
sessionPath, err := m.getSession(m.state.SessionID)
|
sessionID, sessionPath, err := m.discoverSession()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get session path: %w", err)
|
return fmt.Errorf("failed to get session path: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
m.stateMutex.Lock()
|
m.stateMutex.Lock()
|
||||||
|
m.state.SessionID = sessionID
|
||||||
m.state.SessionPath = string(sessionPath)
|
m.state.SessionPath = string(sessionPath)
|
||||||
m.sessionPath = sessionPath
|
m.sessionPath = sessionPath
|
||||||
m.stateMutex.Unlock()
|
m.stateMutex.Unlock()
|
||||||
@@ -79,6 +73,41 @@ func (m *Manager) initialize() error {
|
|||||||
return nil
|
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) {
|
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
||||||
var out dbus.ObjectPath
|
var out dbus.ObjectPath
|
||||||
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
||||||
@@ -88,6 +117,166 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
|||||||
return out, nil
|
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 {
|
func (m *Manager) refreshSessionBinding() error {
|
||||||
if m.managerObj == nil || m.conn == nil {
|
if m.managerObj == nil || m.conn == nil {
|
||||||
return fmt.Errorf("manager not fully initialized")
|
return fmt.Errorf("manager not fully initialized")
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"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/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
@@ -192,6 +193,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
|||||||
return
|
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 {
|
switch req.Method {
|
||||||
case "ping":
|
case "ping":
|
||||||
models.Respond(conn, req.ID, "pong")
|
models.Respond(conn, req.ID, "pong")
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||||
@@ -25,6 +26,7 @@ import (
|
|||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
"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/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||||
@@ -70,6 +72,7 @@ var clipboardManager *clipboard.Manager
|
|||||||
var dbusManager *serverDbus.Manager
|
var dbusManager *serverDbus.Manager
|
||||||
var wlContext *wlcontext.SharedContext
|
var wlContext *wlcontext.SharedContext
|
||||||
var themeModeManager *thememode.Manager
|
var themeModeManager *thememode.Manager
|
||||||
|
var locationManager *location.Manager
|
||||||
|
|
||||||
const dbusClientID = "dms-dbus-client"
|
const dbusClientID = "dms-dbus-client"
|
||||||
|
|
||||||
@@ -188,7 +191,7 @@ func InitializeFreedeskManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeWaylandManager() error {
|
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||||
log.Info("Attempting to initialize Wayland gamma control...")
|
log.Info("Attempting to initialize Wayland gamma control...")
|
||||||
|
|
||||||
if wlContext == nil {
|
if wlContext == nil {
|
||||||
@@ -201,7 +204,7 @@ func InitializeWaylandManager() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
config := wayland.DefaultConfig()
|
config := wayland.DefaultConfig()
|
||||||
manager, err := wayland.NewManager(wlContext.Display(), config)
|
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Failed to initialize wayland manager: %v", err)
|
log.Errorf("Failed to initialize wayland manager: %v", err)
|
||||||
return err
|
return err
|
||||||
@@ -382,14 +385,27 @@ func InitializeDbusManager() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitializeThemeModeManager() error {
|
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
||||||
manager := thememode.NewManager()
|
manager := thememode.NewManager(geoClient)
|
||||||
themeModeManager = manager
|
themeModeManager = manager
|
||||||
|
|
||||||
log.Info("Theme mode automation manager initialized")
|
log.Info("Theme mode automation manager initialized")
|
||||||
return nil
|
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) {
|
func handleConnection(conn net.Conn) {
|
||||||
defer conn.Close()
|
defer conn.Close()
|
||||||
|
|
||||||
@@ -537,6 +553,10 @@ func getServerInfo() ServerInfo {
|
|||||||
caps = append(caps, "theme.auto")
|
caps = append(caps, "theme.auto")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if locationManager != nil {
|
||||||
|
caps = append(caps, "location")
|
||||||
|
}
|
||||||
|
|
||||||
if dbusManager != nil {
|
if dbusManager != nil {
|
||||||
caps = append(caps, "dbus")
|
caps = append(caps, "dbus")
|
||||||
}
|
}
|
||||||
@@ -1307,6 +1327,9 @@ func cleanupManagers() {
|
|||||||
if wlContext != nil {
|
if wlContext != nil {
|
||||||
wlContext.Close()
|
wlContext.Close()
|
||||||
}
|
}
|
||||||
|
if locationManager != nil {
|
||||||
|
locationManager.Close()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Start(printDocs bool) error {
|
func Start(printDocs bool) error {
|
||||||
@@ -1488,6 +1511,9 @@ func Start(printDocs bool) error {
|
|||||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
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("")
|
||||||
}
|
}
|
||||||
log.Info("Initializing managers...")
|
log.Info("Initializing managers...")
|
||||||
@@ -1519,6 +1545,9 @@ func Start(printDocs bool) error {
|
|||||||
loginctlReady := make(chan struct{})
|
loginctlReady := make(chan struct{})
|
||||||
freedesktopReady := make(chan struct{})
|
freedesktopReady := make(chan struct{})
|
||||||
|
|
||||||
|
geoClient := geolocation.NewClient()
|
||||||
|
defer geoClient.Close()
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
defer close(loginctlReady)
|
defer close(loginctlReady)
|
||||||
if err := InitializeLoginctlManager(); err != nil {
|
if err := InitializeLoginctlManager(); err != nil {
|
||||||
@@ -1563,7 +1592,7 @@ func Start(printDocs bool) error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
if err := InitializeWaylandManager(); err != nil {
|
if err := InitializeWaylandManager(geoClient); err != nil {
|
||||||
log.Warnf("Wayland manager unavailable: %v", err)
|
log.Warnf("Wayland manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1595,7 +1624,7 @@ func Start(printDocs bool) error {
|
|||||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := InitializeThemeModeManager(); err != nil {
|
if err := InitializeThemeModeManager(geoClient); err != nil {
|
||||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||||
} else {
|
} else {
|
||||||
notifyCapabilityChange()
|
notifyCapabilityChange()
|
||||||
@@ -1608,6 +1637,12 @@ 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)
|
fatalErrChan := make(chan error, 1)
|
||||||
if wlrOutputManager != nil {
|
if wlrOutputManager != nil {
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
@@ -32,12 +33,14 @@ type Manager struct {
|
|||||||
cachedIPLat *float64
|
cachedIPLat *float64
|
||||||
cachedIPLon *float64
|
cachedIPLon *float64
|
||||||
|
|
||||||
|
geoClient geolocation.Client
|
||||||
|
|
||||||
stopChan chan struct{}
|
stopChan chan struct{}
|
||||||
updateTrigger chan struct{}
|
updateTrigger chan struct{}
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewManager() *Manager {
|
func NewManager(geoClient geolocation.Client) *Manager {
|
||||||
m := &Manager{
|
m := &Manager{
|
||||||
config: Config{
|
config: Config{
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
@@ -51,6 +54,7 @@ func NewManager() *Manager {
|
|||||||
},
|
},
|
||||||
stopChan: make(chan struct{}),
|
stopChan: make(chan struct{}),
|
||||||
updateTrigger: make(chan struct{}, 1),
|
updateTrigger: make(chan struct{}, 1),
|
||||||
|
geoClient: geoClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
m.updateState(time.Now())
|
m.updateState(time.Now())
|
||||||
@@ -327,17 +331,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
|||||||
}
|
}
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
lat, lon, err := wayland.FetchIPLocation()
|
location, err := m.geoClient.GetLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = lat
|
m.cachedIPLat = &location.Latitude
|
||||||
m.cachedIPLon = lon
|
m.cachedIPLon = &location.Longitude
|
||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
|
|
||||||
return lat, lon
|
return m.cachedIPLat, m.cachedIPLon
|
||||||
}
|
}
|
||||||
|
|
||||||
func statesEqual(a, b *State) bool {
|
func statesEqual(a, b *State) bool {
|
||||||
|
|||||||
@@ -13,13 +13,14 @@ import (
|
|||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"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/log"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||||
)
|
)
|
||||||
|
|
||||||
const animKelvinStep = 25
|
const animKelvinStep = 25
|
||||||
|
|
||||||
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
|
||||||
if err := config.Validate(); err != nil {
|
if err := config.Validate(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -40,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
|||||||
updateTrigger: make(chan struct{}, 1),
|
updateTrigger: make(chan struct{}, 1),
|
||||||
dirty: make(chan struct{}, 1),
|
dirty: make(chan struct{}, 1),
|
||||||
dbusSignal: make(chan *dbus.Signal, 16),
|
dbusSignal: make(chan *dbus.Signal, 16),
|
||||||
|
geoClient: geoClient,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := m.setupRegistry(); err != nil {
|
if err := m.setupRegistry(); err != nil {
|
||||||
@@ -437,15 +439,16 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
|||||||
}
|
}
|
||||||
m.locationMutex.RUnlock()
|
m.locationMutex.RUnlock()
|
||||||
|
|
||||||
lat, lon, err := FetchIPLocation()
|
location, err := m.geoClient.GetLocation()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
m.locationMutex.Lock()
|
m.locationMutex.Lock()
|
||||||
m.cachedIPLat = lat
|
m.cachedIPLat = &location.Latitude
|
||||||
m.cachedIPLon = lon
|
m.cachedIPLon = &location.Longitude
|
||||||
m.locationMutex.Unlock()
|
m.locationMutex.Unlock()
|
||||||
return lat, lon
|
return m.cachedIPLat, m.cachedIPLon
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"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"
|
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
|||||||
|
|
||||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
func TestNewManager_GetRegistryError(t *testing.T) {
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||||
|
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||||
|
|
||||||
mockDisplay.EXPECT().Context().Return(nil)
|
mockDisplay.EXPECT().Context().Return(nil)
|
||||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
||||||
|
|
||||||
config := DefaultConfig()
|
config := DefaultConfig()
|
||||||
_, err := NewManager(mockDisplay, config)
|
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "get registry")
|
assert.Contains(t, err.Error(), "get registry")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
func TestNewManager_InvalidConfig(t *testing.T) {
|
||||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||||
|
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||||
|
|
||||||
config := Config{
|
config := Config{
|
||||||
LowTemp: 500,
|
LowTemp: 500,
|
||||||
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
|||||||
Gamma: 1.0,
|
Gamma: 1.0,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := NewManager(mockDisplay, config)
|
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
"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"
|
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
@@ -97,6 +98,8 @@ type Manager struct {
|
|||||||
dbusConn *dbus.Conn
|
dbusConn *dbus.Conn
|
||||||
dbusSignal chan *dbus.Signal
|
dbusSignal chan *dbus.Signal
|
||||||
|
|
||||||
|
geoClient geolocation.Client
|
||||||
|
|
||||||
lastAppliedTemp int
|
lastAppliedTemp int
|
||||||
lastAppliedGamma float64
|
lastAppliedGamma float64
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
lib,
|
lib,
|
||||||
pkgs,
|
pkgs,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -10,7 +9,7 @@ let
|
|||||||
in
|
in
|
||||||
{
|
{
|
||||||
packages = [
|
packages = [
|
||||||
dmsPkgs.dms-shell
|
cfg.package
|
||||||
]
|
]
|
||||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||||
++ lib.optionals cfg.enableVPN [
|
++ lib.optionals cfg.enableVPN [
|
||||||
|
|||||||
+18
-2
@@ -8,6 +8,7 @@
|
|||||||
let
|
let
|
||||||
inherit (lib) types;
|
inherit (lib) types;
|
||||||
cfg = config.programs.dank-material-shell.greeter;
|
cfg = config.programs.dank-material-shell.greeter;
|
||||||
|
cfgDms = config.programs.dank-material-shell;
|
||||||
|
|
||||||
inherit (config.services.greetd.settings.default_session) user;
|
inherit (config.services.greetd.settings.default_session) user;
|
||||||
|
|
||||||
@@ -29,13 +30,13 @@ let
|
|||||||
lib.escapeShellArgs (
|
lib.escapeShellArgs (
|
||||||
[
|
[
|
||||||
"sh"
|
"sh"
|
||||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||||
"--cache-dir"
|
"--cache-dir"
|
||||||
cacheDir
|
cacheDir
|
||||||
"--command"
|
"--command"
|
||||||
cfg.compositor.name
|
cfg.compositor.name
|
||||||
"-p"
|
"-p"
|
||||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
"${cfg.package}/share/quickshell/dms"
|
||||||
]
|
]
|
||||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||||
"-C"
|
"-C"
|
||||||
@@ -65,6 +66,21 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell.greeter = {
|
options.programs.dank-material-shell.greeter = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell 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 {
|
compositor.name = lib.mkOption {
|
||||||
type = types.enum [
|
type = types.enum [
|
||||||
"niri"
|
"niri"
|
||||||
|
|||||||
+1
-3
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -13,7 +12,6 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
dmsPkgs
|
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||||
@@ -96,7 +94,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
Service = {
|
Service = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
ExecStart = lib.getExe cfg.package + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
config,
|
config,
|
||||||
pkgs,
|
pkgs,
|
||||||
lib,
|
lib,
|
||||||
dmsPkgs,
|
|
||||||
...
|
...
|
||||||
}@args:
|
}@args:
|
||||||
let
|
let
|
||||||
@@ -12,7 +11,6 @@ let
|
|||||||
config
|
config
|
||||||
pkgs
|
pkgs
|
||||||
lib
|
lib
|
||||||
dmsPkgs
|
|
||||||
;
|
;
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
@@ -36,7 +34,7 @@ in
|
|||||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||||
|
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
ExecStart = lib.getExe cfg.package + " run --session";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -50,6 +48,7 @@ in
|
|||||||
|
|
||||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||||
services.accounts-daemon.enable = lib.mkDefault true;
|
services.accounts-daemon.enable = lib.mkDefault true;
|
||||||
|
services.geoclue2.enable = lib.mkDefault true;
|
||||||
security.polkit.enable = lib.mkDefault true;
|
security.polkit.enable = lib.mkDefault true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ in
|
|||||||
|
|
||||||
options.programs.dank-material-shell = {
|
options.programs.dank-material-shell = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell";
|
enable = lib.mkEnableOption "DankMaterialShell";
|
||||||
|
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
||||||
|
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
||||||
|
};
|
||||||
|
|
||||||
systemd = {
|
systemd = {
|
||||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||||
|
|||||||
@@ -0,0 +1,174 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,4 +22,9 @@ Singleton {
|
|||||||
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
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 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]
|
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]
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-17
@@ -1,5 +1,6 @@
|
|||||||
pragma Singleton
|
pragma Singleton
|
||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Qt.labs.folderlistmodel
|
import Qt.labs.folderlistmodel
|
||||||
import Quickshell
|
import Quickshell
|
||||||
@@ -8,7 +9,9 @@ import Quickshell.Io
|
|||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
readonly property string _rawLocale: Qt.locale().name
|
property string _resolvedLocale: "en"
|
||||||
|
|
||||||
|
readonly property string _rawLocale: SessionData.locale === "" ? Qt.locale().name : SessionData.locale
|
||||||
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
||||||
readonly property var _candidates: {
|
readonly property var _candidates: {
|
||||||
const fullUnderscore = _rawLocale;
|
const fullUnderscore = _rawLocale;
|
||||||
@@ -21,7 +24,10 @@ Singleton {
|
|||||||
|
|
||||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||||
|
|
||||||
property string currentLocale: "en"
|
readonly property alias folder: dir.folder
|
||||||
|
property var presentLocales: ({
|
||||||
|
"en": Qt.locale("en")
|
||||||
|
})
|
||||||
property var translations: ({})
|
property var translations: ({})
|
||||||
property bool translationsLoaded: false
|
property bool translationsLoaded: false
|
||||||
|
|
||||||
@@ -34,8 +40,10 @@ Singleton {
|
|||||||
showDirs: false
|
showDirs: false
|
||||||
showDotAndDotDot: false
|
showDotAndDotDot: false
|
||||||
|
|
||||||
onStatusChanged: if (status === FolderListModel.Ready)
|
onStatusChanged: if (status === FolderListModel.Ready) {
|
||||||
root._pickTranslation()
|
root._loadPresentLocales();
|
||||||
|
root._pickTranslation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
@@ -46,41 +54,54 @@ Singleton {
|
|||||||
try {
|
try {
|
||||||
root.translations = JSON.parse(text());
|
root.translations = JSON.parse(text());
|
||||||
root.translationsLoaded = true;
|
root.translationsLoaded = true;
|
||||||
console.info(`I18n: Loaded translations for '${root.currentLocale}' ` + `(${Object.keys(root.translations).length} contexts)`);
|
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e, "- falling back to English");
|
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
|
||||||
root._fallbackToEnglish();
|
root._fallbackToEnglish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onLoadFailed: error => {
|
onLoadFailed: error => {
|
||||||
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` + "falling back to English");
|
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
|
||||||
root._fallbackToEnglish();
|
root._fallbackToEnglish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function _pickTranslation() {
|
function locale() {
|
||||||
const present = new Set();
|
if (SessionData.timeLocale)
|
||||||
|
return Qt.locale(SessionData.timeLocale);
|
||||||
|
return Qt.locale();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadPresentLocales() {
|
||||||
|
if (Object.keys(presentLocales).length > 1) {
|
||||||
|
return; // already loaded
|
||||||
|
}
|
||||||
for (let i = 0; i < dir.count; i++) {
|
for (let i = 0; i < dir.count; i++) {
|
||||||
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
||||||
if (name && name.endsWith(".json")) {
|
if (name && name.endsWith(".json")) {
|
||||||
present.add(name.slice(0, -5));
|
const shortName = name.slice(0, -5);
|
||||||
|
presentLocales[shortName] = Qt.locale(shortName);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function _pickTranslation() {
|
||||||
for (let i = 0; i < _candidates.length; i++) {
|
for (let i = 0; i < _candidates.length; i++) {
|
||||||
const cand = _candidates[i];
|
const cand = _candidates[i];
|
||||||
if (present.has(cand)) {
|
if (presentLocales[cand] === undefined)
|
||||||
_useLocale(cand, dir.folder + "/" + cand + ".json");
|
continue;
|
||||||
return;
|
_resolvedLocale = cand;
|
||||||
}
|
useLocale(cand, cand.startsWith("en") ? "" : translationsFolder + "/" + cand + ".json");
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_resolvedLocale = "en";
|
||||||
_fallbackToEnglish();
|
_fallbackToEnglish();
|
||||||
}
|
}
|
||||||
|
|
||||||
function _useLocale(localeTag, fileUrl) {
|
function useLocale(localeTag, fileUrl) {
|
||||||
currentLocale = localeTag;
|
_resolvedLocale = localeTag || "en";
|
||||||
_selectedPath = fileUrl;
|
_selectedPath = fileUrl;
|
||||||
translationsLoaded = false;
|
translationsLoaded = false;
|
||||||
translations = ({});
|
translations = ({});
|
||||||
@@ -88,7 +109,6 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function _fallbackToEnglish() {
|
function _fallbackToEnglish() {
|
||||||
currentLocale = "en";
|
|
||||||
_selectedPath = "";
|
_selectedPath = "";
|
||||||
translationsLoaded = false;
|
translationsLoaded = false;
|
||||||
translations = ({});
|
translations = ({});
|
||||||
|
|||||||
@@ -71,15 +71,40 @@ Singleton {
|
|||||||
return appId;
|
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 {
|
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||||
if (appId === "org.quickshell") {
|
if (appId === "org.quickshell") {
|
||||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||||
}
|
}
|
||||||
|
|
||||||
const moddedId = moddedAppId(appId);
|
const moddedId = moddedAppId(appId);
|
||||||
if (moddedId !== appId) {
|
if (moddedId !== appId)
|
||||||
return Quickshell.iconPath(moddedId, true);
|
return resolveIconPath(appId);
|
||||||
}
|
|
||||||
|
|
||||||
if (desktopEntry && desktopEntry.icon) {
|
if (desktopEntry && desktopEntry.icon) {
|
||||||
return Quickshell.iconPath(desktopEntry.icon, true);
|
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||||
|
|||||||
@@ -21,7 +21,9 @@ Singleton {
|
|||||||
property bool _isReadOnly: false
|
property bool _isReadOnly: false
|
||||||
property bool _hasUnsavedChanges: false
|
property bool _hasUnsavedChanges: false
|
||||||
property var _loadedSessionSnapshot: null
|
property var _loadedSessionSnapshot: null
|
||||||
readonly property var _hooks: ({})
|
readonly property var _hooks: ({
|
||||||
|
"updateLocale": updateLocale
|
||||||
|
})
|
||||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||||
|
|
||||||
@@ -126,6 +128,9 @@ Singleton {
|
|||||||
property var hiddenOutputDeviceNames: []
|
property var hiddenOutputDeviceNames: []
|
||||||
property var hiddenInputDeviceNames: []
|
property var hiddenInputDeviceNames: []
|
||||||
|
|
||||||
|
property string locale: ""
|
||||||
|
property string timeLocale: ""
|
||||||
|
|
||||||
property string launcherLastMode: "all"
|
property string launcherLastMode: "all"
|
||||||
property string appDrawerLastMode: "apps"
|
property string appDrawerLastMode: "apps"
|
||||||
property string niriOverviewLastMode: "apps"
|
property string niriOverviewLastMode: "apps"
|
||||||
@@ -1104,6 +1109,14 @@ Singleton {
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateLocale() {
|
||||||
|
if (!locale) {
|
||||||
|
I18n._pickTranslation();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
function setLauncherLastMode(mode) {
|
function setLauncherLastMode(mode) {
|
||||||
launcherLastMode = mode;
|
launcherLastMode = mode;
|
||||||
saveSettings();
|
saveSettings();
|
||||||
|
|||||||
@@ -37,6 +37,18 @@ Singleton {
|
|||||||
Custom
|
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 {
|
enum SuspendBehavior {
|
||||||
Suspend,
|
Suspend,
|
||||||
Hibernate,
|
Hibernate,
|
||||||
@@ -149,6 +161,7 @@ Singleton {
|
|||||||
property int mangoLayoutRadiusOverride: -1
|
property int mangoLayoutRadiusOverride: -1
|
||||||
property int mangoLayoutBorderSize: -1
|
property int mangoLayoutBorderSize: -1
|
||||||
|
|
||||||
|
property int firstDayOfWeek: -1
|
||||||
property bool use24HourClock: true
|
property bool use24HourClock: true
|
||||||
property bool showSeconds: false
|
property bool showSeconds: false
|
||||||
property bool padHours12Hour: false
|
property bool padHours12Hour: false
|
||||||
@@ -165,6 +178,12 @@ Singleton {
|
|||||||
property int modalCustomAnimationDuration: 150
|
property int modalCustomAnimationDuration: 150
|
||||||
property bool enableRippleEffects: true
|
property bool enableRippleEffects: true
|
||||||
onEnableRippleEffectsChanged: saveSettings()
|
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
|
property bool m3ElevationEnabled: true
|
||||||
onM3ElevationEnabledChanged: saveSettings()
|
onM3ElevationEnabledChanged: saveSettings()
|
||||||
property int m3ElevationIntensity: 12
|
property int m3ElevationIntensity: 12
|
||||||
@@ -512,9 +531,15 @@ Singleton {
|
|||||||
property bool enableFprint: false
|
property bool enableFprint: false
|
||||||
property int maxFprintTries: 15
|
property int maxFprintTries: 15
|
||||||
property bool fprintdAvailable: false
|
property bool fprintdAvailable: false
|
||||||
|
property bool enableU2f: false
|
||||||
|
property string u2fMode: "or"
|
||||||
|
property bool u2fAvailable: false
|
||||||
property string lockScreenActiveMonitor: "all"
|
property string lockScreenActiveMonitor: "all"
|
||||||
property string lockScreenInactiveColor: "#000000"
|
property string lockScreenInactiveColor: "#000000"
|
||||||
property int lockScreenNotificationMode: 0
|
property int lockScreenNotificationMode: 0
|
||||||
|
property bool lockScreenVideoEnabled: false
|
||||||
|
property string lockScreenVideoPath: ""
|
||||||
|
property bool lockScreenVideoCycling: false
|
||||||
property bool hideBrightnessSlider: false
|
property bool hideBrightnessSlider: false
|
||||||
|
|
||||||
property int notificationTimeoutLow: 5000
|
property int notificationTimeoutLow: 5000
|
||||||
@@ -1000,6 +1025,7 @@ Singleton {
|
|||||||
loadSettings();
|
loadSettings();
|
||||||
initializeListModels();
|
initializeListModels();
|
||||||
Processes.detectFprintd();
|
Processes.detectFprintd();
|
||||||
|
Processes.detectU2f();
|
||||||
Processes.checkPluginSettings();
|
Processes.checkPluginSettings();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1173,7 @@ Singleton {
|
|||||||
"updateCompositorLayout": updateCompositorLayout,
|
"updateCompositorLayout": updateCompositorLayout,
|
||||||
"applyStoredIconTheme": applyStoredIconTheme,
|
"applyStoredIconTheme": applyStoredIconTheme,
|
||||||
"updateBarConfigs": updateBarConfigs,
|
"updateBarConfigs": updateBarConfigs,
|
||||||
"updateCompositorCursor": updateCompositorCursor
|
"updateCompositorCursor": updateCompositorCursor,
|
||||||
})
|
})
|
||||||
|
|
||||||
function set(key, value) {
|
function set(key, value) {
|
||||||
|
|||||||
@@ -960,6 +960,24 @@ Singleton {
|
|||||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
"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: {
|
readonly property var animationPresetDurations: {
|
||||||
"none": 0,
|
"none": 0,
|
||||||
"short": 250,
|
"short": 250,
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ Singleton {
|
|||||||
fprintdDetectionProcess.running = true;
|
fprintdDetectionProcess.running = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function detectU2f() {
|
||||||
|
u2fDetectionProcess.running = true;
|
||||||
|
}
|
||||||
|
|
||||||
function checkPluginSettings() {
|
function checkPluginSettings() {
|
||||||
pluginSettingsCheckProcess.running = true;
|
pluginSettingsCheckProcess.running = true;
|
||||||
}
|
}
|
||||||
@@ -57,6 +61,16 @@ 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 {
|
property var pluginSettingsCheckProcess: Process {
|
||||||
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
||||||
running: false
|
running: false
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ var SPEC = {
|
|||||||
hiddenOutputDeviceNames: { def: [] },
|
hiddenOutputDeviceNames: { def: [] },
|
||||||
hiddenInputDeviceNames: { def: [] },
|
hiddenInputDeviceNames: { def: [] },
|
||||||
|
|
||||||
|
locale: { def: "", onChange: "updateLocale" },
|
||||||
|
timeLocale: { def: "" },
|
||||||
|
|
||||||
launcherLastMode: { def: "all" },
|
launcherLastMode: { def: "all" },
|
||||||
appDrawerLastMode: { def: "apps" },
|
appDrawerLastMode: { def: "apps" },
|
||||||
niriOverviewLastMode: { def: "apps" }
|
niriOverviewLastMode: { def: "apps" }
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ var SPEC = {
|
|||||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||||
|
|
||||||
|
firstDayOfWeek: { def: -1 },
|
||||||
use24HourClock: { def: true },
|
use24HourClock: { def: true },
|
||||||
showSeconds: { def: false },
|
showSeconds: { def: false },
|
||||||
padHours12Hour: { def: false },
|
padHours12Hour: { def: false },
|
||||||
@@ -46,6 +47,9 @@ var SPEC = {
|
|||||||
modalAnimationSpeed: { def: 1 },
|
modalAnimationSpeed: { def: 1 },
|
||||||
modalCustomAnimationDuration: { def: 150 },
|
modalCustomAnimationDuration: { def: 150 },
|
||||||
enableRippleEffects: { def: true },
|
enableRippleEffects: { def: true },
|
||||||
|
animationVariant: { def: 0 },
|
||||||
|
motionEffect: { def: 0 },
|
||||||
|
directionalAnimationMode: { def: 0 },
|
||||||
m3ElevationEnabled: { def: true },
|
m3ElevationEnabled: { def: true },
|
||||||
m3ElevationIntensity: { def: 12 },
|
m3ElevationIntensity: { def: 12 },
|
||||||
m3ElevationOpacity: { def: 30 },
|
m3ElevationOpacity: { def: 30 },
|
||||||
@@ -326,9 +330,15 @@ var SPEC = {
|
|||||||
enableFprint: { def: false },
|
enableFprint: { def: false },
|
||||||
maxFprintTries: { def: 15 },
|
maxFprintTries: { def: 15 },
|
||||||
fprintdAvailable: { def: false, persist: false },
|
fprintdAvailable: { def: false, persist: false },
|
||||||
|
enableU2f: { def: false },
|
||||||
|
u2fMode: { def: "or" },
|
||||||
|
u2fAvailable: { def: false, persist: false },
|
||||||
lockScreenActiveMonitor: { def: "all" },
|
lockScreenActiveMonitor: { def: "all" },
|
||||||
lockScreenInactiveColor: { def: "#000000" },
|
lockScreenInactiveColor: { def: "#000000" },
|
||||||
lockScreenNotificationMode: { def: 0 },
|
lockScreenNotificationMode: { def: 0 },
|
||||||
|
lockScreenVideoEnabled: { def: false },
|
||||||
|
lockScreenVideoPath: { def: "" },
|
||||||
|
lockScreenVideoCycling: { def: false },
|
||||||
hideBrightnessSlider: { def: false },
|
hideBrightnessSlider: { def: false },
|
||||||
|
|
||||||
notificationTimeoutLow: { def: 5000 },
|
notificationTimeoutLow: { def: 5000 },
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ function parse(root, jsonObj) {
|
|||||||
|
|
||||||
for (var k in SPEC) {
|
for (var k in SPEC) {
|
||||||
if (k === "pluginSettings") continue;
|
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)) {
|
if (!(k in jsonObj)) {
|
||||||
root[k] = SPEC[k].def;
|
root[k] = SPEC[k].def;
|
||||||
}
|
}
|
||||||
|
|||||||
+79
-31
@@ -21,11 +21,37 @@ Item {
|
|||||||
required property var workspaceRenameModalLoader
|
required property var workspaceRenameModalLoader
|
||||||
required property var windowRuleModalLoader
|
required property var windowRuleModalLoader
|
||||||
|
|
||||||
function getFirstBar() {
|
function getPreferredBar(refPropertyName) {
|
||||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||||
return null;
|
return null;
|
||||||
const firstLoader = root.dankBarRepeater.itemAt(0);
|
|
||||||
return firstLoader ? firstLoader.item : 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
@@ -97,9 +123,9 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(): string {
|
function open(): string {
|
||||||
const bar = root.getFirstBar();
|
const bar = root.getPreferredBar("controlCenterButtonRef");
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerControlCenterOnFocusedScreen();
|
bar.triggerControlCenter();
|
||||||
return "CONTROL_CENTER_OPEN_SUCCESS";
|
return "CONTROL_CENTER_OPEN_SUCCESS";
|
||||||
}
|
}
|
||||||
return "CONTROL_CENTER_OPEN_FAILED";
|
return "CONTROL_CENTER_OPEN_FAILED";
|
||||||
@@ -114,9 +140,14 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(): string {
|
function toggle(): string {
|
||||||
const bar = root.getFirstBar();
|
if (root.controlCenterLoader.item?.shouldBeVisible) {
|
||||||
|
root.controlCenterLoader.item.close();
|
||||||
|
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
const bar = root.getPreferredBar("controlCenterButtonRef");
|
||||||
if (bar) {
|
if (bar) {
|
||||||
bar.triggerControlCenterOnFocusedScreen();
|
bar.triggerControlCenter();
|
||||||
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
||||||
}
|
}
|
||||||
return "CONTROL_CENTER_TOGGLE_FAILED";
|
return "CONTROL_CENTER_TOGGLE_FAILED";
|
||||||
@@ -131,27 +162,37 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function open(tab: string): string {
|
function open(tab: string): string {
|
||||||
root.dankDashPopoutLoader.active = true;
|
const bar = root.getPreferredBar("clockButtonRef");
|
||||||
if (root.dankDashPopoutLoader.item) {
|
if (!bar)
|
||||||
switch (tab.toLowerCase()) {
|
return "DASH_OPEN_FAILED";
|
||||||
case "media":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
const dash = root.dankDashPopoutLoader.item;
|
||||||
break;
|
const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
|
||||||
case "wallpaper":
|
|
||||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
if (!onSameScreen) {
|
||||||
break;
|
bar.triggerWallpaperBrowser();
|
||||||
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";
|
|
||||||
}
|
}
|
||||||
return "DASH_OPEN_FAILED";
|
|
||||||
|
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";
|
||||||
}
|
}
|
||||||
|
|
||||||
function close(): string {
|
function close(): string {
|
||||||
@@ -163,8 +204,14 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function toggle(tab: string): string {
|
function toggle(tab: string): string {
|
||||||
const bar = root.getFirstBar();
|
if (root.dankDashPopoutLoader.item?.dashVisible) {
|
||||||
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
root.dankDashPopoutLoader.item.dashVisible = false;
|
||||||
|
return "DASH_TOGGLE_SUCCESS";
|
||||||
|
}
|
||||||
|
|
||||||
|
const bar = root.getPreferredBar("clockButtonRef");
|
||||||
|
if (bar) {
|
||||||
|
bar.triggerWallpaperBrowser();
|
||||||
if (root.dankDashPopoutLoader.item) {
|
if (root.dankDashPopoutLoader.item) {
|
||||||
switch (tab.toLowerCase()) {
|
switch (tab.toLowerCase()) {
|
||||||
case "media":
|
case "media":
|
||||||
@@ -521,8 +568,9 @@ Item {
|
|||||||
|
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
function wallpaper(): string {
|
function wallpaper(): string {
|
||||||
const bar = root.getFirstBar();
|
const bar = root.getPreferredBar("clockButtonRef");
|
||||||
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
if (bar) {
|
||||||
|
bar.triggerWallpaperBrowser();
|
||||||
return "SUCCESS: Toggled wallpaper browser";
|
return "SUCCESS: Toggled wallpaper browser";
|
||||||
}
|
}
|
||||||
return "ERROR: Failed to toggle wallpaper browser";
|
return "ERROR: Failed to toggle wallpaper browser";
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ Item {
|
|||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.leftMargin: Theme.spacingM
|
anchors.leftMargin: Theme.spacingM
|
||||||
anchors.rightMargin: Theme.spacingM
|
anchors.rightMargin: Theme.spacingM
|
||||||
anchors.bottomMargin: modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0
|
anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
|
||||||
clip: true
|
clip: true
|
||||||
|
|
||||||
DankListView {
|
DankListView {
|
||||||
@@ -112,14 +112,7 @@ Item {
|
|||||||
if (index < 0 || index >= count) {
|
if (index < 0 || index >= count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
positionViewAtIndex(index, ListView.Contain);
|
||||||
const itemY = index * itemHeight;
|
|
||||||
const itemBottom = itemY + itemHeight;
|
|
||||||
if (itemY < contentY) {
|
|
||||||
contentY = itemY;
|
|
||||||
} else if (itemBottom > contentY + height) {
|
|
||||||
contentY = itemBottom - height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
@@ -178,14 +171,7 @@ Item {
|
|||||||
if (index < 0 || index >= count) {
|
if (index < 0 || index >= count) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
positionViewAtIndex(index, ListView.Contain);
|
||||||
const itemY = index * itemHeight;
|
|
||||||
const itemBottom = itemY + itemHeight;
|
|
||||||
if (itemY < contentY) {
|
|
||||||
contentY = itemY;
|
|
||||||
} else if (itemBottom > contentY + height) {
|
|
||||||
contentY = itemBottom - height;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onCurrentIndexChanged: {
|
onCurrentIndexChanged: {
|
||||||
|
|||||||
@@ -31,13 +31,13 @@ Item {
|
|||||||
sourceSize.height: 128
|
sourceSize.height: 128
|
||||||
|
|
||||||
function tryLoadImage() {
|
function tryLoadImage() {
|
||||||
if (loadQueued || entryType !== "image" || cachedImageData) {
|
if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
loadQueued = true;
|
thumbnailImage.loadQueued = true;
|
||||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||||
modal.activeImageLoads++;
|
modal.activeImageLoads++;
|
||||||
loadImage();
|
thumbnailImage.loadImage();
|
||||||
} else {
|
} else {
|
||||||
retryTimer.restart();
|
retryTimer.restart();
|
||||||
}
|
}
|
||||||
@@ -47,7 +47,7 @@ Item {
|
|||||||
DMSService.sendRequest("clipboard.getEntry", {
|
DMSService.sendRequest("clipboard.getEntry", {
|
||||||
"id": entry.id
|
"id": entry.id
|
||||||
}, function (response) {
|
}, function (response) {
|
||||||
loadQueued = false;
|
thumbnailImage.loadQueued = false;
|
||||||
if (modal.activeImageLoads > 0) {
|
if (modal.activeImageLoads > 0) {
|
||||||
modal.activeImageLoads--;
|
modal.activeImageLoads--;
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ Item {
|
|||||||
}
|
}
|
||||||
const data = response.result?.data;
|
const data = response.result?.data;
|
||||||
if (data) {
|
if (data) {
|
||||||
cachedImageData = data;
|
thumbnailImage.cachedImageData = data;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ Item {
|
|||||||
property bool closeOnBackgroundClick: true
|
property bool closeOnBackgroundClick: true
|
||||||
property string animationType: "scale"
|
property string animationType: "scale"
|
||||||
property int animationDuration: Theme.modalAnimationDuration
|
property int animationDuration: Theme.modalAnimationDuration
|
||||||
property real animationScaleCollapsed: 0.96
|
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||||
property real animationOffset: Theme.spacingL
|
property real animationOffset: Theme.effectAnimOffset
|
||||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
|
||||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
property list<real> animationExitCurve: Theme.variantModalExitCurve
|
||||||
property color backgroundColor: Theme.surfaceContainer
|
property color backgroundColor: Theme.surfaceContainer
|
||||||
property color borderColor: Theme.outlineMedium
|
property color borderColor: Theme.outlineMedium
|
||||||
property real borderWidth: 0
|
property real borderWidth: 0
|
||||||
@@ -44,11 +44,13 @@ Item {
|
|||||||
property bool keepPopoutsOpen: false
|
property bool keepPopoutsOpen: false
|
||||||
property var customKeyboardFocus: null
|
property var customKeyboardFocus: null
|
||||||
property bool useOverlayLayer: false
|
property bool useOverlayLayer: false
|
||||||
|
property real frozenMotionOffsetX: 0
|
||||||
|
property real frozenMotionOffsetY: 0
|
||||||
readonly property alias contentWindow: contentWindow
|
readonly property alias contentWindow: contentWindow
|
||||||
readonly property alias clickCatcher: clickCatcher
|
readonly property alias clickCatcher: clickCatcher
|
||||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||||
|
|
||||||
signal opened
|
signal opened
|
||||||
signal dialogClosed
|
signal dialogClosed
|
||||||
@@ -58,19 +60,34 @@ Item {
|
|||||||
|
|
||||||
function open() {
|
function open() {
|
||||||
closeTimer.stop();
|
closeTimer.stop();
|
||||||
|
animationsEnabled = false;
|
||||||
|
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
||||||
|
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
||||||
|
|
||||||
const focusedScreen = CompositorService.getFocusedScreen();
|
const focusedScreen = CompositorService.getFocusedScreen();
|
||||||
if (focusedScreen) {
|
if (focusedScreen) {
|
||||||
contentWindow.screen = focusedScreen;
|
contentWindow.screen = focusedScreen;
|
||||||
if (!useSingleWindow)
|
if (!useSingleWindow)
|
||||||
clickCatcher.screen = focusedScreen;
|
clickCatcher.screen = focusedScreen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||||
|
if (!useSingleWindow)
|
||||||
|
clickCatcher.visible = true;
|
||||||
|
contentWindow.visible = true;
|
||||||
|
}
|
||||||
ModalManager.openModal(root);
|
ModalManager.openModal(root);
|
||||||
shouldBeVisible = true;
|
|
||||||
if (!useSingleWindow)
|
Qt.callLater(() => {
|
||||||
clickCatcher.visible = true;
|
animationsEnabled = true;
|
||||||
contentWindow.visible = true;
|
shouldBeVisible = true;
|
||||||
shouldHaveFocus = false;
|
if (!useSingleWindow && !clickCatcher.visible)
|
||||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
clickCatcher.visible = true;
|
||||||
|
if (!contentWindow.visible)
|
||||||
|
contentWindow.visible = true;
|
||||||
|
shouldHaveFocus = false;
|
||||||
|
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function close() {
|
function close() {
|
||||||
@@ -131,7 +148,7 @@ Item {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeTimer
|
id: closeTimer
|
||||||
interval: animationDuration + 50
|
interval: Theme.variantCloseInterval(animationDuration)
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (shouldBeVisible)
|
if (shouldBeVisible)
|
||||||
return;
|
return;
|
||||||
@@ -145,7 +162,17 @@ Item {
|
|||||||
readonly property var shadowLevel: Theme.elevationLevel3
|
readonly property var shadowLevel: Theme.elevationLevel3
|
||||||
readonly property real shadowFallbackOffset: 6
|
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 shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||||
readonly property real shadowMotionPadding: animationType === "slide" ? 30 : Math.max(0, animationOffset)
|
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 shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||||
@@ -205,9 +232,26 @@ Item {
|
|||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||||
onClicked: root.backgroundClicked()
|
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 {
|
PanelWindow {
|
||||||
@@ -250,9 +294,12 @@ Item {
|
|||||||
bottom: root.useSingleWindow
|
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 {
|
WlrLayershell.margins {
|
||||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
left: actualMarginLeft
|
||||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
top: actualMarginTop
|
||||||
right: 0
|
right: 0
|
||||||
bottom: 0
|
bottom: 0
|
||||||
}
|
}
|
||||||
@@ -282,13 +329,14 @@ Item {
|
|||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
z: -1
|
z: -1
|
||||||
color: "black"
|
color: "black"
|
||||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||||
visible: root.useBackground
|
visible: opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
DankAnim {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -296,8 +344,8 @@ Item {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: modalContainer
|
id: modalContainer
|
||||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||||
|
|
||||||
width: root.alignedWidth
|
width: root.alignedWidth
|
||||||
height: root.alignedHeight
|
height: root.alignedHeight
|
||||||
@@ -313,45 +361,117 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
readonly property bool slide: root.animationType === "slide"
|
readonly property bool slide: root.animationType === "slide"
|
||||||
readonly property real offsetX: slide ? 15 : 0
|
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
readonly property bool depthEffect: Theme.isDepthEffect
|
||||||
|
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
||||||
property real animX: 0
|
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
||||||
property real animY: 0
|
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
||||||
property real scaleValue: root.animationScaleCollapsed
|
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
||||||
|
readonly property real customDistLeft: customAnchorX
|
||||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
readonly property real customDistRight: root.screenWidth - customAnchorX
|
||||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
readonly property real customDistTop: customAnchorY
|
||||||
|
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
||||||
Connections {
|
readonly property real offsetX: {
|
||||||
target: root
|
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||||
function onShouldBeVisibleChanged() {
|
return 0;
|
||||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
if (slide && !directionalEffect && !depthEffect)
|
||||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
return 15;
|
||||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
|
||||||
|
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
|
||||||
|
|
||||||
|
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
||||||
|
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
|
||||||
|
|
||||||
Behavior on animX {
|
Behavior on animX {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
DankAnim {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on animY {
|
Behavior on animY {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
DankAnim {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scaleValue {
|
Behavior on scaleValue {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled
|
||||||
DankAnim {
|
NumberAnimation {
|
||||||
duration: root.animationDuration
|
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||||
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -367,15 +487,14 @@ Item {
|
|||||||
id: animatedContent
|
id: animatedContent
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: false
|
clip: false
|
||||||
opacity: root.shouldBeVisible ? 1 : 0
|
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||||
scale: modalContainer.scaleValue
|
scale: modalContainer.scaleValue
|
||||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
transformOrigin: Item.Center
|
||||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
enabled: root.animationsEnabled
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: animationDuration
|
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,15 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onSearchModeChanged: {
|
||||||
|
if (searchMode === "apps") {
|
||||||
|
_loadAppCategories();
|
||||||
|
} else {
|
||||||
|
appCategory = "";
|
||||||
|
appCategories = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: SettingsData
|
target: SettingsData
|
||||||
function onSortAppsAlphabeticallyChanged() {
|
function onSortAppsAlphabeticallyChanged() {
|
||||||
@@ -65,8 +74,12 @@ Item {
|
|||||||
if (!active)
|
if (!active)
|
||||||
return;
|
return;
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
if (!searchQuery && searchMode === "all")
|
if (searchMode === "apps") {
|
||||||
|
_loadAppCategories();
|
||||||
performSearch();
|
performSearch();
|
||||||
|
} else if (!searchQuery && searchMode === "all") {
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,6 +184,8 @@ Item {
|
|||||||
property string activePluginName: ""
|
property string activePluginName: ""
|
||||||
property var activePluginCategories: []
|
property var activePluginCategories: []
|
||||||
property string activePluginCategory: ""
|
property string activePluginCategory: ""
|
||||||
|
property string appCategory: ""
|
||||||
|
property var appCategories: []
|
||||||
|
|
||||||
function getSectionViewMode(sectionId) {
|
function getSectionViewMode(sectionId) {
|
||||||
if (sectionId === "browse_plugins")
|
if (sectionId === "browse_plugins")
|
||||||
@@ -364,6 +379,8 @@ Item {
|
|||||||
activePluginName = "";
|
activePluginName = "";
|
||||||
activePluginCategories = [];
|
activePluginCategories = [];
|
||||||
activePluginCategory = "";
|
activePluginCategory = "";
|
||||||
|
appCategory = "";
|
||||||
|
appCategories = [];
|
||||||
pluginFilter = "";
|
pluginFilter = "";
|
||||||
collapsedSections = {};
|
collapsedSections = {};
|
||||||
_clearModeCache();
|
_clearModeCache();
|
||||||
@@ -408,6 +425,19 @@ Item {
|
|||||||
performSearch();
|
performSearch();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setAppCategory(category) {
|
||||||
|
if (appCategory === category)
|
||||||
|
return;
|
||||||
|
appCategory = category;
|
||||||
|
_queryDrivenSearch = true;
|
||||||
|
_clearModeCache();
|
||||||
|
performSearch();
|
||||||
|
}
|
||||||
|
|
||||||
|
function _loadAppCategories() {
|
||||||
|
appCategories = AppSearchService.getAllCategories();
|
||||||
|
}
|
||||||
|
|
||||||
function setFileSearchType(type) {
|
function setFileSearchType(type) {
|
||||||
if (fileSearchType === type)
|
if (fileSearchType === type)
|
||||||
return;
|
return;
|
||||||
@@ -592,8 +622,9 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (searchMode === "apps") {
|
if (searchMode === "apps") {
|
||||||
|
var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
|
||||||
var cachedSections = AppSearchService.getCachedDefaultSections();
|
var cachedSections = AppSearchService.getCachedDefaultSections();
|
||||||
if (cachedSections && !searchQuery) {
|
if (cachedSections && !searchQuery && !isCategoryFiltered) {
|
||||||
var modeCache = _getCachedModeData("apps");
|
var modeCache = _getCachedModeData("apps");
|
||||||
if (modeCache) {
|
if (modeCache) {
|
||||||
_applyHighlights(modeCache.sections, "");
|
_applyHighlights(modeCache.sections, "");
|
||||||
@@ -623,9 +654,23 @@ Item {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var apps = searchApps(searchQuery);
|
if (isCategoryFiltered) {
|
||||||
for (var i = 0; i < apps.length; i++) {
|
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||||
allItems.push(apps[i]);
|
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 scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ Item {
|
|||||||
property bool spotlightOpen: false
|
property bool spotlightOpen: false
|
||||||
property bool keyboardActive: false
|
property bool keyboardActive: false
|
||||||
property bool contentVisible: false
|
property bool contentVisible: false
|
||||||
|
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
|
||||||
property var spotlightContent: launcherContentLoader.item
|
property var spotlightContent: launcherContentLoader.item
|
||||||
property bool openedFromOverview: false
|
property bool openedFromOverview: false
|
||||||
property bool isClosing: false
|
property bool isClosing: false
|
||||||
@@ -23,8 +24,14 @@ Item {
|
|||||||
property string _pendingMode: ""
|
property string _pendingMode: ""
|
||||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
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 bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||||
readonly property var effectiveScreen: launcherWindow.screen
|
readonly property var effectiveScreen: contentWindow.screen
|
||||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||||
@@ -78,6 +85,34 @@ Item {
|
|||||||
}
|
}
|
||||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
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
|
signal dialogClosed
|
||||||
|
|
||||||
function _ensureContentLoadedAndInitialize(query, mode) {
|
function _ensureContentLoadedAndInitialize(query, mode) {
|
||||||
@@ -97,7 +132,8 @@ Item {
|
|||||||
if (!spotlightContent)
|
if (!spotlightContent)
|
||||||
return;
|
return;
|
||||||
contentVisible = true;
|
contentVisible = true;
|
||||||
spotlightContent.searchField.forceActiveFocus();
|
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||||
|
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||||
|
|
||||||
if (spotlightContent.searchField) {
|
if (spotlightContent.searchField) {
|
||||||
spotlightContent.searchField.text = query;
|
spotlightContent.searchField.text = query;
|
||||||
@@ -130,40 +166,59 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function _openCommon(query, mode) {
|
||||||
closeCleanupTimer.stop();
|
closeCleanupTimer.stop();
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
|
|
||||||
var focusedScreen = CompositorService.getFocusedScreen();
|
// Disable animations so the snap is instant
|
||||||
if (focusedScreen)
|
animationsEnabled = false;
|
||||||
launcherWindow.screen = focusedScreen;
|
|
||||||
|
|
||||||
spotlightOpen = true;
|
// Freeze the collapsed offsets (they depend on height which could change)
|
||||||
keyboardActive = true;
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// _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);
|
ModalManager.openModal(root);
|
||||||
|
spotlightOpen = true;
|
||||||
|
backgroundWindow.visible = true;
|
||||||
|
contentWindow.visible = true;
|
||||||
if (useHyprlandFocusGrab)
|
if (useHyprlandFocusGrab)
|
||||||
focusGrab.active = true;
|
focusGrab.active = true;
|
||||||
|
|
||||||
_ensureContentLoadedAndInitialize("", "");
|
// 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("", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
function showWithQuery(query) {
|
function showWithQuery(query) {
|
||||||
closeCleanupTimer.stop();
|
_openCommon(query, "");
|
||||||
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() {
|
function hide() {
|
||||||
@@ -171,13 +226,17 @@ Item {
|
|||||||
return;
|
return;
|
||||||
openedFromOverview = false;
|
openedFromOverview = false;
|
||||||
isClosing = true;
|
isClosing = true;
|
||||||
contentVisible = false;
|
// 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;
|
||||||
|
|
||||||
keyboardActive = false;
|
keyboardActive = false;
|
||||||
spotlightOpen = false;
|
spotlightOpen = false;
|
||||||
focusGrab.active = false;
|
focusGrab.active = false;
|
||||||
ModalManager.closeModal(root);
|
ModalManager.closeModal(root);
|
||||||
|
|
||||||
closeCleanupTimer.start();
|
closeCleanupTimer.start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -186,21 +245,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function showWithMode(mode) {
|
function showWithMode(mode) {
|
||||||
closeCleanupTimer.stop();
|
_openCommon("", mode);
|
||||||
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) {
|
function toggleWithMode(mode) {
|
||||||
@@ -221,10 +266,13 @@ Item {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: closeCleanupTimer
|
id: closeCleanupTimer
|
||||||
interval: Theme.modalAnimationDuration + 50
|
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
isClosing = false;
|
isClosing = false;
|
||||||
|
contentVisible = false;
|
||||||
|
contentWindow.visible = false;
|
||||||
|
backgroundWindow.visible = false;
|
||||||
if (root.unloadContentOnClose)
|
if (root.unloadContentOnClose)
|
||||||
launcherContentLoader.active = false;
|
launcherContentLoader.active = false;
|
||||||
dialogClosed();
|
dialogClosed();
|
||||||
@@ -242,7 +290,7 @@ Item {
|
|||||||
|
|
||||||
HyprlandFocusGrab {
|
HyprlandFocusGrab {
|
||||||
id: focusGrab
|
id: focusGrab
|
||||||
windows: [launcherWindow]
|
windows: [contentWindow]
|
||||||
active: false
|
active: false
|
||||||
|
|
||||||
onCleared: {
|
onCleared: {
|
||||||
@@ -267,7 +315,7 @@ Item {
|
|||||||
if (Quickshell.screens.length === 0)
|
if (Quickshell.screens.length === 0)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const screen = launcherWindow.screen;
|
const screen = contentWindow.screen;
|
||||||
const screenName = screen?.name;
|
const screenName = screen?.name;
|
||||||
|
|
||||||
let needsReset = !screen || !screenName;
|
let needsReset = !screen || !screenName;
|
||||||
@@ -289,35 +337,31 @@ Item {
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
root._windowEnabled = false;
|
root._windowEnabled = false;
|
||||||
launcherWindow.screen = newScreen;
|
backgroundWindow.screen = newScreen;
|
||||||
|
contentWindow.screen = newScreen;
|
||||||
Qt.callLater(() => {
|
Qt.callLater(() => {
|
||||||
root._windowEnabled = true;
|
root._windowEnabled = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
||||||
PanelWindow {
|
PanelWindow {
|
||||||
id: launcherWindow
|
id: backgroundWindow
|
||||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
visible: false
|
||||||
color: "transparent"
|
color: "transparent"
|
||||||
exclusionMode: ExclusionMode.Ignore
|
|
||||||
|
|
||||||
WlrLayershell.namespace: "dms:spotlight"
|
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||||
WlrLayershell.layer: {
|
WlrLayershell.layer: WlrLayershell.Top
|
||||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
WlrLayershell.exclusiveZone: -1
|
||||||
case "bottom":
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
|
||||||
return WlrLayershell.Top;
|
WlrLayershell.margins {
|
||||||
case "background":
|
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
||||||
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
||||||
return WlrLayershell.Top;
|
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
||||||
case "overlay":
|
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||||
return WlrLayershell.Overlay;
|
|
||||||
default:
|
|
||||||
return WlrLayershell.Top;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
|
||||||
|
|
||||||
anchors {
|
anchors {
|
||||||
top: true
|
top: true
|
||||||
@@ -327,11 +371,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mask: Region {
|
mask: Region {
|
||||||
item: spotlightOpen ? fullScreenMask : null
|
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||||
}
|
}
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: fullScreenMask
|
id: bgFullScreenMask
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,13 +383,14 @@ Item {
|
|||||||
id: backgroundDarken
|
id: backgroundDarken
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
color: "black"
|
color: "black"
|
||||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||||
visible: contentVisible || opacity > 0
|
visible: launcherMotionVisible || opacity > 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
|
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||||
DankAnim {
|
DankAnim {
|
||||||
duration: Theme.modalAnimationDuration
|
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,88 +398,240 @@ Item {
|
|||||||
MouseArea {
|
MouseArea {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
enabled: spotlightOpen
|
enabled: spotlightOpen
|
||||||
onClicked: mouse => {
|
onClicked: root.hide()
|
||||||
var contentX = modalContainer.x;
|
}
|
||||||
var contentY = modalContainer.y;
|
}
|
||||||
var contentW = modalContainer.width;
|
|
||||||
var contentH = modalContainer.height;
|
|
||||||
|
|
||||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
||||||
root.hide();
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 {
|
Item {
|
||||||
id: modalContainer
|
id: contentInputMask
|
||||||
x: root.modalX
|
visible: false
|
||||||
y: root.modalY
|
x: contentContainer.x + contentWrapper.x
|
||||||
width: root.modalWidth
|
y: contentContainer.y + contentWrapper.y
|
||||||
height: root.modalHeight
|
width: root.alignedWidth
|
||||||
visible: contentVisible || opacity > 0
|
height: root.alignedHeight
|
||||||
|
|
||||||
opacity: contentVisible ? 1 : 0
|
|
||||||
scale: contentVisible ? 1 : 0.96
|
|
||||||
transformOrigin: Item.Center
|
|
||||||
|
|
||||||
Behavior on opacity {
|
|
||||||
DankAnim {
|
|
||||||
duration: Theme.modalAnimationDuration
|
|
||||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Behavior on scale {
|
|
||||||
DankAnim {
|
|
||||||
duration: Theme.modalAnimationDuration
|
|
||||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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"
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Item {
|
||||||
|
id: contentContainer
|
||||||
|
|
||||||
|
// 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
|
||||||
|
DankAnim {
|
||||||
|
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||||
|
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on animY {
|
||||||
|
enabled: root.animationsEnabled
|
||||||
|
DankAnim {
|
||||||
|
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||||
|
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: directionalClipMask
|
||||||
|
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
||||||
|
readonly property real clipOversize: 2000
|
||||||
|
|
||||||
|
clip: shouldClip
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ FocusScope {
|
|||||||
|
|
||||||
Controller {
|
Controller {
|
||||||
id: controller
|
id: controller
|
||||||
active: root.parentModal?.spotlightOpen ?? true
|
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||||
viewModeContext: root.viewModeContext
|
viewModeContext: root.viewModeContext
|
||||||
|
|
||||||
onItemExecuted: {
|
onItemExecuted: {
|
||||||
@@ -462,7 +462,7 @@ FocusScope {
|
|||||||
showClearButton: true
|
showClearButton: true
|
||||||
textColor: Theme.surfaceText
|
textColor: Theme.surfaceText
|
||||||
font.pixelSize: Theme.fontSizeLarge
|
font.pixelSize: Theme.fontSizeLarge
|
||||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||||
placeholderText: ""
|
placeholderText: ""
|
||||||
ignoreUpDownKeys: true
|
ignoreUpDownKeys: true
|
||||||
ignoreTabKeys: true
|
ignoreTabKeys: true
|
||||||
@@ -496,8 +496,9 @@ FocusScope {
|
|||||||
Row {
|
Row {
|
||||||
id: categoryRow
|
id: categoryRow
|
||||||
width: parent.width
|
width: parent.width
|
||||||
height: controller.activePluginCategories.length > 0 ? 36 : 0
|
readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
|
||||||
visible: controller.activePluginCategories.length > 0
|
height: showPluginCategories ? 36 : 0
|
||||||
|
visible: showPluginCategories
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
|
||||||
clip: true
|
clip: true
|
||||||
@@ -511,6 +512,7 @@ FocusScope {
|
|||||||
|
|
||||||
DankDropdown {
|
DankDropdown {
|
||||||
id: categoryDropdown
|
id: categoryDropdown
|
||||||
|
visible: categoryRow.showPluginCategories
|
||||||
width: Math.min(200, parent.width)
|
width: Math.min(200, parent.width)
|
||||||
compactMode: true
|
compactMode: true
|
||||||
dropdownWidth: 200
|
dropdownWidth: 200
|
||||||
@@ -694,7 +696,13 @@ FocusScope {
|
|||||||
Item {
|
Item {
|
||||||
width: parent.width
|
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)
|
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
opacity: {
|
||||||
|
if (!root.parentModal)
|
||||||
|
return 1;
|
||||||
|
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
||||||
|
return 1;
|
||||||
|
return root.parentModal.isClosing ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
ResultsList {
|
ResultsList {
|
||||||
id: resultsList
|
id: resultsList
|
||||||
@@ -789,7 +797,7 @@ FocusScope {
|
|||||||
Image {
|
Image {
|
||||||
width: 40
|
width: 40
|
||||||
height: 40
|
height: 40
|
||||||
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
|
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
|
||||||
sourceSize.width: 40
|
sourceSize.width: 40
|
||||||
sourceSize.height: 40
|
sourceSize.height: 40
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
pragma ComponentBehavior: Bound
|
pragma ComponentBehavior: Bound
|
||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -35,21 +37,190 @@ Rectangle {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
spacing: Theme.spacingS
|
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 {
|
DankIcon {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
// Hide section icon when the category chip already shows one
|
||||||
|
visible: !leftContent.hasAppCategories
|
||||||
name: root.section?.icon ?? "folder"
|
name: root.section?.icon ?? "folder"
|
||||||
size: 16
|
size: 16
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Plain title — hidden when the category chip is shown
|
||||||
StyledText {
|
StyledText {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
visible: !leftContent.hasAppCategories
|
||||||
text: root.section?.title ?? ""
|
text: root.section?.title ?? ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceVariantText
|
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 {
|
StyledText {
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
text: root.section?.items?.length ?? 0
|
text: root.section?.items?.length ?? 0
|
||||||
|
|||||||
@@ -225,7 +225,13 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
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")
|
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);
|
||||||
|
}
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,7 +470,22 @@ FocusScope {
|
|||||||
|
|
||||||
onActiveChanged: {
|
onActiveChanged: {
|
||||||
if (active && item)
|
if (active && item)
|
||||||
Qt.callLater(() => item.forceActiveFocus());
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -246,6 +246,12 @@ Rectangle {
|
|||||||
"icon": "headphones",
|
"icon": "headphones",
|
||||||
"tabIndex": 29
|
"tabIndex": 29
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"id": "locale",
|
||||||
|
"text": I18n.tr("Locale"),
|
||||||
|
"icon": "language",
|
||||||
|
"tabIndex": 30
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"id": "clipboard",
|
"id": "clipboard",
|
||||||
"text": I18n.tr("Clipboard"),
|
"text": I18n.tr("Clipboard"),
|
||||||
|
|||||||
@@ -106,7 +106,7 @@ DankPopout {
|
|||||||
QtObject {
|
QtObject {
|
||||||
id: modalAdapter
|
id: modalAdapter
|
||||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||||
property bool isClosing: false
|
property bool isClosing: appDrawerPopout.isClosing
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
appDrawerPopout.close();
|
appDrawerPopout.close();
|
||||||
|
|||||||
@@ -87,10 +87,6 @@ Variants {
|
|||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||||
|
|
||||||
if (!source) {
|
|
||||||
root._renderSettling = false;
|
|
||||||
}
|
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,7 +109,7 @@ Variants {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: renderSettleTimer
|
id: renderSettleTimer
|
||||||
interval: 100
|
interval: 1000
|
||||||
onTriggered: root._renderSettling = false
|
onTriggered: root._renderSettling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -271,8 +271,8 @@ Item {
|
|||||||
|
|
||||||
text: {
|
text: {
|
||||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
||||||
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
|
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
|
||||||
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
|
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: root.accentColor
|
color: root.accentColor
|
||||||
@@ -324,8 +324,8 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: {
|
text: {
|
||||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
||||||
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
|
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
|
||||||
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
|
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
|
||||||
}
|
}
|
||||||
font.pixelSize: digitalRoot.smallSize
|
font.pixelSize: digitalRoot.smallSize
|
||||||
color: Theme.withAlpha(root.accentColor, 0.7)
|
color: Theme.withAlpha(root.accentColor, 0.7)
|
||||||
@@ -528,7 +528,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
visible: stackedRoot.hasDate
|
visible: stackedRoot.hasDate
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
text: systemClock.date?.toLocaleDateString(Qt.locale(), "MMM dd") ?? ""
|
text: systemClock.date?.toLocaleDateString(I18n.locale(), "MMM dd") ?? ""
|
||||||
font.pixelSize: stackedRoot.smallSize * 0.7
|
font.pixelSize: stackedRoot.smallSize * 0.7
|
||||||
color: Theme.withAlpha(root.accentColor, 0.7)
|
color: Theme.withAlpha(root.accentColor, 0.7)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,9 +126,11 @@ DankPopout {
|
|||||||
z: 5000
|
z: 5000
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: 200
|
duration: Theme.shortDuration
|
||||||
easing.type: Easing.OutCubic
|
easing.type: Easing.BezierSpline
|
||||||
|
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1117,6 +1117,7 @@ Item {
|
|||||||
if (!notificationCenterLoader.item) {
|
if (!notificationCenterLoader.item) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
||||||
const effectiveBarConfig = topBarContent.barConfig;
|
const effectiveBarConfig = topBarContent.barConfig;
|
||||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||||
if (notificationCenterLoader.item.setBarContext) {
|
if (notificationCenterLoader.item.setBarContext) {
|
||||||
|
|||||||
@@ -566,8 +566,9 @@ PanelWindow {
|
|||||||
readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null
|
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 _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null
|
||||||
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : 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) {
|
function sectionRect(section, isCenter, _dep) {
|
||||||
if (!section)
|
if (!section)
|
||||||
return {
|
return {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
@@ -596,7 +597,7 @@ PanelWindow {
|
|||||||
item: clickThroughEnabled ? null : inputMask
|
item: clickThroughEnabled ? null : inputMask
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -609,7 +610,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -622,7 +623,7 @@ PanelWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Region {
|
Region {
|
||||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false) : {
|
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
|
||||||
"x": 0,
|
"x": 0,
|
||||||
"y": 0,
|
"y": 0,
|
||||||
"w": 0,
|
"w": 0,
|
||||||
@@ -633,6 +634,14 @@ PanelWindow {
|
|||||||
width: r.w
|
width: r.w
|
||||||
height: r.h
|
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 {
|
Item {
|
||||||
@@ -645,7 +654,7 @@ PanelWindow {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: revealHold
|
id: revealHold
|
||||||
interval: barConfig?.autoHideDelay ?? 250
|
interval: barWindow.clickThroughEnabled ? Math.max((barConfig?.autoHideDelay ?? 250) * 6, 1500) : (barConfig?.autoHideDelay ?? 250)
|
||||||
repeat: false
|
repeat: false
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) {
|
if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) {
|
||||||
@@ -703,7 +712,6 @@ PanelWindow {
|
|||||||
Connections {
|
Connections {
|
||||||
function onBarConfigChanged() {
|
function onBarConfigChanged() {
|
||||||
topBarCore.autoHide = barConfig?.autoHide ?? false;
|
topBarCore.autoHide = barConfig?.autoHide ?? false;
|
||||||
revealHold.interval = barConfig?.autoHideDelay ?? 250;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
target: rootWindow
|
target: rootWindow
|
||||||
|
|||||||
@@ -273,7 +273,7 @@ PanelWindow {
|
|||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ BasePill {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const locale = Qt.locale();
|
const locale = I18n.locale();
|
||||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
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');
|
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
|
||||||
@@ -144,7 +144,7 @@ BasePill {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const locale = Qt.locale();
|
const locale = I18n.locale();
|
||||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
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');
|
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
|
||||||
@@ -165,7 +165,7 @@ BasePill {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const locale = Qt.locale();
|
const locale = I18n.locale();
|
||||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
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');
|
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
|
||||||
@@ -180,7 +180,7 @@ BasePill {
|
|||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
const locale = Qt.locale();
|
const locale = I18n.locale();
|
||||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
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');
|
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
|
id: dateText
|
||||||
text: {
|
text: {
|
||||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
||||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat);
|
return systemClock?.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat);
|
||||||
}
|
}
|
||||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d");
|
return systemClock?.date?.toLocaleDateString(I18n.locale(), "ddd d");
|
||||||
}
|
}
|
||||||
font.pixelSize: clockRow.fontSize
|
font.pixelSize: clockRow.fontSize
|
||||||
color: Theme.widgetTextColor
|
color: Theme.widgetTextColor
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ DankPopout {
|
|||||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||||
triggerWidth: 80
|
triggerWidth: 80
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
shouldBeVisible: dashVisible
|
|
||||||
|
|
||||||
property bool __focusArmed: false
|
property bool __focusArmed: false
|
||||||
property bool __contentReady: false
|
property bool __contentReady: false
|
||||||
|
|||||||
@@ -44,6 +44,43 @@ Item {
|
|||||||
|
|
||||||
property int __volumeHoverCount: 0
|
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() {
|
function volumeAreaEntered() {
|
||||||
__volumeHoverCount++;
|
__volumeHoverCount++;
|
||||||
panelEntered();
|
panelEntered();
|
||||||
@@ -62,30 +99,47 @@ Item {
|
|||||||
visible: dropdownType === 1 && volumeAvailable
|
visible: dropdownType === 1 && volumeAvailable
|
||||||
width: 60
|
width: 60
|
||||||
height: 180
|
height: 180
|
||||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
|
||||||
y: anchorPos.y - height / 2
|
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
|
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
|
||||||
border.width: 1
|
border.width: 1
|
||||||
|
|
||||||
opacity: dropdownType === 1 ? 1 : 0
|
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
|
||||||
scale: dropdownType === 1 ? 1 : 0.96
|
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
enabled: !Theme.isDirectionalEffect
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
DankAnim {
|
||||||
easing.type: Easing.BezierSpline
|
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,33 +251,50 @@ Item {
|
|||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: audioDevicesPanel
|
id: audioDevicesPanel
|
||||||
visible: dropdownType === 2
|
visible: dropdownType === 2 && activePlayer !== null
|
||||||
width: 280
|
width: 280
|
||||||
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
|
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
|
||||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
|
||||||
y: anchorPos.y - height / 2
|
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 2)
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
opacity: dropdownType === 2 ? 1 : 0
|
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
|
||||||
scale: dropdownType === 2 ? 1 : 0.96
|
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
enabled: !Theme.isDirectionalEffect
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
DankAnim {
|
||||||
easing.type: Easing.BezierSpline
|
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,30 +425,47 @@ Item {
|
|||||||
visible: dropdownType === 3
|
visible: dropdownType === 3
|
||||||
width: 240
|
width: 240
|
||||||
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
||||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
|
||||||
y: anchorPos.y - height / 2
|
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
|
||||||
radius: Theme.cornerRadius * 2
|
radius: Theme.cornerRadius * 2
|
||||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
|
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.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
|
||||||
border.width: 2
|
border.width: 2
|
||||||
|
|
||||||
opacity: dropdownType === 3 ? 1 : 0
|
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
|
||||||
scale: dropdownType === 3 ? 1 : 0.96
|
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
|
||||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
enabled: !Theme.isDirectionalEffect
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
DankAnim {
|
||||||
easing.type: Easing.BezierSpline
|
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,8 +14,15 @@ Rectangle {
|
|||||||
|
|
||||||
signal closeDash
|
signal closeDash
|
||||||
|
|
||||||
|
function weekStartQt() {
|
||||||
|
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
|
||||||
|
return Qt.locale().firstDayOfWeek;
|
||||||
|
}
|
||||||
|
return SettingsData.firstDayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
function weekStartJs() {
|
function weekStartJs() {
|
||||||
return Qt.locale().firstDayOfWeek % 7;
|
return weekStartQt() % 7;
|
||||||
}
|
}
|
||||||
|
|
||||||
function startOfWeek(dateObj) {
|
function startOfWeek(dateObj) {
|
||||||
@@ -179,7 +186,7 @@ Rectangle {
|
|||||||
StyledText {
|
StyledText {
|
||||||
width: parent.width - 56
|
width: parent.width - 56
|
||||||
height: 28
|
height: 28
|
||||||
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
@@ -223,11 +230,10 @@ Rectangle {
|
|||||||
Repeater {
|
Repeater {
|
||||||
model: {
|
model: {
|
||||||
const days = [];
|
const days = [];
|
||||||
const loc = Qt.locale();
|
const qtFirst = weekStartQt();
|
||||||
const qtFirst = loc.firstDayOfWeek;
|
|
||||||
for (let i = 0; i < 7; ++i) {
|
for (let i = 0; i < 7; ++i) {
|
||||||
const qtDay = ((qtFirst - 1 + i) % 7) + 1;
|
const qtDay = ((qtFirst - 1 + i) % 7) + 1;
|
||||||
days.push(loc.dayName(qtDay, Locale.ShortFormat));
|
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
|
||||||
}
|
}
|
||||||
return days;
|
return days;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
|
text: systemClock?.date?.toLocaleDateString(I18n.locale(), "MMM dd")
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
|||||||
@@ -447,9 +447,8 @@ Variants {
|
|||||||
|
|
||||||
height: {
|
height: {
|
||||||
if (dock.isVertical) {
|
if (dock.isVertical) {
|
||||||
if (!dock.reveal)
|
// 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);
|
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;
|
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||||
}
|
}
|
||||||
@@ -457,8 +456,7 @@ Variants {
|
|||||||
if (dock.isVertical) {
|
if (dock.isVertical) {
|
||||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||||
}
|
}
|
||||||
if (!dock.reveal)
|
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
|
||||||
return Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5);
|
|
||||||
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
||||||
}
|
}
|
||||||
anchors {
|
anchors {
|
||||||
|
|||||||
@@ -329,7 +329,7 @@ PanelWindow {
|
|||||||
|
|
||||||
IconImage {
|
IconImage {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
|
||||||
smooth: true
|
smooth: true
|
||||||
asynchronous: true
|
asynchronous: true
|
||||||
visible: status === Image.Ready
|
visible: status === Image.Ready
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ Item {
|
|||||||
|
|
||||||
property string fullTimeStr: {
|
property string fullTimeStr: {
|
||||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||||
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
|
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||||
}
|
}
|
||||||
property var timeParts: fullTimeStr.split(':')
|
property var timeParts: fullTimeStr.split(':')
|
||||||
property string hours: timeParts[0] || ""
|
property string hours: timeParts[0] || ""
|
||||||
@@ -328,9 +328,9 @@ Item {
|
|||||||
anchors.topMargin: 4
|
anchors.topMargin: 4
|
||||||
text: {
|
text: {
|
||||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
|
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
||||||
}
|
}
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
|
|||||||
@@ -333,9 +333,9 @@ Item {
|
|||||||
visible: SettingsData.lockScreenShowDate
|
visible: SettingsData.lockScreenShowDate
|
||||||
text: {
|
text: {
|
||||||
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
|
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat);
|
return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
|
||||||
}
|
}
|
||||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeXLarge
|
font.pixelSize: Theme.fontSizeXLarge
|
||||||
color: "white"
|
color: "white"
|
||||||
@@ -687,14 +687,24 @@ Item {
|
|||||||
|
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
name: {
|
name: {
|
||||||
|
if (pam.u2fPending)
|
||||||
|
return "passkey";
|
||||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||||
return "fingerprint_off";
|
return "fingerprint_off";
|
||||||
if (pam.fprint.active)
|
if (pam.fprint.active)
|
||||||
return "fingerprint";
|
return "fingerprint";
|
||||||
|
if (pam.u2f.active)
|
||||||
|
return "passkey";
|
||||||
return "lock";
|
return "lock";
|
||||||
}
|
}
|
||||||
size: 20
|
size: 20
|
||||||
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
|
color: {
|
||||||
|
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||||
|
return Theme.error;
|
||||||
|
if (pam.u2fState !== "")
|
||||||
|
return Theme.tertiary;
|
||||||
|
return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
|
||||||
|
}
|
||||||
opacity: pam.passwd.active ? 0 : 1
|
opacity: pam.passwd.active ? 0 : 1
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
@@ -745,8 +755,7 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
onAccepted: {
|
onAccepted: {
|
||||||
if (!demoMode && !pam.passwd.active) {
|
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
||||||
console.log("Enter pressed, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -756,6 +765,11 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === Qt.Key_Escape) {
|
if (event.key === Qt.Key_Escape) {
|
||||||
|
if (pam.u2fPending) {
|
||||||
|
pam.cancelU2fPending();
|
||||||
|
event.accepted = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
clear();
|
clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,6 +834,11 @@ Item {
|
|||||||
if (root.unlocking) {
|
if (root.unlocking) {
|
||||||
return "Unlocking...";
|
return "Unlocking...";
|
||||||
}
|
}
|
||||||
|
if (pam.u2fPending) {
|
||||||
|
if (pam.u2fState === "insert")
|
||||||
|
return "Insert your security key...";
|
||||||
|
return "Touch your security key...";
|
||||||
|
}
|
||||||
if (pam.passwd.active) {
|
if (pam.passwd.active) {
|
||||||
return "Authenticating...";
|
return "Authenticating...";
|
||||||
}
|
}
|
||||||
@@ -894,7 +913,7 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard"
|
iconName: "keyboard"
|
||||||
buttonSize: 32
|
buttonSize: 32
|
||||||
visible: !demoMode && !pam.passwd.active && !root.unlocking
|
visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
|
||||||
enabled: visible
|
enabled: visible
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (keyboardController.isKeyboardActive) {
|
if (keyboardController.isKeyboardActive) {
|
||||||
@@ -995,11 +1014,10 @@ Item {
|
|||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
iconName: "keyboard_return"
|
iconName: "keyboard_return"
|
||||||
buttonSize: 36
|
buttonSize: 36
|
||||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||||
enabled: !demoMode
|
enabled: !demoMode
|
||||||
onClicked: {
|
onClicked: {
|
||||||
if (!demoMode) {
|
if (!demoMode && !pam.u2fPending) {
|
||||||
console.log("Enter button clicked, starting PAM authentication");
|
|
||||||
pam.passwd.start();
|
pam.passwd.start();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1025,6 +1043,12 @@ Item {
|
|||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.preferredHeight: 20
|
Layout.preferredHeight: 20
|
||||||
text: {
|
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") {
|
if (root.pamState === "error") {
|
||||||
return "Authentication error - try again";
|
return "Authentication error - try again";
|
||||||
}
|
}
|
||||||
@@ -1036,10 +1060,10 @@ Item {
|
|||||||
}
|
}
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
color: Theme.error
|
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
opacity: root.pamState !== "" ? 1 : 0
|
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
@@ -1607,6 +1631,14 @@ Item {
|
|||||||
root.passwordBuffer = "";
|
root.passwordBuffer = "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onU2fPendingChanged: {
|
||||||
|
if (u2fPending) {
|
||||||
|
passwordField.text = "";
|
||||||
|
root.passwordBuffer = "";
|
||||||
|
if (keyboardController.isKeyboardActive)
|
||||||
|
keyboardController.hide();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Binding {
|
Binding {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell.Wayland
|
import Quickshell.Wayland
|
||||||
|
import qs.Common
|
||||||
|
|
||||||
Rectangle {
|
FocusScope {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
required property WlSessionLock lock
|
required property WlSessionLock lock
|
||||||
@@ -14,7 +15,17 @@ Rectangle {
|
|||||||
signal passwordChanged(string newPassword)
|
signal passwordChanged(string newPassword)
|
||||||
signal unlockRequested
|
signal unlockRequested
|
||||||
|
|
||||||
color: "transparent"
|
Keys.onPressed: event => {
|
||||||
|
if (videoScreensaver.active && videoScreensaver.inputEnabled) {
|
||||||
|
videoScreensaver.dismiss();
|
||||||
|
event.accepted = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
LockScreenContent {
|
LockScreenContent {
|
||||||
id: lockContent
|
id: lockContent
|
||||||
@@ -23,17 +34,38 @@ Rectangle {
|
|||||||
demoMode: false
|
demoMode: false
|
||||||
passwordBuffer: root.sharedPasswordBuffer
|
passwordBuffer: root.sharedPasswordBuffer
|
||||||
screenName: root.screenName
|
screenName: root.screenName
|
||||||
|
enabled: !videoScreensaver.active
|
||||||
|
focus: !videoScreensaver.active
|
||||||
|
opacity: videoScreensaver.active ? 0 : 1
|
||||||
onUnlockRequested: root.unlockRequested()
|
onUnlockRequested: root.unlockRequested()
|
||||||
onPasswordBufferChanged: {
|
onPasswordBufferChanged: {
|
||||||
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
||||||
root.passwordChanged(passwordBuffer);
|
root.passwordChanged(passwordBuffer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: 200
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VideoScreensaver {
|
||||||
|
id: videoScreensaver
|
||||||
|
anchors.fill: parent
|
||||||
|
screenName: root.screenName
|
||||||
|
}
|
||||||
|
|
||||||
|
Component.onCompleted: forceActiveFocus()
|
||||||
|
|
||||||
onIsLockedChanged: {
|
onIsLockedChanged: {
|
||||||
if (isLocked) {
|
if (isLocked) {
|
||||||
|
forceActiveFocus();
|
||||||
lockContent.resetLockState();
|
lockContent.resetLockState();
|
||||||
|
if (SettingsData.lockScreenVideoEnabled) {
|
||||||
|
videoScreensaver.start();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
lockContent.unlocking = false;
|
lockContent.unlocking = false;
|
||||||
|
|||||||
@@ -14,14 +14,51 @@ Scope {
|
|||||||
|
|
||||||
readonly property alias passwd: passwd
|
readonly property alias passwd: passwd
|
||||||
readonly property alias fprint: fprint
|
readonly property alias fprint: fprint
|
||||||
|
readonly property alias u2f: u2f
|
||||||
property string lockMessage
|
property string lockMessage
|
||||||
property string state
|
property string state
|
||||||
property string fprintState
|
property string fprintState
|
||||||
|
property string u2fState
|
||||||
|
property bool u2fPending: false
|
||||||
property string buffer
|
property string buffer
|
||||||
|
|
||||||
signal flashMsg
|
signal flashMsg
|
||||||
signal unlockRequested
|
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 {
|
FileView {
|
||||||
id: dankshellConfigWatcher
|
id: dankshellConfigWatcher
|
||||||
|
|
||||||
@@ -30,9 +67,9 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileView {
|
FileView {
|
||||||
id: loginConfigWatcher
|
id: u2fConfigWatcher
|
||||||
|
|
||||||
path: "/etc/pam.d/login"
|
path: "/etc/pam.d/dankshell-u2f"
|
||||||
printErrors: false
|
printErrors: false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +77,7 @@ Scope {
|
|||||||
id: passwd
|
id: passwd
|
||||||
|
|
||||||
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
config: dankshellConfigWatcher.loaded ? "dankshell" : "login"
|
||||||
configDirectory: dankshellConfigWatcher.loaded || loginConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
configDirectory: dankshellConfigWatcher.loaded ? "/etc/pam.d" : Quickshell.shellDir + "/assets/pam"
|
||||||
|
|
||||||
onMessageChanged: {
|
onMessageChanged: {
|
||||||
if (message.startsWith("The account is locked"))
|
if (message.startsWith("The account is locked"))
|
||||||
@@ -59,9 +96,8 @@ Scope {
|
|||||||
onCompleted: res => {
|
onCompleted: res => {
|
||||||
if (res === PamResult.Success) {
|
if (res === PamResult.Success) {
|
||||||
if (!root.unlockInProgress) {
|
if (!root.unlockInProgress) {
|
||||||
root.unlockInProgress = true;
|
|
||||||
fprint.abort();
|
fprint.abort();
|
||||||
root.unlockRequested();
|
root.proceedAfterPrimaryAuth();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -105,9 +141,8 @@ Scope {
|
|||||||
|
|
||||||
if (res === PamResult.Success) {
|
if (res === PamResult.Success) {
|
||||||
if (!root.unlockInProgress) {
|
if (!root.unlockInProgress) {
|
||||||
root.unlockInProgress = true;
|
|
||||||
passwd.abort();
|
passwd.abort();
|
||||||
root.unlockRequested();
|
root.proceedAfterPrimaryAuth();
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -135,6 +170,74 @@ 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 {
|
Process {
|
||||||
id: availProc
|
id: availProc
|
||||||
|
|
||||||
@@ -145,6 +248,16 @@ 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 {
|
Timer {
|
||||||
id: errorRetry
|
id: errorRetry
|
||||||
|
|
||||||
@@ -152,6 +265,20 @@ Scope {
|
|||||||
onTriggered: fprint.start()
|
onTriggered: fprint.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fErrorRetry
|
||||||
|
|
||||||
|
interval: 800
|
||||||
|
onTriggered: u2f.start()
|
||||||
|
}
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
id: u2fPendingTimeout
|
||||||
|
|
||||||
|
interval: 30000
|
||||||
|
onTriggered: root.cancelU2fPending()
|
||||||
|
}
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: stateReset
|
id: stateReset
|
||||||
|
|
||||||
@@ -175,13 +302,22 @@ Scope {
|
|||||||
onLockSecuredChanged: {
|
onLockSecuredChanged: {
|
||||||
if (lockSecured) {
|
if (lockSecured) {
|
||||||
availProc.running = true;
|
availProc.running = true;
|
||||||
|
u2fAvailProc.running = true;
|
||||||
root.state = "";
|
root.state = "";
|
||||||
root.fprintState = "";
|
root.fprintState = "";
|
||||||
|
root.u2fState = "";
|
||||||
|
root.u2fPending = false;
|
||||||
root.lockMessage = "";
|
root.lockMessage = "";
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
} else {
|
} else {
|
||||||
fprint.abort();
|
fprint.abort();
|
||||||
passwd.abort();
|
passwd.abort();
|
||||||
|
u2f.abort();
|
||||||
|
errorRetry.running = false;
|
||||||
|
u2fErrorRetry.running = false;
|
||||||
|
u2fPendingTimeout.running = false;
|
||||||
|
root.u2fPending = false;
|
||||||
|
root.u2fState = "";
|
||||||
root.unlockInProgress = false;
|
root.unlockInProgress = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -192,5 +328,20 @@ Scope {
|
|||||||
function onEnableFprintChanged(): void {
|
function onEnableFprintChanged(): void {
|
||||||
fprint.checkAvail();
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -137,12 +137,12 @@ Rectangle {
|
|||||||
return "";
|
return "";
|
||||||
const appIcon = historyItem.appIcon;
|
const appIcon = historyItem.appIcon;
|
||||||
if (!appIcon)
|
if (!appIcon)
|
||||||
return iconFromImage ? "image://icon/" + iconFromImage : "";
|
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
|
||||||
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
||||||
return appIcon;
|
return appIcon;
|
||||||
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
||||||
return "";
|
return "";
|
||||||
return Quickshell.iconPath(appIcon, true);
|
return Paths.resolveIconPath(appIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasImage: hasNotificationImage
|
hasImage: hasNotificationImage
|
||||||
|
|||||||
@@ -215,12 +215,12 @@ Rectangle {
|
|||||||
return "";
|
return "";
|
||||||
const appIcon = notificationGroup?.latestNotification?.appIcon;
|
const appIcon = notificationGroup?.latestNotification?.appIcon;
|
||||||
if (!appIcon)
|
if (!appIcon)
|
||||||
return iconFromImage ? "image://icon/" + iconFromImage : "";
|
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
|
||||||
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
||||||
return appIcon;
|
return appIcon;
|
||||||
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
||||||
return "";
|
return "";
|
||||||
return Quickshell.iconPath(appIcon, true);
|
return Paths.resolveIconPath(appIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasImage: hasNotificationImage
|
hasImage: hasNotificationImage
|
||||||
@@ -552,12 +552,12 @@ Rectangle {
|
|||||||
return "";
|
return "";
|
||||||
const appIcon = modelData?.appIcon;
|
const appIcon = modelData?.appIcon;
|
||||||
if (!appIcon)
|
if (!appIcon)
|
||||||
return iconFromImage ? "image://icon/" + iconFromImage : "";
|
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
|
||||||
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
||||||
return appIcon;
|
return appIcon;
|
||||||
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
||||||
return "";
|
return "";
|
||||||
return Quickshell.iconPath(appIcon, true);
|
return Paths.resolveIconPath(appIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
fallbackIcon: {
|
fallbackIcon: {
|
||||||
|
|||||||
@@ -39,11 +39,9 @@ DankPopout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popupWidth: triggerScreen ? Math.min(500, Math.max(380, triggerScreen.width - 48)) : 400
|
popupWidth: 400
|
||||||
popupHeight: stablePopupHeight
|
popupHeight: stablePopupHeight
|
||||||
positioning: ""
|
positioning: ""
|
||||||
animationScaleCollapsed: 0.94
|
|
||||||
animationOffset: 0
|
|
||||||
suspendShadowWhileResizing: false
|
suspendShadowWhileResizing: false
|
||||||
|
|
||||||
screen: triggerScreen
|
screen: triggerScreen
|
||||||
|
|||||||
@@ -34,51 +34,51 @@ Rectangle {
|
|||||||
|
|
||||||
readonly property var timeoutOptions: [
|
readonly property var timeoutOptions: [
|
||||||
{
|
{
|
||||||
"text": "Never",
|
"text": I18n.tr("Never"),
|
||||||
"value": 0
|
"value": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "1 second",
|
"text": I18n.tr("1 second"),
|
||||||
"value": 1000
|
"value": 1000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "3 seconds",
|
"text": I18n.tr("3 seconds"),
|
||||||
"value": 3000
|
"value": 3000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "5 seconds",
|
"text": I18n.tr("5 seconds"),
|
||||||
"value": 5000
|
"value": 5000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "8 seconds",
|
"text": I18n.tr("8 seconds"),
|
||||||
"value": 8000
|
"value": 8000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "10 seconds",
|
"text": I18n.tr("10 seconds"),
|
||||||
"value": 10000
|
"value": 10000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "15 seconds",
|
"text": I18n.tr("15 seconds"),
|
||||||
"value": 15000
|
"value": 15000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "30 seconds",
|
"text": I18n.tr("30 seconds"),
|
||||||
"value": 30000
|
"value": 30000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "1 minute",
|
"text": I18n.tr("1 minute"),
|
||||||
"value": 60000
|
"value": 60000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "2 minutes",
|
"text": I18n.tr("2 minutes"),
|
||||||
"value": 120000
|
"value": 120000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "5 minutes",
|
"text": I18n.tr("5 minutes"),
|
||||||
"value": 300000
|
"value": 300000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"text": "10 minutes",
|
"text": I18n.tr("10 minutes"),
|
||||||
"value": 600000
|
"value": 600000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -24,6 +24,29 @@ PanelWindow {
|
|||||||
property real _lastReportedAlignedHeight: -1
|
property real _lastReportedAlignedHeight: -1
|
||||||
property real _storedTopMargin: 0
|
property real _storedTopMargin: 0
|
||||||
property real _storedBottomMargin: 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")
|
readonly property string clearText: I18n.tr("Dismiss")
|
||||||
property bool descriptionExpanded: false
|
property bool descriptionExpanded: false
|
||||||
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
readonly property bool hasExpandableBody: (notificationData?.htmlBody || "").replace(/<[^>]*>/g, "").trim().length > 0
|
||||||
@@ -137,9 +160,9 @@ PanelWindow {
|
|||||||
enabled: !exiting && !_isDestroying
|
enabled: !exiting && !_isDestroying
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
id: implicitHeightAnim
|
id: implicitHeightAnim
|
||||||
duration: descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration
|
duration: Theme.variantDuration(descriptionExpanded ? Theme.notificationExpandDuration : Theme.notificationCollapseDuration, descriptionExpanded)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasized
|
easing.bezierCurve: descriptionExpanded ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,12 +542,12 @@ PanelWindow {
|
|||||||
return "";
|
return "";
|
||||||
const appIcon = notificationData.appIcon;
|
const appIcon = notificationData.appIcon;
|
||||||
if (!appIcon)
|
if (!appIcon)
|
||||||
return iconFromImage ? "image://icon/" + iconFromImage : "";
|
return iconFromImage ? Paths.resolveIconUrl(iconFromImage) : "";
|
||||||
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
if (appIcon.startsWith("file://") || appIcon.startsWith("http://") || appIcon.startsWith("https://") || appIcon.includes("/"))
|
||||||
return appIcon;
|
return appIcon;
|
||||||
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
if (appIcon.startsWith("material:") || appIcon.startsWith("svg:") || appIcon.startsWith("unicode:") || appIcon.startsWith("image:"))
|
||||||
return "";
|
return "";
|
||||||
return Quickshell.iconPath(appIcon, true);
|
return Paths.resolveIconPath(appIcon);
|
||||||
}
|
}
|
||||||
|
|
||||||
hasImage: hasNotificationImage
|
hasImage: hasNotificationImage
|
||||||
@@ -911,9 +934,9 @@ PanelWindow {
|
|||||||
if (isCenterPosition)
|
if (isCenterPosition)
|
||||||
return 0;
|
return 0;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
return isLeft ? -entryTravel : entryTravel;
|
||||||
}
|
}
|
||||||
y: isTopCenter ? -Anims.slidePx : isBottomCenter ? Anims.slidePx : 0
|
y: isTopCenter ? -entryTravel : isBottomCenter ? entryTravel : 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -925,16 +948,16 @@ PanelWindow {
|
|||||||
property: isCenterPosition ? "y" : "x"
|
property: isCenterPosition ? "y" : "x"
|
||||||
from: {
|
from: {
|
||||||
if (isTopCenter)
|
if (isTopCenter)
|
||||||
return -Anims.slidePx;
|
return -entryTravel;
|
||||||
if (isBottomCenter)
|
if (isBottomCenter)
|
||||||
return Anims.slidePx;
|
return entryTravel;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
return isLeft ? -entryTravel : entryTravel;
|
||||||
}
|
}
|
||||||
to: 0
|
to: 0
|
||||||
duration: Theme.notificationEnterDuration
|
duration: Theme.variantDuration(Theme.notificationEnterDuration, true)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: isCenterPosition ? Theme.expressiveCurves.standardDecel : Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantPopoutEnterCurve
|
||||||
onStopped: {
|
onStopped: {
|
||||||
if (!win.exiting && !win._isDestroying) {
|
if (!win.exiting && !win._isDestroying) {
|
||||||
if (isCenterPosition) {
|
if (isCenterPosition) {
|
||||||
@@ -959,35 +982,35 @@ PanelWindow {
|
|||||||
from: 0
|
from: 0
|
||||||
to: {
|
to: {
|
||||||
if (isTopCenter)
|
if (isTopCenter)
|
||||||
return -Anims.slidePx;
|
return -exitTravel;
|
||||||
if (isBottomCenter)
|
if (isBottomCenter)
|
||||||
return Anims.slidePx;
|
return exitTravel;
|
||||||
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
const isLeft = SettingsData.notificationPopupPosition === SettingsData.Position.Left || SettingsData.notificationPopupPosition === SettingsData.Position.Bottom;
|
||||||
return isLeft ? -Anims.slidePx : Anims.slidePx;
|
return isLeft ? -exitTravel : exitTravel;
|
||||||
}
|
}
|
||||||
duration: Theme.notificationExitDuration
|
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: content
|
target: content
|
||||||
property: "opacity"
|
property: "opacity"
|
||||||
from: 1
|
from: 1
|
||||||
to: 0
|
to: Theme.isDirectionalEffect ? 1 : 0
|
||||||
duration: Theme.notificationExitDuration
|
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.standardAccel
|
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
|
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
target: content
|
target: content
|
||||||
property: "scale"
|
property: "scale"
|
||||||
from: 1
|
from: 1
|
||||||
to: 0.98
|
to: Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed
|
||||||
duration: Theme.notificationExitDuration
|
duration: Theme.variantDuration(Theme.notificationExitDuration, false)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedAccel
|
easing.bezierCurve: Theme.variantPopoutExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,11 +27,11 @@ DankOSD {
|
|||||||
let icon = "music_note";
|
let icon = "music_note";
|
||||||
switch (player.playbackState) {
|
switch (player.playbackState) {
|
||||||
case MprisPlaybackState.Playing:
|
case MprisPlaybackState.Playing:
|
||||||
icon = "play_arrow";
|
icon = "pause";
|
||||||
break;
|
break;
|
||||||
case MprisPlaybackState.Paused:
|
case MprisPlaybackState.Paused:
|
||||||
case MprisPlaybackState.Stopped:
|
case MprisPlaybackState.Stopped:
|
||||||
icon = "pause";
|
icon = "play_arrow";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (icon === _displayIcon)
|
if (icon === _displayIcon)
|
||||||
|
|||||||
@@ -311,7 +311,9 @@ Item {
|
|||||||
const prefs = cfg?.screenPreferences || ["all"];
|
const prefs = cfg?.screenPreferences || ["all"];
|
||||||
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"))
|
if (prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all"))
|
||||||
return I18n.tr("All displays");
|
return I18n.tr("All displays");
|
||||||
return I18n.tr("%1 display(s)").replace("%1", prefs.length);
|
return prefs.length === 1
|
||||||
|
? I18n.tr("%1 display").arg(prefs.length)
|
||||||
|
: I18n.tr("%1 displays").arg(prefs.length);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
|
|||||||
@@ -344,7 +344,11 @@ Item {
|
|||||||
return I18n.tr("%1 exists but is not included in config. Custom keybinds will not work until this is fixed.").arg(bindsFile);
|
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) {
|
if (warningBox.showWarning) {
|
||||||
const count = warningBox.status.overriddenBy;
|
const count = warningBox.status.overriddenBy;
|
||||||
return I18n.tr("%1 DMS bind(s) may be overridden by config binds that come after the include.").arg(count);
|
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 "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -897,7 +897,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
width: 24
|
width: 24
|
||||||
height: 24
|
height: 24
|
||||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
||||||
sourceSize.width: 24
|
sourceSize.width: 24
|
||||||
sourceSize.height: 24
|
sourceSize.height: 24
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
@@ -1008,7 +1008,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
width: 24
|
width: 24
|
||||||
height: 24
|
height: 24
|
||||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
||||||
sourceSize.width: 24
|
sourceSize.width: 24
|
||||||
sourceSize.height: 24
|
sourceSize.height: 24
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
@@ -1154,7 +1154,7 @@ Item {
|
|||||||
Image {
|
Image {
|
||||||
width: 24
|
width: 24
|
||||||
height: 24
|
height: 24
|
||||||
source: modelData.icon ? "image://icon/" + modelData.icon : "image://icon/application-x-executable"
|
source: Paths.resolveIconUrl(modelData.icon || "application-x-executable")
|
||||||
sourceSize.width: 24
|
sourceSize.width: 24
|
||||||
sourceSize.height: 24
|
sourceSize.height: 24
|
||||||
fillMode: Image.PreserveAspectFit
|
fillMode: Image.PreserveAspectFit
|
||||||
@@ -1170,7 +1170,7 @@ Item {
|
|||||||
spacing: 2
|
spacing: 2
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: modelData.name || "Unknown App"
|
text: modelData.name || I18n.tr("Unknown App")
|
||||||
font.pixelSize: Theme.fontSizeMedium
|
font.pixelSize: Theme.fontSizeMedium
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -1179,7 +1179,7 @@ Item {
|
|||||||
StyledText {
|
StyledText {
|
||||||
text: {
|
text: {
|
||||||
if (!modelData.lastUsed)
|
if (!modelData.lastUsed)
|
||||||
return "Never used";
|
return I18n.tr("Never used");
|
||||||
var date = new Date(modelData.lastUsed);
|
var date = new Date(modelData.lastUsed);
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
var diffMs = now - date;
|
var diffMs = now - date;
|
||||||
@@ -1189,11 +1189,17 @@ Item {
|
|||||||
if (diffMins < 1)
|
if (diffMins < 1)
|
||||||
return I18n.tr("Last launched just now");
|
return I18n.tr("Last launched just now");
|
||||||
if (diffMins < 60)
|
if (diffMins < 60)
|
||||||
return I18n.tr("Last launched %1 minute%2 ago").arg(diffMins).arg(diffMins === 1 ? "" : "s");
|
return diffMins === 1
|
||||||
|
? I18n.tr("Last launched %1 minute ago").arg(diffMins)
|
||||||
|
: I18n.tr("Last launched %1 minutes ago").arg(diffMins);
|
||||||
if (diffHours < 24)
|
if (diffHours < 24)
|
||||||
return I18n.tr("Last launched %1 hour%2 ago").arg(diffHours).arg(diffHours === 1 ? "" : "s");
|
return diffHours === 1
|
||||||
|
? I18n.tr("Last launched %1 hour ago").arg(diffHours)
|
||||||
|
: I18n.tr("Last launched %1 hours ago").arg(diffHours);
|
||||||
if (diffDays < 7)
|
if (diffDays < 7)
|
||||||
return I18n.tr("Last launched %1 day%2 ago").arg(diffDays).arg(diffDays === 1 ? "" : "s");
|
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").arg(date.toLocaleDateString());
|
return I18n.tr("Last launched %1").arg(date.toLocaleDateString());
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Modals.FileBrowser
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Widgets
|
import qs.Widgets
|
||||||
import qs.Modules.Settings.Widgets
|
import qs.Modules.Settings.Widgets
|
||||||
@@ -8,6 +9,16 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
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 {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -161,11 +172,120 @@ Item {
|
|||||||
settingKey: "enableFprint"
|
settingKey: "enableFprint"
|
||||||
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
tags: ["lock", "screen", "fingerprint", "authentication", "biometric", "fprint"]
|
||||||
text: I18n.tr("Enable fingerprint authentication")
|
text: I18n.tr("Enable fingerprint authentication")
|
||||||
description: I18n.tr("Use fingerprint reader for lock screen authentication (requires enrolled fingerprints)")
|
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
|
||||||
checked: SettingsData.enableFprint
|
checked: SettingsData.enableFprint
|
||||||
visible: SettingsData.fprintdAvailable
|
enabled: SettingsData.fprintdAvailable
|
||||||
onToggled: checked => SettingsData.set("enableFprint", checked)
|
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 {
|
SettingsCard {
|
||||||
|
|||||||
@@ -340,7 +340,9 @@ Item {
|
|||||||
if (devices.length === 0)
|
if (devices.length === 0)
|
||||||
return I18n.tr("No adapters");
|
return I18n.tr("No adapters");
|
||||||
if (connected === 0)
|
if (connected === 0)
|
||||||
return I18n.tr("%1 adapter(s), none connected").arg(devices.length);
|
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 connected").arg(connected);
|
return I18n.tr("%1 connected").arg(connected);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ Item {
|
|||||||
LayoutMirroring.childrenInherit: true
|
LayoutMirroring.childrenInherit: true
|
||||||
|
|
||||||
property bool showAddPrinter: false
|
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 newPrinterName: ""
|
||||||
property string selectedDeviceUri: ""
|
property string selectedDeviceUri: ""
|
||||||
property var selectedDevice: null
|
property var selectedDevice: null
|
||||||
@@ -23,6 +29,12 @@ Item {
|
|||||||
property var suggestedPPDs: []
|
property var suggestedPPDs: []
|
||||||
|
|
||||||
function resetAddPrinterForm() {
|
function resetAddPrinterForm() {
|
||||||
|
manualEntryMode = false;
|
||||||
|
manualHost = "";
|
||||||
|
manualPort = "631";
|
||||||
|
manualProtocol = "ipp";
|
||||||
|
testingConnection = false;
|
||||||
|
testConnectionResult = null;
|
||||||
newPrinterName = "";
|
newPrinterName = "";
|
||||||
selectedDeviceUri = "";
|
selectedDeviceUri = "";
|
||||||
selectedDevice = null;
|
selectedDevice = null;
|
||||||
@@ -32,6 +44,45 @@ Item {
|
|||||||
suggestedPPDs = [];
|
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) {
|
function selectDevice(device) {
|
||||||
if (!device)
|
if (!device)
|
||||||
return;
|
return;
|
||||||
@@ -276,9 +327,93 @@ Item {
|
|||||||
color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
|
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 {
|
Column {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
spacing: Theme.spacingS
|
spacing: Theme.spacingS
|
||||||
|
visible: !printerTab.manualEntryMode
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -351,6 +486,202 @@ Item {
|
|||||||
elide: Text.ElideRight
|
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 {
|
Row {
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -548,7 +879,7 @@ Item {
|
|||||||
const count = CupsService.printerNames.length;
|
const count = CupsService.printerNames.length;
|
||||||
if (count === 0)
|
if (count === 0)
|
||||||
return I18n.tr("No printers configured");
|
return I18n.tr("No printers configured");
|
||||||
return I18n.tr("%1 printer(s)").arg(count);
|
return I18n.ntr("%1 printer", "%1 printers", count).arg(count);
|
||||||
}
|
}
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
@@ -698,7 +1029,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("%1 job(s)").arg(printerData?.jobs?.length ?? 0)
|
text: I18n.ntr("%1 job", "%1 jobs", printerData?.jobs?.length ?? 0).arg(printerData?.jobs?.length ?? 0)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
visible: (printerData?.jobs?.length ?? 0) > 0
|
visible: (printerData?.jobs?.length ?? 0) > 0
|
||||||
@@ -1245,7 +1576,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("%1 class(es)").arg(CupsService.printerClasses.length)
|
text: I18n.ntr("%1 class", "%1 classes", CupsService.printerClasses.length).arg(CupsService.printerClasses.length)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
width: parent.width
|
width: parent.width
|
||||||
@@ -1310,7 +1641,7 @@ Item {
|
|||||||
}
|
}
|
||||||
|
|
||||||
StyledText {
|
StyledText {
|
||||||
text: I18n.tr("%1 printer(s)").arg(modelData.members?.length ?? 0)
|
text: I18n.ntr("%1 printer", "%1 printers", modelData.members?.length ?? 0).arg(modelData.members?.length ?? 0)
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
color: Theme.surfaceVariantText
|
color: Theme.surfaceVariantText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2100,6 +2100,7 @@ Item {
|
|||||||
tags: ["modal", "darken", "background", "overlay"]
|
tags: ["modal", "darken", "background", "overlay"]
|
||||||
title: I18n.tr("Modal Background")
|
title: I18n.tr("Modal Background")
|
||||||
settingKey: "modalBackground"
|
settingKey: "modalBackground"
|
||||||
|
iconName: "layers"
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
tab: "theme"
|
tab: "theme"
|
||||||
@@ -2117,7 +2118,7 @@ Item {
|
|||||||
tags: ["applications", "portal", "dark", "terminal"]
|
tags: ["applications", "portal", "dark", "terminal"]
|
||||||
title: I18n.tr("Applications")
|
title: I18n.tr("Applications")
|
||||||
settingKey: "applications"
|
settingKey: "applications"
|
||||||
iconName: "terminal"
|
iconName: "apps"
|
||||||
|
|
||||||
SettingsToggleRow {
|
SettingsToggleRow {
|
||||||
tab: "theme"
|
tab: "theme"
|
||||||
@@ -2334,6 +2335,7 @@ Item {
|
|||||||
tags: ["icon", "theme", "system"]
|
tags: ["icon", "theme", "system"]
|
||||||
title: I18n.tr("Icon Theme")
|
title: I18n.tr("Icon Theme")
|
||||||
settingKey: "iconTheme"
|
settingKey: "iconTheme"
|
||||||
|
iconName: "interests"
|
||||||
|
|
||||||
SettingsDropdownRow {
|
SettingsDropdownRow {
|
||||||
tab: "theme"
|
tab: "theme"
|
||||||
@@ -2672,7 +2674,7 @@ Item {
|
|||||||
tags: ["system", "app", "theming", "gtk", "qt"]
|
tags: ["system", "app", "theming", "gtk", "qt"]
|
||||||
title: I18n.tr("System App Theming")
|
title: I18n.tr("System App Theming")
|
||||||
settingKey: "systemAppTheming"
|
settingKey: "systemAppTheming"
|
||||||
iconName: "extension"
|
iconName: "brush"
|
||||||
visible: Theme.matugenAvailable
|
visible: Theme.matugenAvailable
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
|
|||||||
@@ -9,6 +9,22 @@ import qs.Modules.Settings.Widgets
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
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 {
|
DankFlickable {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
clip: true
|
clip: true
|
||||||
@@ -69,12 +85,39 @@ Item {
|
|||||||
settingKey: "dateFormat"
|
settingKey: "dateFormat"
|
||||||
iconName: "calendar_today"
|
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 {
|
SettingsDropdownRow {
|
||||||
tab: "time"
|
tab: "time"
|
||||||
tags: ["date", "format", "topbar"]
|
tags: ["date", "format", "topbar"]
|
||||||
settingKey: "clockDateFormat"
|
settingKey: "clockDateFormat"
|
||||||
text: I18n.tr("Top Bar Format")
|
text: I18n.tr("Top Bar Format")
|
||||||
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(Qt.locale(), "ddd d"))
|
description: "Preview: " + (SettingsData.clockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) : new Date().toLocaleDateString(I18n.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")]
|
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: {
|
currentValue: {
|
||||||
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0)
|
if (!SettingsData.clockDateFormat || SettingsData.clockDateFormat.length === 0)
|
||||||
@@ -161,7 +204,7 @@ Item {
|
|||||||
tags: ["date", "format", "lock", "screen"]
|
tags: ["date", "format", "lock", "screen"]
|
||||||
settingKey: "lockDateFormat"
|
settingKey: "lockDateFormat"
|
||||||
text: I18n.tr("Lock Screen Format")
|
text: I18n.tr("Lock Screen Format")
|
||||||
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(Qt.locale(), Locale.LongFormat))
|
description: "Preview: " + (SettingsData.lockDateFormat ? new Date().toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat) : new Date().toLocaleDateString(I18n.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")]
|
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: {
|
currentValue: {
|
||||||
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0)
|
if (!SettingsData.lockDateFormat || SettingsData.lockDateFormat.length === 0)
|
||||||
|
|||||||
@@ -55,6 +55,180 @@ Item {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
spacing: Theme.spacingXL
|
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 {
|
SettingsCard {
|
||||||
tab: "typography"
|
tab: "typography"
|
||||||
tags: ["font", "family", "text", "typography"]
|
tags: ["font", "family", "text", "typography"]
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ Variants {
|
|||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: renderSettleTimer
|
id: renderSettleTimer
|
||||||
interval: 100
|
interval: 1000
|
||||||
onTriggered: root._renderSettling = false
|
onTriggered: root._renderSettling = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -121,9 +121,9 @@ Scope {
|
|||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,45 +154,69 @@ Scope {
|
|||||||
id: scaleTransform
|
id: scaleTransform
|
||||||
origin.x: contentContainer.width / 2
|
origin.x: contentContainer.width / 2
|
||||||
origin.y: contentContainer.height / 2
|
origin.y: contentContainer.height / 2
|
||||||
xScale: overviewScope.overviewOpen ? 1 : 0.96
|
xScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
||||||
yScale: overviewScope.overviewOpen ? 1 : 0.96
|
yScale: overviewScope.overviewOpen ? 1 : Theme.effectScaleCollapsed
|
||||||
|
|
||||||
Behavior on xScale {
|
Behavior on xScale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on yScale {
|
Behavior on yScale {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Translate {
|
Translate {
|
||||||
id: motionTransform
|
id: motionTransform
|
||||||
x: 0
|
x: {
|
||||||
y: overviewScope.overviewOpen ? 0 : Theme.spacingL
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewScope.overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: overviewScope.overviewOpen ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
easing.bezierCurve: overviewScope.overviewOpen ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -202,8 +202,18 @@ Scope {
|
|||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: spotlightContainer
|
id: spotlightContainer
|
||||||
x: Theme.snap((parent.width - width) / 2, overlayWindow.dpr)
|
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||||
y: Theme.snap((parent.height - height) / 2, overlayWindow.dpr)
|
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)
|
||||||
|
|
||||||
readonly property int baseWidth: {
|
readonly property int baseWidth: {
|
||||||
switch (SettingsData.dankLauncherV2Size) {
|
switch (SettingsData.dankLauncherV2Size) {
|
||||||
@@ -234,8 +244,8 @@ Scope {
|
|||||||
|
|
||||||
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
readonly property bool animatingOut: niriOverviewScope.isClosing && overlayWindow.isSpotlightScreen
|
||||||
|
|
||||||
scale: overlayWindow.shouldShowSpotlight ? 1.0 : 0.96
|
scale: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1.0 : Theme.effectScaleCollapsed)
|
||||||
opacity: overlayWindow.shouldShowSpotlight ? 1 : 0
|
opacity: Theme.isDirectionalEffect ? 1 : (overlayWindow.shouldShowSpotlight ? 1 : 0)
|
||||||
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
visible: overlayWindow.shouldShowSpotlight || animatingOut
|
||||||
enabled: overlayWindow.shouldShowSpotlight
|
enabled: overlayWindow.shouldShowSpotlight
|
||||||
|
|
||||||
@@ -245,10 +255,11 @@ Scope {
|
|||||||
|
|
||||||
Behavior on scale {
|
Behavior on scale {
|
||||||
id: scaleAnimation
|
id: scaleAnimation
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.fast
|
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
easing.bezierCurve: spotlightContainer.visible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||||
onRunningChanged: {
|
onRunningChanged: {
|
||||||
if (running || !spotlightContainer.animatingOut)
|
if (running || !spotlightContainer.animatingOut)
|
||||||
return;
|
return;
|
||||||
@@ -258,10 +269,27 @@ Scope {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Behavior on opacity {
|
Behavior on opacity {
|
||||||
|
enabled: !Theme.isDirectionalEffect
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.fast
|
duration: Theme.variantDuration(Theme.expressiveDurations.fast, overlayWindow.shouldShowSpotlight)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: spotlightContainer.visible ? Theme.expressiveCurves.expressiveFastSpatial : Theme.expressiveCurves.standardAccel
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -62,30 +62,30 @@ Item {
|
|||||||
|
|
||||||
Behavior on x {
|
Behavior on x {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on y {
|
Behavior on y {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,16 +124,16 @@ Item {
|
|||||||
|
|
||||||
Behavior on width {
|
Behavior on width {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Behavior on height {
|
Behavior on height {
|
||||||
NumberAnimation {
|
NumberAnimation {
|
||||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, overviewOpen)
|
||||||
easing.type: Easing.BezierSpline
|
easing.type: Easing.BezierSpline
|
||||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
easing.bezierCurve: Theme.variantModalEnterCurve
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ DesktopPluginComponent {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.bottom: parent.bottom
|
anchors.bottom: parent.bottom
|
||||||
anchors.bottomMargin: Theme.spacingXS
|
anchors.bottomMargin: Theme.spacingXS
|
||||||
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
|
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||||
font.pixelSize: Theme.fontSizeSmall
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
font.weight: Font.Medium
|
font.weight: Font.Medium
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
@@ -163,7 +163,7 @@ DesktopPluginComponent {
|
|||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.top: timeText.bottom
|
anchors.top: timeText.bottom
|
||||||
anchors.topMargin: Theme.spacingXS
|
anchors.topMargin: Theme.spacingXS
|
||||||
text: systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? ""
|
text: systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? ""
|
||||||
font.pixelSize: digitalRoot.dateFontSize
|
font.pixelSize: digitalRoot.dateFontSize
|
||||||
color: Theme.surfaceText
|
color: Theme.surfaceText
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -687,6 +687,12 @@ Singleton {
|
|||||||
appCategories.forEach(cat => categories.add(cat));
|
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();
|
const pluginCategories = getPluginCategories();
|
||||||
pluginCategories.forEach(cat => categories.add(cat));
|
pluginCategories.forEach(cat => categories.add(cat));
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import Quickshell
|
|||||||
import Quickshell.Io
|
import Quickshell.Io
|
||||||
import Quickshell.Services.Pipewire
|
import Quickshell.Services.Pipewire
|
||||||
import qs.Common
|
import qs.Common
|
||||||
|
import qs.Services
|
||||||
|
|
||||||
Singleton {
|
Singleton {
|
||||||
id: root
|
id: root
|
||||||
@@ -14,7 +15,7 @@ Singleton {
|
|||||||
readonly property PwNode sink: Pipewire.defaultAudioSink
|
readonly property PwNode sink: Pipewire.defaultAudioSink
|
||||||
readonly property PwNode source: Pipewire.defaultAudioSource
|
readonly property PwNode source: Pipewire.defaultAudioSource
|
||||||
|
|
||||||
property bool soundsAvailable: false
|
readonly property bool soundsAvailable: MultimediaService.available
|
||||||
property bool gsettingsAvailable: false
|
property bool gsettingsAvailable: false
|
||||||
property var availableSoundThemes: []
|
property var availableSoundThemes: []
|
||||||
property string currentSoundTheme: ""
|
property string currentSoundTheme: ""
|
||||||
@@ -312,24 +313,6 @@ 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() {
|
function checkGsettings() {
|
||||||
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
Proc.runCommand("checkGsettings", ["sh", "-c", "gsettings get org.gnome.desktop.sound theme-name 2>/dev/null"], (output, exitCode) => {
|
||||||
gsettingsAvailable = (exitCode === 0);
|
gsettingsAvailable = (exitCode === 0);
|
||||||
@@ -1028,10 +1011,7 @@ EOFCONFIG
|
|||||||
}
|
}
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (!detectSoundsAvailability()) {
|
if (soundsAvailable) {
|
||||||
console.warn("AudioService: QtMultimedia not available - sound effects disabled");
|
|
||||||
} else {
|
|
||||||
console.info("AudioService: Sound effects enabled");
|
|
||||||
checkGsettings();
|
checkGsettings();
|
||||||
Qt.callLater(createSoundPlayers);
|
Qt.callLater(createSoundPlayers);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,8 +56,8 @@ Singleton {
|
|||||||
}
|
}
|
||||||
readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging)
|
readonly property bool isCharging: batteryAvailable && batteries.some(b => b.state === UPowerDeviceState.Charging)
|
||||||
|
|
||||||
// Is the system plugged in (none of the batteries are discharging or empty)
|
// Is the system plugged in (Is not running on battery)
|
||||||
readonly property bool isPluggedIn: batteryAvailable && batteries.every(b => b.state !== UPowerDeviceState.Discharging)
|
readonly property bool isPluggedIn: !UPower.onBattery
|
||||||
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
readonly property bool isLowBattery: batteryAvailable && batteryLevel <= 20
|
||||||
|
|
||||||
onIsPluggedInChanged: {
|
onIsPluggedInChanged: {
|
||||||
|
|||||||
@@ -157,12 +157,12 @@ Singleton {
|
|||||||
// Parse start and end dates using detected format
|
// Parse start and end dates using detected format
|
||||||
let startDate, endDate
|
let startDate, endDate
|
||||||
if (event['start-date']) {
|
if (event['start-date']) {
|
||||||
startDate = Date.fromLocaleString(Qt.locale(), event['start-date'], root.khalDateFormat)
|
startDate = Date.fromLocaleString(I18n.locale(), event['start-date'], root.khalDateFormat)
|
||||||
} else {
|
} else {
|
||||||
startDate = new Date()
|
startDate = new Date()
|
||||||
}
|
}
|
||||||
if (event['end-date']) {
|
if (event['end-date']) {
|
||||||
endDate = Date.fromLocaleString(Qt.locale(), event['end-date'], root.khalDateFormat)
|
endDate = Date.fromLocaleString(I18n.locale(), event['end-date'], root.khalDateFormat)
|
||||||
} else {
|
} else {
|
||||||
endDate = new Date(startDate)
|
endDate = new Date(startDate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -479,6 +479,21 @@ 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) {
|
function createPrinter(name, deviceURI, ppd, options) {
|
||||||
if (!cupsAvailable)
|
if (!cupsAvailable)
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -61,12 +61,13 @@ Singleton {
|
|||||||
signal appPickerRequested(var data)
|
signal appPickerRequested(var data)
|
||||||
signal screensaverStateUpdate(var data)
|
signal screensaverStateUpdate(var data)
|
||||||
signal clipboardStateUpdate(var data)
|
signal clipboardStateUpdate(var data)
|
||||||
|
signal locationStateUpdate(var data)
|
||||||
|
|
||||||
property bool capsLockState: false
|
property bool capsLockState: false
|
||||||
property bool screensaverInhibited: false
|
property bool screensaverInhibited: false
|
||||||
property var screensaverInhibitors: []
|
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"]
|
property var activeSubscriptions: ["network", "network.credentials", "loginctl", "freedesktop", "freedesktop.screensaver", "gamma", "theme.auto", "bluetooth", "bluetooth.pairing", "dwl", "brightness", "wlroutput", "evdev", "browser", "dbus", "clipboard", "location"]
|
||||||
|
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
if (socketPath && socketPath.length > 0) {
|
if (socketPath && socketPath.length > 0) {
|
||||||
@@ -284,7 +285,7 @@ Singleton {
|
|||||||
|
|
||||||
function removeSubscription(service) {
|
function removeSubscription(service) {
|
||||||
if (activeSubscriptions.includes("all")) {
|
if (activeSubscriptions.includes("all")) {
|
||||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser"];
|
const allServices = ["network", "loginctl", "freedesktop", "gamma", "bluetooth", "dwl", "brightness", "extworkspace", "browser", "location"];
|
||||||
const filtered = allServices.filter(s => s !== service);
|
const filtered = allServices.filter(s => s !== service);
|
||||||
subscribe(filtered);
|
subscribe(filtered);
|
||||||
} else {
|
} else {
|
||||||
@@ -306,7 +307,7 @@ Singleton {
|
|||||||
excludeServices = [excludeServices];
|
excludeServices = [excludeServices];
|
||||||
}
|
}
|
||||||
|
|
||||||
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus"];
|
const allServices = ["network", "loginctl", "freedesktop", "gamma", "theme.auto", "bluetooth", "cups", "dwl", "brightness", "extworkspace", "browser", "dbus", "location"];
|
||||||
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
const filtered = allServices.filter(s => !excludeServices.includes(s));
|
||||||
subscribe(filtered);
|
subscribe(filtered);
|
||||||
}
|
}
|
||||||
@@ -395,6 +396,8 @@ Singleton {
|
|||||||
dbusSignalReceived(data.subscriptionId || "", data);
|
dbusSignalReceived(data.subscriptionId || "", data);
|
||||||
} else if (service === "clipboard") {
|
} else if (service === "clipboard") {
|
||||||
clipboardStateUpdate(data);
|
clipboardStateUpdate(data);
|
||||||
|
} else if (service === "location") {
|
||||||
|
locationStateUpdate(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ Singleton {
|
|||||||
readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json"
|
readonly property string historyFile: Paths.strip(Paths.cache) + "/notification_history.json"
|
||||||
readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images"
|
readonly property string imageCacheDir: Paths.strip(Paths.cache) + "/notification_images"
|
||||||
property bool historyLoaded: false
|
property bool historyLoaded: false
|
||||||
|
property int historyEntryCounter: 0
|
||||||
|
|
||||||
property list<NotifWrapper> notificationQueue: []
|
property list<NotifWrapper> notificationQueue: []
|
||||||
property list<NotifWrapper> visibleNotifications: []
|
property list<NotifWrapper> visibleNotifications: []
|
||||||
@@ -73,6 +74,12 @@ Singleton {
|
|||||||
onTriggered: root.performSaveHistory()
|
onTriggered: root.performSaveHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function _makeHistoryEntryId(sourceId, timestamp) {
|
||||||
|
historyEntryCounter += 1;
|
||||||
|
const safeSource = sourceId && sourceId !== "" ? sourceId : "notification";
|
||||||
|
return safeSource + "_" + (timestamp || Date.now()) + "_" + historyEntryCounter;
|
||||||
|
}
|
||||||
|
|
||||||
function getImageCachePath(wrapper) {
|
function getImageCachePath(wrapper) {
|
||||||
const ts = wrapper.time ? wrapper.time.getTime() : Date.now();
|
const ts = wrapper.time ? wrapper.time.getTime() : Date.now();
|
||||||
const id = wrapper.notification?.id?.toString() || "0";
|
const id = wrapper.notification?.id?.toString() || "0";
|
||||||
@@ -80,12 +87,13 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateHistoryImage(wrapperId, imagePath) {
|
function updateHistoryImage(wrapperId, imagePath) {
|
||||||
const idx = historyList.findIndex(n => n.id === wrapperId);
|
const idx = historyList.findIndex(n => n.sourceNotificationId === wrapperId || n.id === wrapperId);
|
||||||
if (idx < 0)
|
if (idx < 0)
|
||||||
return;
|
return;
|
||||||
const item = historyList[idx];
|
const item = historyList[idx];
|
||||||
const updated = {
|
const updated = {
|
||||||
id: item.id,
|
id: item.id,
|
||||||
|
sourceNotificationId: item.sourceNotificationId || item.id,
|
||||||
summary: item.summary,
|
summary: item.summary,
|
||||||
body: item.body,
|
body: item.body,
|
||||||
htmlBody: item.htmlBody,
|
htmlBody: item.htmlBody,
|
||||||
@@ -113,8 +121,11 @@ Singleton {
|
|||||||
} else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) {
|
} else if (imageUrl && !imageUrl.startsWith("image://qsimage/")) {
|
||||||
persistableImage = imageUrl;
|
persistableImage = imageUrl;
|
||||||
}
|
}
|
||||||
|
const sourceNotificationId = wrapper.notification?.id?.toString() || "";
|
||||||
|
const timestamp = wrapper.time.getTime();
|
||||||
const data = {
|
const data = {
|
||||||
id: wrapper.notification?.id?.toString() || Date.now().toString(),
|
id: _makeHistoryEntryId(sourceNotificationId, timestamp),
|
||||||
|
sourceNotificationId: sourceNotificationId,
|
||||||
summary: wrapper.summary || "",
|
summary: wrapper.summary || "",
|
||||||
body: wrapper.body || "",
|
body: wrapper.body || "",
|
||||||
htmlBody: wrapper.htmlBody || wrapper.body || "",
|
htmlBody: wrapper.htmlBody || wrapper.body || "",
|
||||||
@@ -122,7 +133,7 @@ Singleton {
|
|||||||
appIcon: wrapper.appIcon || "",
|
appIcon: wrapper.appIcon || "",
|
||||||
image: persistableImage,
|
image: persistableImage,
|
||||||
urgency: urg,
|
urgency: urg,
|
||||||
timestamp: wrapper.time.getTime(),
|
timestamp: timestamp,
|
||||||
desktopEntry: wrapper.desktopEntry || ""
|
desktopEntry: wrapper.desktopEntry || ""
|
||||||
};
|
};
|
||||||
let newList = [data, ...historyList];
|
let newList = [data, ...historyList];
|
||||||
@@ -152,6 +163,8 @@ Singleton {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const maxAgeMs = maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : 0;
|
const maxAgeMs = maxAgeDays > 0 ? maxAgeDays * 24 * 60 * 60 * 1000 : 0;
|
||||||
const loaded = [];
|
const loaded = [];
|
||||||
|
const seenIds = {};
|
||||||
|
let needsRewrite = false;
|
||||||
|
|
||||||
for (const item of historyAdapter.notifications || []) {
|
for (const item of historyAdapter.notifications || []) {
|
||||||
if (maxAgeMs > 0 && (now - item.timestamp) > maxAgeMs)
|
if (maxAgeMs > 0 && (now - item.timestamp) > maxAgeMs)
|
||||||
@@ -162,8 +175,18 @@ Singleton {
|
|||||||
if (htmlBody) {
|
if (htmlBody) {
|
||||||
htmlBody = htmlBody.replace(/<img\b[^>]*>/gi, "");
|
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({
|
loaded.push({
|
||||||
id: item.id || "",
|
id: historyId,
|
||||||
|
sourceNotificationId: sourceNotificationId,
|
||||||
summary: item.summary || "",
|
summary: item.summary || "",
|
||||||
body: body,
|
body: body,
|
||||||
htmlBody: htmlBody,
|
htmlBody: htmlBody,
|
||||||
@@ -177,7 +200,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
historyList = loaded;
|
historyList = loaded;
|
||||||
historyLoaded = true;
|
historyLoaded = true;
|
||||||
if (maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length)
|
if ((maxAgeMs > 0 && loaded.length !== (historyAdapter.notifications || []).length) || needsRewrite)
|
||||||
saveHistory();
|
saveHistory();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("NotificationService: load history failed:", e);
|
console.warn("NotificationService: load history failed:", e);
|
||||||
@@ -252,7 +275,7 @@ Singleton {
|
|||||||
if (daysDiff === 0)
|
if (daysDiff === 0)
|
||||||
return timeStr;
|
return timeStr;
|
||||||
try {
|
try {
|
||||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
|
||||||
const weekday = date.toLocaleDateString(localeName, {
|
const weekday = date.toLocaleDateString(localeName, {
|
||||||
weekday: "long"
|
weekday: "long"
|
||||||
});
|
});
|
||||||
@@ -695,7 +718,7 @@ Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const localeName = (typeof Qt !== "undefined" && Qt.locale) ? Qt.locale().name : "en-US";
|
const localeName = (typeof I18n !== "undefined" && I18n.locale) ? I18n.locale().name : "en-US";
|
||||||
const weekday = time.toLocaleDateString(localeName, {
|
const weekday = time.toLocaleDateString(localeName, {
|
||||||
weekday: "long"
|
weekday: "long"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -44,24 +44,26 @@ Singleton {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly property var archBasedPMSettings: {
|
readonly property var archBasedPMSettings: function(requiresSudo) {
|
||||||
"listUpdatesSettings": {
|
return {
|
||||||
"params": ["-Qu"],
|
"listUpdatesSettings": {
|
||||||
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
|
"params": ["-Qu"],
|
||||||
},
|
"correctExitCodes": [0, 1] // Exit code 0 = updates available, 1 = no updates
|
||||||
"upgradeSettings": {
|
},
|
||||||
"params": ["-Syu"],
|
"upgradeSettings": {
|
||||||
"requiresSudo": false
|
"params": ["-Syu"],
|
||||||
},
|
"requiresSudo": requiresSudo
|
||||||
"parserSettings": {
|
},
|
||||||
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
|
"parserSettings": {
|
||||||
"entryProducer": function (match) {
|
"lineRegex": /^(\S+)\s+([^\s]+)\s+->\s+([^\s]+)$/,
|
||||||
return {
|
"entryProducer": function (match) {
|
||||||
"name": match[1],
|
return {
|
||||||
"currentVersion": match[2],
|
"name": match[1],
|
||||||
"newVersion": match[3],
|
"currentVersion": match[2],
|
||||||
"description": `${match[1]} ${match[2]} → ${match[3]}`
|
"newVersion": match[3],
|
||||||
};
|
"description": `${match[1]} ${match[2]} → ${match[3]}`
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,8 +94,9 @@ Singleton {
|
|||||||
"checkupdates": archBasedUCSettings
|
"checkupdates": archBasedUCSettings
|
||||||
}
|
}
|
||||||
readonly property var packageManagerParams: {
|
readonly property var packageManagerParams: {
|
||||||
"yay": archBasedPMSettings,
|
"yay": archBasedPMSettings(false),
|
||||||
"paru": archBasedPMSettings,
|
"paru": archBasedPMSettings(false),
|
||||||
|
"pacman": archBasedPMSettings(true),
|
||||||
"dnf": fedoraBasedPMSettings
|
"dnf": fedoraBasedPMSettings
|
||||||
}
|
}
|
||||||
readonly property list<string> supportedDistributions: ["arch", "artix", "cachyos", "manjaro", "endeavouros", "fedora"]
|
readonly property list<string> supportedDistributions: ["arch", "artix", "cachyos", "manjaro", "endeavouros", "fedora"]
|
||||||
@@ -182,7 +185,7 @@ Singleton {
|
|||||||
|
|
||||||
Process {
|
Process {
|
||||||
id: pkgManagerDetection
|
id: pkgManagerDetection
|
||||||
command: ["sh", "-c", "which paru || which yay || which dnf"]
|
command: ["sh", "-c", "which paru || which yay || which pacman || which dnf"]
|
||||||
|
|
||||||
onExited: exitCode => {
|
onExited: exitCode => {
|
||||||
if (exitCode === 0) {
|
if (exitCode === 0) {
|
||||||
@@ -259,7 +262,7 @@ Singleton {
|
|||||||
const terminal = Quickshell.env("TERMINAL") || "xterm";
|
const terminal = Quickshell.env("TERMINAL") || "xterm";
|
||||||
|
|
||||||
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
|
if (SettingsData.updaterUseCustomCommand && SettingsData.updaterCustomCommand.length > 0) {
|
||||||
const updateCommand = `${SettingsData.updaterCustomCommand} && echo "Updates complete! Press Enter to close..." && read`;
|
const updateCommand = `${SettingsData.updaterCustomCommand} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
|
||||||
const termClass = SettingsData.updaterTerminalAdditionalParams;
|
const termClass = SettingsData.updaterTerminalAdditionalParams;
|
||||||
|
|
||||||
var finalCommand = [terminal];
|
var finalCommand = [terminal];
|
||||||
@@ -274,7 +277,7 @@ Singleton {
|
|||||||
} else {
|
} else {
|
||||||
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ");
|
const params = packageManagerParams[pkgManager].upgradeSettings.params.join(" ");
|
||||||
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : "";
|
const sudo = packageManagerParams[pkgManager].upgradeSettings.requiresSudo ? "sudo" : "";
|
||||||
const updateCommand = `${sudo} ${pkgManager} ${params} && echo "Updates complete! Press Enter to close..." && read`;
|
const updateCommand = `${sudo} ${pkgManager} ${params} && echo -n "Updates complete! " ; echo "Press Enter to close..." && read`;
|
||||||
|
|
||||||
updater.command = [terminal, "-e", "sh", "-c", updateCommand];
|
updater.command = [terminal, "-e", "sh", "-c", updateCommand];
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user