1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-31 08:52:49 -05:00

Compare commits

...

5 Commits

Author SHA1 Message Date
bbedward
c2787f1282 wallpaper: disable cycling if any toplevel is full screen 2025-11-24 22:28:53 -05:00
bbedward
df940124b1 net: allow overriding wifi device 2025-11-24 21:27:18 -05:00
bbedward
5288d042ca media: fix player button control popup things 2025-11-24 20:51:05 -05:00
bbedward
fa98a27c90 dankbar: add generic bar widget IPC for popouts
fixes #750
2025-11-24 19:52:26 -05:00
bbedward
d341a5a60b dankbar/controlcenter: add VPN, mic, brightness, battery, and printer
options for widget
2025-11-24 16:36:49 -05:00
42 changed files with 4501 additions and 3364 deletions

View File

@@ -28,7 +28,7 @@ packages:
outpkg: mocks_brightness outpkg: mocks_brightness
interfaces: interfaces:
DBusConn: DBusConn:
github.com/AvengeMedia/danklinux/internal/server/network: github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config: config:
dir: "internal/mocks/network" dir: "internal/mocks/network"
outpkg: mocks_network outpkg: mocks_network

View File

@@ -509,6 +509,52 @@ func (_c *MockBackend_DisconnectWiFi_Call) RunAndReturn(run func() error) *MockB
return _c return _c
} }
// DisconnectWiFiDevice provides a mock function with given fields: device
func (_m *MockBackend) DisconnectWiFiDevice(device string) error {
ret := _m.Called(device)
if len(ret) == 0 {
panic("no return value specified for DisconnectWiFiDevice")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(device)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_DisconnectWiFiDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'DisconnectWiFiDevice'
type MockBackend_DisconnectWiFiDevice_Call struct {
*mock.Call
}
// DisconnectWiFiDevice is a helper method to define mock.On call
// - device string
func (_e *MockBackend_Expecter) DisconnectWiFiDevice(device interface{}) *MockBackend_DisconnectWiFiDevice_Call {
return &MockBackend_DisconnectWiFiDevice_Call{Call: _e.mock.On("DisconnectWiFiDevice", device)}
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) Run(run func(device string)) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) Return(_a0 error) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_DisconnectWiFiDevice_Call) RunAndReturn(run func(string) error) *MockBackend_DisconnectWiFiDevice_Call {
_c.Call.Return(run)
return _c
}
// ForgetWiFiNetwork provides a mock function with given fields: ssid // ForgetWiFiNetwork provides a mock function with given fields: ssid
func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error { func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error {
ret := _m.Called(ssid) ret := _m.Called(ssid)
@@ -659,6 +705,53 @@ func (_c *MockBackend_GetPromptBroker_Call) RunAndReturn(run func() network.Prom
return _c return _c
} }
// GetWiFiDevices provides a mock function with no fields
func (_m *MockBackend) GetWiFiDevices() []network.WiFiDevice {
ret := _m.Called()
if len(ret) == 0 {
panic("no return value specified for GetWiFiDevices")
}
var r0 []network.WiFiDevice
if rf, ok := ret.Get(0).(func() []network.WiFiDevice); ok {
r0 = rf()
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).([]network.WiFiDevice)
}
}
return r0
}
// MockBackend_GetWiFiDevices_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetWiFiDevices'
type MockBackend_GetWiFiDevices_Call struct {
*mock.Call
}
// GetWiFiDevices is a helper method to define mock.On call
func (_e *MockBackend_Expecter) GetWiFiDevices() *MockBackend_GetWiFiDevices_Call {
return &MockBackend_GetWiFiDevices_Call{Call: _e.mock.On("GetWiFiDevices")}
}
func (_c *MockBackend_GetWiFiDevices_Call) Run(run func()) *MockBackend_GetWiFiDevices_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockBackend_GetWiFiDevices_Call) Return(_a0 []network.WiFiDevice) *MockBackend_GetWiFiDevices_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_GetWiFiDevices_Call) RunAndReturn(run func() []network.WiFiDevice) *MockBackend_GetWiFiDevices_Call {
_c.Call.Return(run)
return _c
}
// GetWiFiEnabled provides a mock function with no fields // GetWiFiEnabled provides a mock function with no fields
func (_m *MockBackend) GetWiFiEnabled() (bool, error) { func (_m *MockBackend) GetWiFiEnabled() (bool, error) {
ret := _m.Called() ret := _m.Called()
@@ -1091,6 +1184,52 @@ func (_c *MockBackend_ScanWiFi_Call) RunAndReturn(run func() error) *MockBackend
return _c return _c
} }
// ScanWiFiDevice provides a mock function with given fields: device
func (_m *MockBackend) ScanWiFiDevice(device string) error {
ret := _m.Called(device)
if len(ret) == 0 {
panic("no return value specified for ScanWiFiDevice")
}
var r0 error
if rf, ok := ret.Get(0).(func(string) error); ok {
r0 = rf(device)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockBackend_ScanWiFiDevice_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ScanWiFiDevice'
type MockBackend_ScanWiFiDevice_Call struct {
*mock.Call
}
// ScanWiFiDevice is a helper method to define mock.On call
// - device string
func (_e *MockBackend_Expecter) ScanWiFiDevice(device interface{}) *MockBackend_ScanWiFiDevice_Call {
return &MockBackend_ScanWiFiDevice_Call{Call: _e.mock.On("ScanWiFiDevice", device)}
}
func (_c *MockBackend_ScanWiFiDevice_Call) Run(run func(device string)) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockBackend_ScanWiFiDevice_Call) Return(_a0 error) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Return(_a0)
return _c
}
func (_c *MockBackend_ScanWiFiDevice_Call) RunAndReturn(run func(string) error) *MockBackend_ScanWiFiDevice_Call {
_c.Call.Return(run)
return _c
}
// SetPromptBroker provides a mock function with given fields: broker // SetPromptBroker provides a mock function with given fields: broker
func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error { func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error {
ret := _m.Called(broker) ret := _m.Called(broker)

View File

@@ -8,10 +8,13 @@ type Backend interface {
SetWiFiEnabled(enabled bool) error SetWiFiEnabled(enabled bool) error
ScanWiFi() error ScanWiFi() error
ScanWiFiDevice(device string) error
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error) GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
GetWiFiDevices() []WiFiDevice
ConnectWiFi(req ConnectionRequest) error ConnectWiFi(req ConnectionRequest) error
DisconnectWiFi() error DisconnectWiFi() error
DisconnectWiFiDevice(device string) error
ForgetWiFiNetwork(ssid string) error ForgetWiFiNetwork(ssid string) error
SetWiFiAutoconnect(ssid string, autoconnect bool) error SetWiFiAutoconnect(ssid string, autoconnect bool) error
@@ -54,11 +57,13 @@ type BackendState struct {
WiFiBSSID string WiFiBSSID string
WiFiSignal uint8 WiFiSignal uint8
WiFiNetworks []WiFiNetwork WiFiNetworks []WiFiNetwork
WiFiDevices []WiFiDevice
WiredConnections []WiredConnection WiredConnections []WiredConnection
VPNProfiles []VPNProfile VPNProfiles []VPNProfile
VPNActive []VPNActive VPNActive []VPNActive
IsConnecting bool IsConnecting bool
ConnectingSSID string ConnectingSSID string
ConnectingDevice string
IsConnectingVPN bool IsConnectingVPN bool
ConnectingVPNUUID string ConnectingVPNUUID string
LastError string LastError string

View File

@@ -196,3 +196,15 @@ func (b *HybridIwdNetworkdBackend) CancelCredentials(token string) error {
func (b *HybridIwdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { func (b *HybridIwdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
return b.wifi.SetWiFiAutoconnect(ssid, autoconnect) return b.wifi.SetWiFiAutoconnect(ssid, autoconnect)
} }
func (b *HybridIwdNetworkdBackend) ScanWiFiDevice(device string) error {
return b.wifi.ScanWiFiDevice(device)
}
func (b *HybridIwdNetworkdBackend) DisconnectWiFiDevice(device string) error {
return b.wifi.DisconnectWiFiDevice(device)
}
func (b *HybridIwdNetworkdBackend) GetWiFiDevices() []WiFiDevice {
return b.wifi.GetWiFiDevices()
}

View File

@@ -139,9 +139,13 @@ func (b *IWDBackend) discoverDevices() error {
} }
func (b *IWDBackend) GetCurrentState() (*BackendState, error) { func (b *IWDBackend) GetCurrentState() (*BackendState, error) {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
state := *b.state state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.WiFiDevices = b.getWiFiDevicesLocked()
return &state, nil return &state, nil
} }

View File

@@ -45,3 +45,38 @@ func (b *IWDBackend) DisconnectAllVPN() error {
func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error { func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error {
return fmt.Errorf("VPN not supported by iwd backend") return fmt.Errorf("VPN not supported by iwd backend")
} }
func (b *IWDBackend) ScanWiFiDevice(device string) error {
return b.ScanWiFi()
}
func (b *IWDBackend) DisconnectWiFiDevice(device string) error {
return b.DisconnectWiFi()
}
func (b *IWDBackend) GetWiFiDevices() []WiFiDevice {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
return b.getWiFiDevicesLocked()
}
func (b *IWDBackend) getWiFiDevicesLocked() []WiFiDevice {
if b.state.WiFiDevice == "" {
return nil
}
stateStr := "disconnected"
if b.state.WiFiConnected {
stateStr = "connected"
}
return []WiFiDevice{{
Name: b.state.WiFiDevice,
State: stateStr,
Connected: b.state.WiFiConnected,
SSID: b.state.WiFiSSID,
Signal: b.state.WiFiSignal,
IP: b.state.WiFiIP,
Networks: b.state.WiFiNetworks,
}}
}

View File

@@ -57,3 +57,15 @@ func (b *SystemdNetworkdBackend) ClearVPNCredentials(uuidOrName string) error {
func (b *SystemdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error { func (b *SystemdNetworkdBackend) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
return fmt.Errorf("WiFi autoconnect not supported by networkd backend") return fmt.Errorf("WiFi autoconnect not supported by networkd backend")
} }
func (b *SystemdNetworkdBackend) ScanWiFiDevice(device string) error {
return fmt.Errorf("WiFi scan not supported by networkd backend")
}
func (b *SystemdNetworkdBackend) DisconnectWiFiDevice(device string) error {
return fmt.Errorf("WiFi disconnect not supported by networkd backend")
}
func (b *SystemdNetworkdBackend) GetWiFiDevices() []WiFiDevice {
return nil
}

View File

@@ -30,12 +30,20 @@ const (
NmDeviceStateReasonNewActivation = 60 NmDeviceStateReasonNewActivation = 60
) )
type wifiDeviceInfo struct {
device gonetworkmanager.Device
wireless gonetworkmanager.DeviceWireless
name string
hwAddress string
}
type NetworkManagerBackend struct { type NetworkManagerBackend struct {
nmConn interface{} nmConn interface{}
ethernetDevice interface{} ethernetDevice interface{}
wifiDevice interface{} wifiDevice interface{}
settings interface{} settings interface{}
wifiDev interface{} wifiDev interface{}
wifiDevices map[string]*wifiDeviceInfo
dbusConn *dbus.Conn dbusConn *dbus.Conn
signals chan *dbus.Signal signals chan *dbus.Signal
@@ -71,8 +79,9 @@ func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*Netwo
} }
backend := &NetworkManagerBackend{ backend := &NetworkManagerBackend{
nmConn: nm, nmConn: nm,
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
wifiDevices: make(map[string]*wifiDeviceInfo),
state: &BackendState{ state: &BackendState{
Backend: "networkmanager", Backend: "networkmanager",
}, },
@@ -114,27 +123,48 @@ func (b *NetworkManagerBackend) Initialize() error {
} }
case gonetworkmanager.NmDeviceTypeWifi: case gonetworkmanager.NmDeviceTypeWifi:
b.wifiDevice = dev iface, err := dev.GetPropertyInterface()
if w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath()); err == nil { if err != nil {
b.wifiDev = w
}
wifiEnabled, err := nm.GetPropertyWirelessEnabled()
if err == nil {
b.stateMutex.Lock()
b.state.WiFiEnabled = wifiEnabled
b.stateMutex.Unlock()
}
if err := b.updateWiFiState(); err != nil {
continue continue
} }
if wifiEnabled { w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath())
if _, err := b.updateWiFiNetworks(); err != nil { if err != nil {
log.Warnf("Failed to get initial networks: %v", err) continue
} }
hwAddr, _ := w.GetPropertyHwAddress()
b.wifiDevices[iface] = &wifiDeviceInfo{
device: dev,
wireless: w,
name: iface,
hwAddress: hwAddr,
}
if b.wifiDevice == nil {
b.wifiDevice = dev
b.wifiDev = w
} }
} }
} }
wifiEnabled, err := nm.GetPropertyWirelessEnabled()
if err == nil {
b.stateMutex.Lock()
b.state.WiFiEnabled = wifiEnabled
b.stateMutex.Unlock()
}
if err := b.updateWiFiState(); err != nil {
log.Warnf("Failed to update WiFi state: %v", err)
}
if wifiEnabled {
if _, err := b.updateWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial networks: %v", err)
}
b.updateAllWiFiDevices()
}
if err := b.updatePrimaryConnection(); err != nil { if err := b.updatePrimaryConnection(); err != nil {
return err return err
} }
@@ -165,6 +195,7 @@ func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) {
state := *b.state state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...) state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.WiFiDevices = append([]WiFiDevice(nil), b.state.WiFiDevices...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...) state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.VPNProfiles = append([]VPNProfile(nil), b.state.VPNProfiles...) state.VPNProfiles = append([]VPNProfile(nil), b.state.VPNProfiles...)
state.VPNActive = append([]VPNActive(nil), b.state.VPNActive...) state.VPNActive = append([]VPNActive(nil), b.state.VPNActive...)

View File

@@ -197,21 +197,23 @@ func (b *NetworkManagerBackend) GetWiFiNetworkDetails(ssid string) (*NetworkInfo
} }
func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error { func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
if b.wifiDevice == nil { devInfo, err := b.getWifiDeviceForConnection(req.Device)
return fmt.Errorf("no WiFi device available") if err != nil {
return err
} }
b.stateMutex.RLock() b.stateMutex.RLock()
alreadyConnected := b.state.WiFiConnected && b.state.WiFiSSID == req.SSID alreadyConnected := b.state.WiFiConnected && b.state.WiFiSSID == req.SSID
b.stateMutex.RUnlock() b.stateMutex.RUnlock()
if alreadyConnected && !req.Interactive { if alreadyConnected && !req.Interactive && req.Device == "" {
return nil return nil
} }
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnecting = true b.state.IsConnecting = true
b.state.ConnectingSSID = req.SSID b.state.ConnectingSSID = req.SSID
b.state.ConnectingDevice = req.Device
b.state.LastError = "" b.state.LastError = ""
b.stateMutex.Unlock() b.stateMutex.Unlock()
@@ -223,14 +225,13 @@ func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
existingConn, err := b.findConnection(req.SSID) existingConn, err := b.findConnection(req.SSID)
if err == nil && existingConn != nil { if err == nil && existingConn != nil {
dev := b.wifiDevice.(gonetworkmanager.Device) _, err := nm.ActivateConnection(existingConn, devInfo.device, nil)
_, err := nm.ActivateConnection(existingConn, dev, nil)
if err != nil { if err != nil {
log.Warnf("[ConnectWiFi] Failed to activate existing connection: %v", err) log.Warnf("[ConnectWiFi] Failed to activate existing connection: %v", err)
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.ConnectingDevice = ""
b.state.LastError = fmt.Sprintf("failed to activate connection: %v", err) b.state.LastError = fmt.Sprintf("failed to activate connection: %v", err)
b.stateMutex.Unlock() b.stateMutex.Unlock()
if b.onStateChange != nil { if b.onStateChange != nil {
@@ -242,11 +243,12 @@ func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
return nil return nil
} }
if err := b.createAndConnectWiFi(req); err != nil { if err := b.createAndConnectWiFiOnDevice(req, devInfo); err != nil {
log.Warnf("[ConnectWiFi] Failed to create and connect: %v", err) log.Warnf("[ConnectWiFi] Failed to create and connect: %v", err)
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnecting = false b.state.IsConnecting = false
b.state.ConnectingSSID = "" b.state.ConnectingSSID = ""
b.state.ConnectingDevice = ""
b.state.LastError = err.Error() b.state.LastError = err.Error()
b.stateMutex.Unlock() b.stateMutex.Unlock()
if b.onStateChange != nil { if b.onStateChange != nil {
@@ -502,19 +504,17 @@ func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Co
} }
func (b *NetworkManagerBackend) createAndConnectWiFi(req ConnectionRequest) error { func (b *NetworkManagerBackend) createAndConnectWiFi(req ConnectionRequest) error {
if b.wifiDevice == nil { devInfo, err := b.getWifiDeviceForConnection(req.Device)
return fmt.Errorf("no WiFi device available") if err != nil {
}
nm := b.nmConn.(gonetworkmanager.NetworkManager)
dev := b.wifiDevice.(gonetworkmanager.Device)
if err := b.ensureWiFiDevice(); err != nil {
return err return err
} }
wifiDev := b.wifiDev return b.createAndConnectWiFiOnDevice(req, devInfo)
}
w := wifiDev.(gonetworkmanager.DeviceWireless) func (b *NetworkManagerBackend) createAndConnectWiFiOnDevice(req ConnectionRequest, devInfo *wifiDeviceInfo) error {
nm := b.nmConn.(gonetworkmanager.NetworkManager)
dev := devInfo.device
w := devInfo.wireless
apPaths, err := w.GetAccessPoints() apPaths, err := w.GetAccessPoints()
if err != nil { if err != nil {
return fmt.Errorf("failed to get access points: %w", err) return fmt.Errorf("failed to get access points: %w", err)
@@ -716,3 +716,254 @@ func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool
return nil return nil
} }
func (b *NetworkManagerBackend) ScanWiFiDevice(device string) error {
devInfo, ok := b.wifiDevices[device]
if !ok {
return fmt.Errorf("WiFi device not found: %s", device)
}
b.stateMutex.RLock()
enabled := b.state.WiFiEnabled
b.stateMutex.RUnlock()
if !enabled {
return fmt.Errorf("WiFi is disabled")
}
if err := devInfo.wireless.RequestScan(); err != nil {
return fmt.Errorf("scan request failed: %w", err)
}
b.updateAllWiFiDevices()
return nil
}
func (b *NetworkManagerBackend) DisconnectWiFiDevice(device string) error {
devInfo, ok := b.wifiDevices[device]
if !ok {
return fmt.Errorf("WiFi device not found: %s", device)
}
if err := devInfo.device.Disconnect(); err != nil {
return fmt.Errorf("failed to disconnect: %w", err)
}
b.updateWiFiState()
b.updateAllWiFiDevices()
b.updatePrimaryConnection()
if b.onStateChange != nil {
b.onStateChange()
}
return nil
}
func (b *NetworkManagerBackend) GetWiFiDevices() []WiFiDevice {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
return append([]WiFiDevice(nil), b.state.WiFiDevices...)
}
func (b *NetworkManagerBackend) updateAllWiFiDevices() {
s := b.settings
if s == nil {
var err error
s, err = gonetworkmanager.NewSettings()
if err != nil {
return
}
b.settings = s
}
settingsMgr := s.(gonetworkmanager.Settings)
connections, err := settingsMgr.ListConnections()
if err != nil {
return
}
savedSSIDs := make(map[string]bool)
autoconnectMap := make(map[string]bool)
for _, conn := range connections {
connSettings, err := conn.GetSettings()
if err != nil {
continue
}
connMeta, ok := connSettings["connection"]
if !ok {
continue
}
connType, ok := connMeta["type"].(string)
if !ok || connType != "802-11-wireless" {
continue
}
wifiSettings, ok := connSettings["802-11-wireless"]
if !ok {
continue
}
ssidBytes, ok := wifiSettings["ssid"].([]byte)
if !ok {
continue
}
ssid := string(ssidBytes)
savedSSIDs[ssid] = true
autoconnect := true
if ac, ok := connMeta["autoconnect"].(bool); ok {
autoconnect = ac
}
autoconnectMap[ssid] = autoconnect
}
var devices []WiFiDevice
for name, devInfo := range b.wifiDevices {
state, _ := devInfo.device.GetPropertyState()
connected := state == gonetworkmanager.NmDeviceStateActivated
var ssid, bssid, ip string
var signal uint8
if connected {
if activeAP, err := devInfo.wireless.GetPropertyActiveAccessPoint(); err == nil && activeAP != nil && activeAP.GetPath() != "/" {
ssid, _ = activeAP.GetPropertySSID()
signal, _ = activeAP.GetPropertyStrength()
bssid, _ = activeAP.GetPropertyHWAddress()
}
ip = b.getDeviceIP(devInfo.device)
}
stateStr := "disconnected"
switch state {
case gonetworkmanager.NmDeviceStateActivated:
stateStr = "connected"
case gonetworkmanager.NmDeviceStateConfig, gonetworkmanager.NmDeviceStateIpConfig:
stateStr = "connecting"
case gonetworkmanager.NmDeviceStatePrepare:
stateStr = "preparing"
case gonetworkmanager.NmDeviceStateDeactivating:
stateStr = "disconnecting"
}
apPaths, err := devInfo.wireless.GetAccessPoints()
var networks []WiFiNetwork
if err == nil {
seenSSIDs := make(map[string]*WiFiNetwork)
for _, ap := range apPaths {
apSSID, err := ap.GetPropertySSID()
if err != nil || apSSID == "" {
continue
}
if existing, exists := seenSSIDs[apSSID]; exists {
strength, _ := ap.GetPropertyStrength()
if strength > existing.Signal {
existing.Signal = strength
freq, _ := ap.GetPropertyFrequency()
existing.Frequency = freq
apBSSID, _ := ap.GetPropertyHWAddress()
existing.BSSID = apBSSID
}
continue
}
strength, _ := ap.GetPropertyStrength()
flags, _ := ap.GetPropertyFlags()
wpaFlags, _ := ap.GetPropertyWPAFlags()
rsnFlags, _ := ap.GetPropertyRSNFlags()
freq, _ := ap.GetPropertyFrequency()
maxBitrate, _ := ap.GetPropertyMaxBitrate()
apBSSID, _ := ap.GetPropertyHWAddress()
mode, _ := ap.GetPropertyMode()
secured := flags != uint32(gonetworkmanager.Nm80211APFlagsNone) ||
wpaFlags != uint32(gonetworkmanager.Nm80211APSecNone) ||
rsnFlags != uint32(gonetworkmanager.Nm80211APSecNone)
enterprise := (rsnFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0) ||
(wpaFlags&uint32(gonetworkmanager.Nm80211APSecKeyMgmt8021X) != 0)
var modeStr string
switch mode {
case gonetworkmanager.Nm80211ModeAdhoc:
modeStr = "adhoc"
case gonetworkmanager.Nm80211ModeInfra:
modeStr = "infrastructure"
case gonetworkmanager.Nm80211ModeAp:
modeStr = "ap"
default:
modeStr = "unknown"
}
channel := frequencyToChannel(freq)
network := WiFiNetwork{
SSID: apSSID,
BSSID: apBSSID,
Signal: strength,
Secured: secured,
Enterprise: enterprise,
Connected: connected && apSSID == ssid,
Saved: savedSSIDs[apSSID],
Autoconnect: autoconnectMap[apSSID],
Frequency: freq,
Mode: modeStr,
Rate: maxBitrate / 1000,
Channel: channel,
Device: name,
}
seenSSIDs[apSSID] = &network
networks = append(networks, network)
}
sortWiFiNetworks(networks)
}
devices = append(devices, WiFiDevice{
Name: name,
HwAddress: devInfo.hwAddress,
State: stateStr,
Connected: connected,
SSID: ssid,
BSSID: bssid,
Signal: signal,
IP: ip,
Networks: networks,
})
}
sort.Slice(devices, func(i, j int) bool {
return devices[i].Name < devices[j].Name
})
b.stateMutex.Lock()
b.state.WiFiDevices = devices
b.stateMutex.Unlock()
}
func (b *NetworkManagerBackend) getWifiDeviceForConnection(deviceName string) (*wifiDeviceInfo, error) {
if deviceName != "" {
devInfo, ok := b.wifiDevices[deviceName]
if !ok {
return nil, fmt.Errorf("WiFi device not found: %s", deviceName)
}
return devInfo, nil
}
if b.wifiDevice == nil {
return nil, fmt.Errorf("no WiFi device available")
}
dev := b.wifiDevice.(gonetworkmanager.Device)
iface, _ := dev.GetPropertyInterface()
if devInfo, ok := b.wifiDevices[iface]; ok {
return devInfo, nil
}
return nil, fmt.Errorf("no WiFi device available")
}

View File

@@ -101,10 +101,21 @@ func TestNetworkManagerBackend_ConnectWiFi_AlreadyConnected(t *testing.T) {
backend.wifiDevice = mockDeviceWireless backend.wifiDevice = mockDeviceWireless
backend.wifiDev = mockDeviceWireless backend.wifiDev = mockDeviceWireless
backend.wifiDevices = map[string]*wifiDeviceInfo{
"wlan0": {
device: nil,
wireless: mockDeviceWireless,
name: "wlan0",
hwAddress: "00:11:22:33:44:55",
},
}
mockDeviceWireless.EXPECT().GetPropertyInterface().Return("wlan0", nil)
backend.stateMutex.Lock() backend.stateMutex.Lock()
backend.state.WiFiConnected = true backend.state.WiFiConnected = true
backend.state.WiFiSSID = "TestNetwork" backend.state.WiFiSSID = "TestNetwork"
backend.state.WiFiDevice = "wlan0"
backend.stateMutex.Unlock() backend.stateMutex.Unlock()
req := ConnectionRequest{SSID: "TestNetwork", Password: "password"} req := ConnectionRequest{SSID: "TestNetwork", Password: "password"}

View File

@@ -135,7 +135,14 @@ func handleGetState(conn net.Conn, req Request, manager *Manager) {
} }
func handleScanWiFi(conn net.Conn, req Request, manager *Manager) { func handleScanWiFi(conn net.Conn, req Request, manager *Manager) {
if err := manager.ScanWiFi(); err != nil { device, _ := req.Params["device"].(string)
var err error
if device != "" {
err = manager.ScanWiFiDevice(device)
} else {
err = manager.ScanWiFi()
}
if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }
@@ -163,6 +170,9 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
if username, ok := req.Params["username"].(string); ok { if username, ok := req.Params["username"].(string); ok {
connReq.Username = username connReq.Username = username
} }
if device, ok := req.Params["device"].(string); ok {
connReq.Device = device
}
if interactive, ok := req.Params["interactive"].(bool); ok { if interactive, ok := req.Params["interactive"].(bool); ok {
connReq.Interactive = interactive connReq.Interactive = interactive
@@ -170,7 +180,7 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState() state := manager.GetState()
alreadyConnected := state.WiFiConnected && state.WiFiSSID == ssid alreadyConnected := state.WiFiConnected && state.WiFiSSID == ssid
if alreadyConnected { if alreadyConnected && connReq.Device == "" {
connReq.Interactive = false connReq.Interactive = false
} else { } else {
networkInfo, err := manager.GetNetworkInfo(ssid) networkInfo, err := manager.GetNetworkInfo(ssid)
@@ -200,7 +210,14 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
} }
func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) { func handleDisconnectWiFi(conn net.Conn, req Request, manager *Manager) {
if err := manager.DisconnectWiFi(); err != nil { device, _ := req.Params["device"].(string)
var err error
if device != "" {
err = manager.DisconnectWiFiDevice(device)
} else {
err = manager.DisconnectWiFi()
}
if err != nil {
models.RespondError(conn, req.ID, err.Error()) models.RespondError(conn, req.ID, err.Error())
return return
} }

View File

@@ -117,11 +117,13 @@ func (m *Manager) syncStateFromBackend() error {
m.state.WiFiBSSID = backendState.WiFiBSSID m.state.WiFiBSSID = backendState.WiFiBSSID
m.state.WiFiSignal = backendState.WiFiSignal m.state.WiFiSignal = backendState.WiFiSignal
m.state.WiFiNetworks = backendState.WiFiNetworks m.state.WiFiNetworks = backendState.WiFiNetworks
m.state.WiFiDevices = backendState.WiFiDevices
m.state.WiredConnections = backendState.WiredConnections m.state.WiredConnections = backendState.WiredConnections
m.state.VPNProfiles = backendState.VPNProfiles m.state.VPNProfiles = backendState.VPNProfiles
m.state.VPNActive = backendState.VPNActive m.state.VPNActive = backendState.VPNActive
m.state.IsConnecting = backendState.IsConnecting m.state.IsConnecting = backendState.IsConnecting
m.state.ConnectingSSID = backendState.ConnectingSSID m.state.ConnectingSSID = backendState.ConnectingSSID
m.state.ConnectingDevice = backendState.ConnectingDevice
m.state.LastError = backendState.LastError m.state.LastError = backendState.LastError
m.stateMutex.Unlock() m.stateMutex.Unlock()
@@ -151,6 +153,7 @@ func (m *Manager) snapshotState() NetworkState {
defer m.stateMutex.RUnlock() defer m.stateMutex.RUnlock()
s := *m.state s := *m.state
s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...) s.WiFiNetworks = append([]WiFiNetwork(nil), m.state.WiFiNetworks...)
s.WiFiDevices = append([]WiFiDevice(nil), m.state.WiFiDevices...)
s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...) s.WiredConnections = append([]WiredConnection(nil), m.state.WiredConnections...)
s.VPNProfiles = append([]VPNProfile(nil), m.state.VPNProfiles...) s.VPNProfiles = append([]VPNProfile(nil), m.state.VPNProfiles...)
s.VPNActive = append([]VPNActive(nil), m.state.VPNActive...) s.VPNActive = append([]VPNActive(nil), m.state.VPNActive...)
@@ -204,6 +207,9 @@ func stateChangedMeaningfully(old, new *NetworkState) bool {
if len(old.WiFiNetworks) != len(new.WiFiNetworks) { if len(old.WiFiNetworks) != len(new.WiFiNetworks) {
return true return true
} }
if len(old.WiFiDevices) != len(new.WiFiDevices) {
return true
}
if len(old.WiredConnections) != len(new.WiredConnections) { if len(old.WiredConnections) != len(new.WiredConnections) {
return true return true
} }
@@ -505,3 +511,19 @@ func (m *Manager) ClearVPNCredentials(uuidOrName string) error {
func (m *Manager) SetWiFiAutoconnect(ssid string, autoconnect bool) error { func (m *Manager) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
return m.backend.SetWiFiAutoconnect(ssid, autoconnect) return m.backend.SetWiFiAutoconnect(ssid, autoconnect)
} }
func (m *Manager) GetWiFiDevices() []WiFiDevice {
m.stateMutex.RLock()
defer m.stateMutex.RUnlock()
devices := make([]WiFiDevice, len(m.state.WiFiDevices))
copy(devices, m.state.WiFiDevices)
return devices
}
func (m *Manager) ScanWiFiDevice(device string) error {
return m.backend.ScanWiFiDevice(device)
}
func (m *Manager) DisconnectWiFiDevice(device string) error {
return m.backend.DisconnectWiFiDevice(device)
}

View File

@@ -37,6 +37,19 @@ type WiFiNetwork struct {
Mode string `json:"mode"` Mode string `json:"mode"`
Rate uint32 `json:"rate"` Rate uint32 `json:"rate"`
Channel uint32 `json:"channel"` Channel uint32 `json:"channel"`
Device string `json:"device,omitempty"`
}
type WiFiDevice struct {
Name string `json:"name"`
HwAddress string `json:"hwAddress"`
State string `json:"state"`
Connected bool `json:"connected"`
SSID string `json:"ssid,omitempty"`
BSSID string `json:"bssid,omitempty"`
Signal uint8 `json:"signal,omitempty"`
IP string `json:"ip,omitempty"`
Networks []WiFiNetwork `json:"networks"`
} }
type VPNProfile struct { type VPNProfile struct {
@@ -76,11 +89,13 @@ type NetworkState struct {
WiFiBSSID string `json:"wifiBSSID"` WiFiBSSID string `json:"wifiBSSID"`
WiFiSignal uint8 `json:"wifiSignal"` WiFiSignal uint8 `json:"wifiSignal"`
WiFiNetworks []WiFiNetwork `json:"wifiNetworks"` WiFiNetworks []WiFiNetwork `json:"wifiNetworks"`
WiFiDevices []WiFiDevice `json:"wifiDevices"`
WiredConnections []WiredConnection `json:"wiredConnections"` WiredConnections []WiredConnection `json:"wiredConnections"`
VPNProfiles []VPNProfile `json:"vpnProfiles"` VPNProfiles []VPNProfile `json:"vpnProfiles"`
VPNActive []VPNActive `json:"vpnActive"` VPNActive []VPNActive `json:"vpnActive"`
IsConnecting bool `json:"isConnecting"` IsConnecting bool `json:"isConnecting"`
ConnectingSSID string `json:"connectingSSID"` ConnectingSSID string `json:"connectingSSID"`
ConnectingDevice string `json:"connectingDevice,omitempty"`
LastError string `json:"lastError"` LastError string `json:"lastError"`
} }
@@ -91,6 +106,7 @@ type ConnectionRequest struct {
AnonymousIdentity string `json:"anonymousIdentity,omitempty"` AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"` DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
Interactive bool `json:"interactive,omitempty"` Interactive bool `json:"interactive,omitempty"`
Device string `json:"device,omitempty"`
} }
type WiredConnection struct { type WiredConnection struct {

View File

@@ -31,7 +31,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap" "github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
) )
const APIVersion = 19 const APIVersion = 20
type Capabilities struct { type Capabilities struct {
Capabilities []string `json:"capabilities"` Capabilities []string `json:"capabilities"`
@@ -1071,10 +1071,10 @@ func Start(printDocs bool) error {
log.Info(" plugins.search - Search plugins (params: query, category?, compositor?, capability?)") log.Info(" plugins.search - Search plugins (params: query, category?, compositor?, capability?)")
log.Info("Network:") log.Info("Network:")
log.Info(" network.getState - Get current network state") log.Info(" network.getState - Get current network state")
log.Info(" network.wifi.scan - Scan for WiFi networks") log.Info(" network.wifi.scan - Scan for WiFi networks (params: device?)")
log.Info(" network.wifi.networks - Get WiFi network list") log.Info(" network.wifi.networks - Get WiFi network list")
log.Info(" network.wifi.connect - Connect to WiFi (params: ssid, password?, username?)") log.Info(" network.wifi.connect - Connect to WiFi (params: ssid, password?, username?, device?)")
log.Info(" network.wifi.disconnect - Disconnect WiFi") log.Info(" network.wifi.disconnect - Disconnect WiFi (params: device?)")
log.Info(" network.wifi.forget - Forget network (params: ssid)") log.Info(" network.wifi.forget - Forget network (params: ssid)")
log.Info(" network.wifi.toggle - Toggle WiFi radio") log.Info(" network.wifi.toggle - Toggle WiFi radio")
log.Info(" network.wifi.enable - Enable WiFi") log.Info(" network.wifi.enable - Enable WiFi")

File diff suppressed because it is too large Load Diff

View File

@@ -105,6 +105,11 @@ Singleton {
property bool controlCenterShowNetworkIcon: true property bool controlCenterShowNetworkIcon: true
property bool controlCenterShowBluetoothIcon: true property bool controlCenterShowBluetoothIcon: true
property bool controlCenterShowAudioIcon: true property bool controlCenterShowAudioIcon: true
property bool controlCenterShowVpnIcon: false
property bool controlCenterShowBrightnessIcon: false
property bool controlCenterShowMicIcon: false
property bool controlCenterShowBatteryIcon: false
property bool controlCenterShowPrinterIcon: false
property bool showPrivacyButton: true property bool showPrivacyButton: true
property bool privacyShowMicIcon: false property bool privacyShowMicIcon: false
property bool privacyShowCameraIcon: false property bool privacyShowCameraIcon: false

View File

@@ -51,6 +51,11 @@ var SPEC = {
controlCenterShowNetworkIcon: { def: true }, controlCenterShowNetworkIcon: { def: true },
controlCenterShowBluetoothIcon: { def: true }, controlCenterShowBluetoothIcon: { def: true },
controlCenterShowAudioIcon: { def: true }, controlCenterShowAudioIcon: { def: true },
controlCenterShowVpnIcon: { def: false },
controlCenterShowBrightnessIcon: { def: false },
controlCenterShowMicIcon: { def: false },
controlCenterShowBatteryIcon: { def: false },
controlCenterShowPrinterIcon: { def: false },
showPrivacyButton: { def: true }, showPrivacyButton: { def: true },
privacyShowMicIcon: { def: false }, privacyShowMicIcon: { def: false },

View File

@@ -563,4 +563,42 @@ Item {
target: "file" target: "file"
} }
IpcHandler {
function toggle(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const success = BarWidgetService.triggerWidgetPopout(widgetId);
return success ? `WIDGET_TOGGLE_SUCCESS: ${widgetId}` : `WIDGET_TOGGLE_FAILED: ${widgetId}`;
}
function list(): string {
const widgets = BarWidgetService.getRegisteredWidgetIds();
if (widgets.length === 0)
return "No widgets registered";
return widgets.join("\n");
}
function status(widgetId: string): string {
if (!widgetId)
return "ERROR: No widget ID specified";
if (!BarWidgetService.hasWidget(widgetId))
return `WIDGET_NOT_FOUND: ${widgetId}`;
const widget = BarWidgetService.getWidgetOnFocusedScreen(widgetId);
if (!widget)
return `WIDGET_NOT_AVAILABLE: ${widgetId}`;
if (widget.popoutTarget?.shouldBeVisible)
return "visible";
return "hidden";
}
target: "widget"
}
} }

View File

@@ -2,7 +2,6 @@ import QtQuick
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Modules.ControlCenter.Details import qs.Modules.ControlCenter.Details
import qs.Modules.ControlCenter.Models
Item { Item {
id: root id: root
@@ -11,17 +10,21 @@ Item {
property var expandedWidgetData: null property var expandedWidgetData: null
property var bluetoothCodecSelector: null property var bluetoothCodecSelector: null
property string screenName: "" property string screenName: ""
property string screenModel: ""
property var pluginDetailInstance: null property var pluginDetailInstance: null
property var widgetModel: null property var widgetModel: null
property var collapseCallback: null property var collapseCallback: null
function getDetailHeight(section) { function getDetailHeight(section) {
const maxAvailable = parent ? parent.height - Theme.spacingS : 9999 const maxAvailable = parent ? parent.height - Theme.spacingS : 9999;
if (section === "wifi") return Math.min(350, maxAvailable) if (section === "wifi")
if (section === "bluetooth") return Math.min(350, maxAvailable) return Math.min(350, maxAvailable);
if (section.startsWith("brightnessSlider_")) return Math.min(400, maxAvailable) if (section === "bluetooth")
return Math.min(250, maxAvailable) return Math.min(350, maxAvailable);
if (section.startsWith("brightnessSlider_"))
return Math.min(400, maxAvailable);
return Math.min(250, maxAvailable);
} }
Loader { Loader {
@@ -49,18 +52,18 @@ Item {
function onDeviceNameChanged(newDeviceName) { function onDeviceNameChanged(newDeviceName) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") { if (root.expandedWidgetData && root.expandedWidgetData.id === "brightnessSlider") {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
const newWidgets = widgets.map(w => { const newWidgets = widgets.map(w => {
if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) { if (w.id === "brightnessSlider" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w) const updatedWidget = Object.assign({}, w);
updatedWidget.deviceName = newDeviceName updatedWidget.deviceName = newDeviceName;
return updatedWidget return updatedWidget;
} }
return w return w;
}) });
SettingsData.set("controlCenterWidgets", newWidgets) SettingsData.set("controlCenterWidgets", newWidgets);
if (root.collapseCallback) { if (root.collapseCallback) {
root.collapseCallback() root.collapseCallback();
} }
} }
} }
@@ -73,18 +76,18 @@ Item {
function onMountPathChanged(newMountPath) { function onMountPathChanged(newMountPath) {
if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") { if (root.expandedWidgetData && root.expandedWidgetData.id === "diskUsage") {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
const newWidgets = widgets.map(w => { const newWidgets = widgets.map(w => {
if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) { if (w.id === "diskUsage" && w.instanceId === root.expandedWidgetData.instanceId) {
const updatedWidget = Object.assign({}, w) const updatedWidget = Object.assign({}, w);
updatedWidget.mountPath = newMountPath updatedWidget.mountPath = newMountPath;
return updatedWidget return updatedWidget;
} }
return w return w;
}) });
SettingsData.set("controlCenterWidgets", newWidgets) SettingsData.set("controlCenterWidgets", newWidgets);
if (root.collapseCallback) { if (root.collapseCallback) {
root.collapseCallback() root.collapseCallback();
} }
} }
} }
@@ -92,86 +95,97 @@ Item {
onExpandedSectionChanged: { onExpandedSectionChanged: {
if (pluginDetailInstance) { if (pluginDetailInstance) {
pluginDetailInstance.destroy() pluginDetailInstance.destroy();
pluginDetailInstance = null pluginDetailInstance = null;
} }
pluginDetailLoader.active = false pluginDetailLoader.active = false;
coreDetailLoader.active = false coreDetailLoader.active = false;
if (!root.expandedSection) { if (!root.expandedSection) {
return return;
} }
if (root.expandedSection.startsWith("builtin_")) { if (root.expandedSection.startsWith("builtin_")) {
const builtinId = root.expandedSection const builtinId = root.expandedSection;
let builtinInstance = null let builtinInstance = null;
if (builtinId === "builtin_vpn") { if (builtinId === "builtin_vpn") {
if (widgetModel?.vpnLoader) { if (widgetModel?.vpnLoader) {
widgetModel.vpnLoader.active = true widgetModel.vpnLoader.active = true;
} }
builtinInstance = widgetModel.vpnBuiltinInstance builtinInstance = widgetModel.vpnBuiltinInstance;
} }
if (builtinId === "builtin_cups") { if (builtinId === "builtin_cups") {
if (widgetModel?.cupsLoader) { if (widgetModel?.cupsLoader) {
widgetModel.cupsLoader.active = true widgetModel.cupsLoader.active = true;
} }
builtinInstance = widgetModel.cupsBuiltinInstance builtinInstance = widgetModel.cupsBuiltinInstance;
} }
if (!builtinInstance || !builtinInstance.ccDetailContent) { if (!builtinInstance || !builtinInstance.ccDetailContent) {
return return;
} }
pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent pluginDetailLoader.sourceComponent = builtinInstance.ccDetailContent;
pluginDetailLoader.active = parent.height > 0 pluginDetailLoader.active = parent.height > 0;
return return;
} }
if (root.expandedSection.startsWith("plugin_")) { if (root.expandedSection.startsWith("plugin_")) {
const pluginId = root.expandedSection.replace("plugin_", "") const pluginId = root.expandedSection.replace("plugin_", "");
const pluginComponent = PluginService.pluginWidgetComponents[pluginId] const pluginComponent = PluginService.pluginWidgetComponents[pluginId];
if (!pluginComponent) { if (!pluginComponent) {
return return;
} }
pluginDetailInstance = pluginComponent.createObject(null) pluginDetailInstance = pluginComponent.createObject(null);
if (!pluginDetailInstance || !pluginDetailInstance.ccDetailContent) { if (!pluginDetailInstance || !pluginDetailInstance.ccDetailContent) {
if (pluginDetailInstance) { if (pluginDetailInstance) {
pluginDetailInstance.destroy() pluginDetailInstance.destroy();
pluginDetailInstance = null pluginDetailInstance = null;
} }
return return;
} }
pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent pluginDetailLoader.sourceComponent = pluginDetailInstance.ccDetailContent;
pluginDetailLoader.active = parent.height > 0 pluginDetailLoader.active = parent.height > 0;
return return;
} }
if (root.expandedSection.startsWith("diskUsage_")) { if (root.expandedSection.startsWith("diskUsage_")) {
coreDetailLoader.sourceComponent = diskUsageDetailComponent coreDetailLoader.sourceComponent = diskUsageDetailComponent;
coreDetailLoader.active = parent.height > 0 coreDetailLoader.active = parent.height > 0;
return return;
} }
if (root.expandedSection.startsWith("brightnessSlider_")) { if (root.expandedSection.startsWith("brightnessSlider_")) {
coreDetailLoader.sourceComponent = brightnessDetailComponent coreDetailLoader.sourceComponent = brightnessDetailComponent;
coreDetailLoader.active = parent.height > 0 coreDetailLoader.active = parent.height > 0;
return return;
} }
switch (root.expandedSection) { switch (root.expandedSection) {
case "network": case "network":
case "wifi": coreDetailLoader.sourceComponent = networkDetailComponent; break case "wifi":
case "bluetooth": coreDetailLoader.sourceComponent = bluetoothDetailComponent; break coreDetailLoader.sourceComponent = networkDetailComponent;
case "audioOutput": coreDetailLoader.sourceComponent = audioOutputDetailComponent; break break;
case "audioInput": coreDetailLoader.sourceComponent = audioInputDetailComponent; break case "bluetooth":
case "battery": coreDetailLoader.sourceComponent = batteryDetailComponent; break coreDetailLoader.sourceComponent = bluetoothDetailComponent;
default: return break;
case "audioOutput":
coreDetailLoader.sourceComponent = audioOutputDetailComponent;
break;
case "audioInput":
coreDetailLoader.sourceComponent = audioInputDetailComponent;
break;
case "battery":
coreDetailLoader.sourceComponent = batteryDetailComponent;
break;
default:
return;
} }
coreDetailLoader.active = parent.height > 0 coreDetailLoader.active = parent.height > 0;
} }
Component { Component {
@@ -183,12 +197,12 @@ Item {
id: bluetoothDetailComponent id: bluetoothDetailComponent
BluetoothDetail { BluetoothDetail {
id: bluetoothDetail id: bluetoothDetail
onShowCodecSelector: function(device) { onShowCodecSelector: function (device) {
if (root.bluetoothCodecSelector) { if (root.bluetoothCodecSelector) {
root.bluetoothCodecSelector.show(device) root.bluetoothCodecSelector.show(device);
root.bluetoothCodecSelector.codecSelected.connect(function(deviceAddress, codecName) { root.bluetoothCodecSelector.codecSelected.connect(function (deviceAddress, codecName) {
bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName) bluetoothDetail.updateDeviceCodecDisplay(deviceAddress, codecName);
}) });
} }
} }
} }
@@ -223,6 +237,7 @@ Item {
initialDeviceName: root.expandedWidgetData?.deviceName || "" initialDeviceName: root.expandedWidgetData?.deviceName || ""
instanceId: root.expandedWidgetData?.instanceId || "" instanceId: root.expandedWidgetData?.instanceId || ""
screenName: root.screenName screenName: root.screenName
screenModel: root.screenModel
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@@ -82,6 +82,7 @@ DankPopout {
onShouldBeVisibleChanged: { onShouldBeVisibleChanged: {
if (shouldBeVisible) { if (shouldBeVisible) {
collapseAll();
Qt.callLater(() => { Qt.callLater(() => {
if (NetworkService.activeService) { if (NetworkService.activeService) {
NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled; NetworkService.activeService.autoRefreshEnabled = NetworkService.wifiEnabled;
@@ -179,6 +180,7 @@ DankPopout {
bluetoothCodecSelector: bluetoothCodecSelector bluetoothCodecSelector: bluetoothCodecSelector
colorPickerModal: root.colorPickerModal colorPickerModal: root.colorPickerModal
screenName: root.triggerScreen?.name || "" screenName: root.triggerScreen?.name || ""
screenModel: root.triggerScreen?.model || ""
parentScreen: root.triggerScreen parentScreen: root.triggerScreen
onExpandClicked: (widgetData, globalIndex) => { onExpandClicked: (widgetData, globalIndex) => {
root.expandedWidgetIndex = globalIndex; root.expandedWidgetIndex = globalIndex;

View File

@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import qs.Common import qs.Common
@@ -10,8 +9,8 @@ Rectangle {
id: root id: root
property bool hasInputVolumeSliderInCC: { property bool hasInputVolumeSliderInCC: {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
return widgets.some(widget => widget.id === "inputVolumeSlider") return widgets.some(widget => widget.id === "inputVolumeSlider");
} }
implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM implicitHeight: headerRow.height + (hasInputVolumeSliderInCC ? 0 : volumeSlider.height) + audioContent.height + Theme.spacingM
@@ -66,7 +65,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (AudioService.source && AudioService.source.audio) { if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.muted = !AudioService.source.audio.muted AudioService.source.audio.muted = !AudioService.source.audio.muted;
} }
} }
} }
@@ -74,9 +73,10 @@ Rectangle {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: { name: {
if (!AudioService.source || !AudioService.source.audio) return "mic_off" if (!AudioService.source || !AudioService.source.audio)
let muted = AudioService.source.audio.muted return "mic_off";
return muted ? "mic_off" : "mic" let muted = AudioService.source.audio.muted;
return muted ? "mic_off" : "mic";
} }
size: Theme.iconSize size: Theme.iconSize
color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText color: AudioService.source && AudioService.source.audio && !AudioService.source.audio.muted && AudioService.source.audio.volume > 0 ? Theme.primary : Theme.surfaceText
@@ -97,11 +97,11 @@ Rectangle {
valueOverride: actualVolumePercent valueOverride: actualVolumePercent
thumbOutlineColor: Theme.surfaceVariant thumbOutlineColor: Theme.surfaceVariant
onSliderValueChanged: function(newValue) { onSliderValueChanged: function (newValue) {
if (AudioService.source && AudioService.source.audio) { if (AudioService.source && AudioService.source.audio) {
AudioService.source.audio.volume = newValue / 100 AudioService.source.audio.volume = newValue / 100;
if (newValue > 0 && AudioService.source.audio.muted) { if (newValue > 0 && AudioService.source.audio.muted) {
AudioService.source.audio.muted = false AudioService.source.audio.muted = false;
} }
} }
} }
@@ -128,22 +128,26 @@ Rectangle {
model: ScriptModel { model: ScriptModel {
values: { values: {
const nodes = Pipewire.nodes.values.filter(node => { const nodes = Pipewire.nodes.values.filter(node => {
return node.audio && !node.isSink && !node.isStream return node.audio && !node.isSink && !node.isStream;
}) });
const pins = SettingsData.audioInputDevicePins || {} const pins = SettingsData.audioInputDevicePins || {};
const pinnedName = pins["preferredInput"] const pinnedName = pins["preferredInput"];
let sorted = [...nodes] let sorted = [...nodes];
sorted.sort((a, b) => { sorted.sort((a, b) => {
// Pinned device first // Pinned device first
if (a.name === pinnedName && b.name !== pinnedName) return -1 if (a.name === pinnedName && b.name !== pinnedName)
if (b.name === pinnedName && a.name !== pinnedName) return 1 return -1;
if (b.name === pinnedName && a.name !== pinnedName)
return 1;
// Then active device // Then active device
if (a === AudioService.source && b !== AudioService.source) return -1 if (a === AudioService.source && b !== AudioService.source)
if (b === AudioService.source && a !== AudioService.source) return 1 return -1;
return 0 if (b === AudioService.source && a !== AudioService.source)
}) return 1;
return sorted return 0;
});
return sorted;
} }
} }
@@ -167,11 +171,11 @@ Rectangle {
DankIcon { DankIcon {
name: { name: {
if (modelData.name.includes("bluez")) if (modelData.name.includes("bluez"))
return "headset" return "headset";
else if (modelData.name.includes("usb")) else if (modelData.name.includes("usb"))
return "headset" return "headset";
else else
return "mic" return "mic";
} }
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText color: modelData === AudioService.source ? Theme.primary : Theme.surfaceText
@@ -181,9 +185,9 @@ Rectangle {
Column { Column {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: { width: {
const iconWidth = Theme.iconSize const iconWidth = Theme.iconSize;
const pinButtonWidth = pinInputRow.width + Theme.spacingS * 4 + Theme.spacingM const pinButtonWidth = pinInputRow.width + Theme.spacingS * 4 + Theme.spacingM;
return parent.parent.width - iconWidth - parent.spacing - pinButtonWidth - Theme.spacingM * 2 return parent.parent.width - iconWidth - parent.spacing - pinButtonWidth - Theme.spacingM * 2;
} }
StyledText { StyledText {
@@ -215,8 +219,8 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05) return isThisDevicePinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
} }
Row { Row {
@@ -228,21 +232,21 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
return isThisDevicePinned ? Theme.primary : Theme.surfaceText return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: { text: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
return isThisDevicePinned ? "Pinned" : "Pin" return isThisDevicePinned ? "Pinned" : "Pin";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name const isThisDevicePinned = (SettingsData.audioInputDevicePins || {})["preferredInput"] === modelData.name;
return isThisDevicePinned ? Theme.primary : Theme.surfaceText return isThisDevicePinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -252,16 +256,16 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {})) const pins = JSON.parse(JSON.stringify(SettingsData.audioInputDevicePins || {}));
const isCurrentlyPinned = pins["preferredInput"] === modelData.name const isCurrentlyPinned = pins["preferredInput"] === modelData.name;
if (isCurrentlyPinned) { if (isCurrentlyPinned) {
delete pins["preferredInput"] delete pins["preferredInput"];
} else { } else {
pins["preferredInput"] = modelData.name pins["preferredInput"] = modelData.name;
} }
SettingsData.set("audioInputDevicePins", pins) SettingsData.set("audioInputDevicePins", pins);
} }
} }
} }
@@ -274,7 +278,7 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (modelData) { if (modelData) {
Pipewire.preferredDefaultAudioSource = modelData Pipewire.preferredDefaultAudioSource = modelData;
} }
} }
} }

View File

@@ -1,6 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
@@ -11,88 +9,91 @@ Rectangle {
property string initialDeviceName: "" property string initialDeviceName: ""
property string instanceId: "" property string instanceId: ""
property string screenName: "" property string screenName: ""
property string screenModel: ""
signal deviceNameChanged(string newDeviceName) signal deviceNameChanged(string newDeviceName)
property string currentDeviceName: "" property string currentDeviceName: ""
function getScreenPinKey() {
if (SettingsData.displayNameMode === "model" && screenModel && screenModel.length > 0) {
return screenModel;
}
return screenName || "";
}
function resolveDeviceName() { function resolveDeviceName() {
if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) { if (!DisplayService.brightnessAvailable || !DisplayService.devices || DisplayService.devices.length === 0) {
return "" return "";
} }
if (screenName && screenName.length > 0) { const pinKey = getScreenPinKey();
const pins = SettingsData.brightnessDevicePins || {} if (pinKey.length > 0) {
const pinnedDevice = pins[screenName] const pins = SettingsData.brightnessDevicePins || {};
const pinnedDevice = pins[pinKey];
if (pinnedDevice && pinnedDevice.length > 0) { if (pinnedDevice && pinnedDevice.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === pinnedDevice) const found = DisplayService.devices.find(dev => dev.name === pinnedDevice);
if (found) { if (found)
return found.name return found.name;
}
} }
} }
if (initialDeviceName && initialDeviceName.length > 0) { if (initialDeviceName && initialDeviceName.length > 0) {
const found = DisplayService.devices.find(dev => dev.name === initialDeviceName) const found = DisplayService.devices.find(dev => dev.name === initialDeviceName);
if (found) { if (found)
return found.name return found.name;
}
} }
const currentDeviceNameFromService = DisplayService.currentDevice const currentDeviceNameFromService = DisplayService.currentDevice;
if (currentDeviceNameFromService) { if (currentDeviceNameFromService) {
const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService) const found = DisplayService.devices.find(dev => dev.name === currentDeviceNameFromService);
if (found) { if (found)
return found.name return found.name;
}
} }
const backlight = DisplayService.devices.find(d => d.class === "backlight") const backlight = DisplayService.devices.find(d => d.class === "backlight");
if (backlight) { if (backlight)
return backlight.name return backlight.name;
}
const ddc = DisplayService.devices.find(d => d.class === "ddc") const ddc = DisplayService.devices.find(d => d.class === "ddc");
if (ddc) { if (ddc)
return ddc.name return ddc.name;
}
return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "" return DisplayService.devices.length > 0 ? DisplayService.devices[0].name : "";
} }
Component.onCompleted: { Component.onCompleted: {
currentDeviceName = resolveDeviceName() currentDeviceName = resolveDeviceName();
} }
property bool isPinnedToScreen: { property bool isPinnedToScreen: {
if (!screenName || screenName.length === 0) { const pinKey = getScreenPinKey();
return false if (!pinKey || pinKey.length === 0)
} return false;
const pins = SettingsData.brightnessDevicePins || {} const pins = SettingsData.brightnessDevicePins || {};
return pins[screenName] === currentDeviceName return pins[pinKey] === currentDeviceName;
} }
function togglePinToScreen() { function togglePinToScreen() {
if (!screenName || screenName.length === 0 || !currentDeviceName || currentDeviceName.length === 0) { const pinKey = getScreenPinKey();
return if (!pinKey || pinKey.length === 0 || !currentDeviceName || currentDeviceName.length === 0)
} return;
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}))
if (isPinnedToScreen) { if (isPinnedToScreen) {
delete pins[screenName] delete pins[pinKey];
} else { } else {
pins[screenName] = currentDeviceName pins[pinKey] = currentDeviceName;
} }
SettingsData.set("brightnessDevicePins", pins) SettingsData.set("brightnessDevicePins", pins);
} }
implicitHeight: { implicitHeight: {
if (height > 0) { if (height > 0) {
return height return height;
} }
return brightnessContent.height + Theme.spacingM return brightnessContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -165,7 +166,7 @@ Rectangle {
} }
StyledText { StyledText {
text: screenName || "Unknown Monitor" text: root.getScreenPinKey() || "Unknown Monitor"
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -216,8 +217,8 @@ Rectangle {
required property int index required property int index
property real deviceBrightness: { property real deviceBrightness: {
DisplayService.brightnessVersion DisplayService.brightnessVersion;
return DisplayService.getDeviceBrightness(modelData.name) return DisplayService.getDeviceBrightness(modelData.name);
} }
width: parent.width width: parent.width
@@ -248,19 +249,19 @@ Rectangle {
DankIcon { DankIcon {
name: { name: {
const deviceClass = modelData.class || "" const deviceClass = modelData.class || "";
const deviceName = modelData.name || "" const deviceName = modelData.name || "";
if (deviceClass === "backlight" || deviceClass === "ddc") { if (deviceClass === "backlight" || deviceClass === "ddc") {
if (deviceBrightness <= 33) if (deviceBrightness <= 33)
return "brightness_low" return "brightness_low";
if (deviceBrightness <= 66) if (deviceBrightness <= 66)
return "brightness_medium" return "brightness_medium";
return "brightness_high" return "brightness_high";
} else if (deviceName.includes("kbd")) { } else if (deviceName.includes("kbd")) {
return "keyboard" return "keyboard";
} else { } else {
return "lightbulb" return "lightbulb";
} }
} }
size: Theme.iconSize size: Theme.iconSize
@@ -283,12 +284,12 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const name = modelData.name || "" const name = modelData.name || "";
const deviceClass = modelData.class || "" const deviceClass = modelData.class || "";
if (deviceClass === "backlight") { if (deviceClass === "backlight") {
return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase()) return name.replace("_", " ").replace(/\b\w/g, c => c.toUpperCase());
} }
return name return name;
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
@@ -307,14 +308,14 @@ Rectangle {
StyledText { StyledText {
text: { text: {
const deviceClass = modelData.class || "" const deviceClass = modelData.class || "";
if (deviceClass === "backlight") if (deviceClass === "backlight")
return "Backlight device" return "Backlight device";
if (deviceClass === "ddc") if (deviceClass === "ddc")
return "DDC/CI monitor" return "DDC/CI monitor";
if (deviceClass === "leds") if (deviceClass === "leds")
return "LED device" return "LED device";
return deviceClass return deviceClass;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText color: Theme.surfaceVariantText
@@ -353,9 +354,9 @@ Rectangle {
cornerRadius: parent.radius cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0 enabled: SessionData.getBrightnessExponent(modelData.name) > 1.0
onClicked: { onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name) const current = SessionData.getBrightnessExponent(modelData.name);
const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10) const newValue = Math.max(1.0, Math.round((current - 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue) SessionData.setBrightnessExponent(modelData.name, newValue);
} }
} }
} }
@@ -395,9 +396,9 @@ Rectangle {
cornerRadius: parent.radius cornerRadius: parent.radius
enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5 enabled: SessionData.getBrightnessExponent(modelData.name) < 2.5
onClicked: { onClicked: {
const current = SessionData.getBrightnessExponent(modelData.name) const current = SessionData.getBrightnessExponent(modelData.name);
const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10) const newValue = Math.min(2.5, Math.round((current + 0.1) * 10) / 10);
SessionData.setBrightnessExponent(modelData.name, newValue) SessionData.setBrightnessExponent(modelData.name, newValue);
} }
} }
} }
@@ -433,8 +434,8 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const currentState = SessionData.getBrightnessExponential(modelData.name) const currentState = SessionData.getBrightnessExponential(modelData.name);
SessionData.setBrightnessExponential(modelData.name, !currentState) SessionData.setBrightnessExponential(modelData.name, !currentState);
} }
} }
} }
@@ -447,15 +448,16 @@ Rectangle {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
if (screenName && screenName.length > 0 && modelData.name !== currentDeviceName) { const pinKey = root.getScreenPinKey();
const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {})) if (pinKey.length > 0 && modelData.name !== currentDeviceName) {
if (pins[screenName]) { const pins = JSON.parse(JSON.stringify(SettingsData.brightnessDevicePins || {}));
delete pins[screenName] if (pins[pinKey]) {
SettingsData.set("brightnessDevicePins", pins) delete pins[pinKey];
SettingsData.set("brightnessDevicePins", pins);
} }
} }
currentDeviceName = modelData.name currentDeviceName = modelData.name;
deviceNameChanged(modelData.name) deviceNameChanged(modelData.name);
} }
} }
} }

View File

@@ -11,15 +11,15 @@ Rectangle {
implicitHeight: { implicitHeight: {
if (height > 0) { if (height > 0) {
return height return height;
} }
if (NetworkService.wifiToggling) { if (NetworkService.wifiToggling) {
return headerRow.height + wifiToggleContent.height + Theme.spacingM return headerRow.height + wifiToggleContent.height + Theme.spacingM;
} }
if (NetworkService.wifiEnabled) { if (NetworkService.wifiEnabled) {
return headerRow.height + wifiContent.height + Theme.spacingM return headerRow.height + wifiContent.height + Theme.spacingM;
} }
return headerRow.height + wifiOffContent.height + Theme.spacingM return headerRow.height + wifiOffContent.height + Theme.spacingM;
} }
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
@@ -27,35 +27,35 @@ Rectangle {
border.width: 0 border.width: 0
Component.onCompleted: { Component.onCompleted: {
NetworkService.addRef() NetworkService.addRef();
} }
Component.onDestruction: { Component.onDestruction: {
NetworkService.removeRef() NetworkService.removeRef();
} }
property int currentPreferenceIndex: { property int currentPreferenceIndex: {
if (DMSService.apiVersion < 5) { if (DMSService.apiVersion < 5) {
return 1 return 1;
} }
if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10) { if (NetworkService.backend !== "networkmanager" || DMSService.apiVersion <= 10) {
return 1 return 1;
} }
const pref = NetworkService.userPreference const pref = NetworkService.userPreference;
const status = NetworkService.networkStatus const status = NetworkService.networkStatus;
let index = 1 let index = 1;
if (pref === "ethernet") { if (pref === "ethernet") {
index = 0 index = 0;
} else if (pref === "wifi") { } else if (pref === "wifi") {
index = 1 index = 1;
} else { } else {
index = status === "ethernet" ? 0 : 1 index = status === "ethernet" ? 0 : 1;
} }
return index return index;
} }
Row { Row {
@@ -78,28 +78,56 @@ Rectangle {
} }
Item { Item {
width: Math.max(0, parent.width - headerText.implicitWidth - preferenceControls.width - Theme.spacingM) height: 1
height: parent.height width: parent.width - headerText.width - rightControls.width
} }
DankButtonGroup { Row {
id: preferenceControls id: rightControls
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10 spacing: Theme.spacingS
model: ["Ethernet", "WiFi"] DankDropdown {
currentIndex: currentPreferenceIndex id: wifiDeviceDropdown
selectionMode: "single" anchors.verticalCenter: parent.verticalCenter
onSelectionChanged: (index, selected) => { visible: currentPreferenceIndex === 1 && (NetworkService.wifiDevices?.length ?? 0) > 1
if (!selected) return compactMode: true
console.log("NetworkDetail: Setting preference to", index === 0 ? "ethernet" : "wifi") dropdownWidth: 120
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi") popupWidth: 160
alignPopupRight: true
options: {
const devices = NetworkService.wifiDevices;
if (!devices || devices.length === 0)
return [I18n.tr("Auto")];
return [I18n.tr("Auto")].concat(devices.map(d => d.name));
}
currentValue: NetworkService.wifiDeviceOverride || I18n.tr("Auto")
onValueChanged: value => {
const deviceName = value === I18n.tr("Auto") ? "" : value;
NetworkService.setWifiDeviceOverride(deviceName);
}
}
DankButtonGroup {
id: preferenceControls
anchors.verticalCenter: parent.verticalCenter
visible: NetworkService.backend === "networkmanager" && DMSService.apiVersion > 10
model: ["Ethernet", "WiFi"]
currentIndex: currentPreferenceIndex
selectionMode: "single"
onSelectionChanged: (index, selected) => {
if (!selected)
return;
NetworkService.setNetworkPreference(index === 0 ? "ethernet" : "wifi");
}
} }
} }
} }
Item { Item {
id: wifiToggleContent id: wifiToggleContent
anchors.top: headerRow.bottom anchors.top: headerRow.bottom
@@ -194,7 +222,6 @@ Rectangle {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: NetworkService.toggleWifiRadio() onClicked: NetworkService.toggleWifiRadio()
} }
} }
} }
} }
@@ -219,15 +246,17 @@ Rectangle {
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: { values: {
const currentUuid = NetworkService.ethernetConnectionUuid const currentUuid = NetworkService.ethernetConnectionUuid;
const networks = NetworkService.wiredConnections const networks = NetworkService.wiredConnections;
let sorted = [...networks] let sorted = [...networks];
sorted.sort((a, b) => { sorted.sort((a, b) => {
if (a.isActive && !b.isActive) return -1 if (a.isActive && !b.isActive)
if (!a.isActive && b.isActive) return 1 return -1;
return a.id.localeCompare(b.id) if (!a.isActive && b.isActive)
}) return 1;
return sorted return a.id.localeCompare(b.id);
});
return sorted;
} }
} }
@@ -279,12 +308,12 @@ Rectangle {
buttonSize: 28 buttonSize: 28
onClicked: { onClicked: {
if (wiredNetworkContextMenu.visible) { if (wiredNetworkContextMenu.visible) {
wiredNetworkContextMenu.close() wiredNetworkContextMenu.close();
} else { } else {
wiredNetworkContextMenu.currentID = modelData.id wiredNetworkContextMenu.currentID = modelData.id;
wiredNetworkContextMenu.currentUUID = modelData.uuid wiredNetworkContextMenu.currentUUID = modelData.uuid;
wiredNetworkContextMenu.currentConnected = modelData.isActive wiredNetworkContextMenu.currentConnected = modelData.isActive;
wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS) wiredNetworkContextMenu.popup(wiredOptionsButton, -wiredNetworkContextMenu.width + wiredOptionsButton.width, wiredOptionsButton.height + Theme.spacingXS);
} }
} }
} }
@@ -295,14 +324,13 @@ Rectangle {
anchors.rightMargin: wiredOptionsButton.width + Theme.spacingS anchors.rightMargin: wiredOptionsButton.width + Theme.spacingS
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: function(event) { onClicked: function (event) {
if (modelData.uuid !== NetworkService.ethernetConnectionUuid) { if (modelData.uuid !== NetworkService.ethernetConnectionUuid) {
NetworkService.connectToSpecificWiredConfig(modelData.uuid) NetworkService.connectToSpecificWiredConfig(modelData.uuid);
} }
event.accepted = true event.accepted = true;
} }
} }
} }
} }
} }
@@ -343,7 +371,7 @@ Rectangle {
onTriggered: { onTriggered: {
if (!networkContextMenu.currentConnected) { if (!networkContextMenu.currentConnected) {
NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID) NetworkService.connectToSpecificWiredConfig(wiredNetworkContextMenu.currentUUID);
} }
} }
} }
@@ -366,8 +394,8 @@ Rectangle {
} }
onTriggered: { onTriggered: {
let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID) let networkData = NetworkService.getWiredNetworkInfo(wiredNetworkContextMenu.currentUUID);
networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData) networkWiredInfoModal.showNetworkInfo(wiredNetworkContextMenu.currentID, networkData);
} }
} }
} }
@@ -416,26 +444,30 @@ Rectangle {
Repeater { Repeater {
model: ScriptModel { model: ScriptModel {
values: { values: {
const ssid = NetworkService.currentWifiSSID const ssid = NetworkService.currentWifiSSID;
const networks = NetworkService.wifiNetworks const networks = NetworkService.wifiNetworks;
const pins = SettingsData.wifiNetworkPins || {} const pins = SettingsData.wifiNetworkPins || {};
const pinnedSSID = pins["preferredWifi"] const pinnedSSID = pins["preferredWifi"];
let sorted = [...networks] let sorted = [...networks];
sorted.sort((a, b) => { sorted.sort((a, b) => {
// Pinned network first // Pinned network first
if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID) return -1 if (a.ssid === pinnedSSID && b.ssid !== pinnedSSID)
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID) return 1 return -1;
if (b.ssid === pinnedSSID && a.ssid !== pinnedSSID)
return 1;
// Then currently connected // Then currently connected
if (a.ssid === ssid) return -1 if (a.ssid === ssid)
if (b.ssid === ssid) return 1 return -1;
if (b.ssid === ssid)
return 1;
// Then by signal strength // Then by signal strength
return b.signal - a.signal return b.signal - a.signal;
}) });
if (!wifiContent.menuOpen) { if (!wifiContent.menuOpen) {
wifiContent.frozenNetworks = sorted wifiContent.frozenNetworks = sorted;
} }
return wifiContent.menuOpen ? wifiContent.frozenNetworks : sorted return wifiContent.menuOpen ? wifiContent.frozenNetworks : sorted;
} }
} }
@@ -458,10 +490,12 @@ Rectangle {
DankIcon { DankIcon {
name: { name: {
let strength = modelData.signal || 0 let strength = modelData.signal || 0;
if (strength >= 50) return "wifi" if (strength >= 50)
if (strength >= 25) return "wifi_2_bar" return "wifi";
return "wifi_1_bar" if (strength >= 25)
return "wifi_2_bar";
return "wifi_1_bar";
} }
size: Theme.iconSize - 4 size: Theme.iconSize - 4
color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText color: modelData.ssid === NetworkService.currentWifiSSID ? Theme.primary : Theme.surfaceText
@@ -515,16 +549,16 @@ Rectangle {
buttonSize: 28 buttonSize: 28
onClicked: { onClicked: {
if (networkContextMenu.visible) { if (networkContextMenu.visible) {
networkContextMenu.close() networkContextMenu.close();
} else { } else {
wifiContent.menuOpen = true wifiContent.menuOpen = true;
networkContextMenu.currentSSID = modelData.ssid networkContextMenu.currentSSID = modelData.ssid;
networkContextMenu.currentSecured = modelData.secured networkContextMenu.currentSecured = modelData.secured;
networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID networkContextMenu.currentConnected = modelData.ssid === NetworkService.currentWifiSSID;
networkContextMenu.currentSaved = modelData.saved networkContextMenu.currentSaved = modelData.saved;
networkContextMenu.currentSignal = modelData.signal networkContextMenu.currentSignal = modelData.signal;
networkContextMenu.currentAutoconnect = modelData.autoconnect || false networkContextMenu.currentAutoconnect = modelData.autoconnect || false;
networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS) networkContextMenu.popup(optionsButton, -networkContextMenu.width + optionsButton.width, optionsButton.height + Theme.spacingXS);
} }
} }
} }
@@ -537,8 +571,8 @@ Rectangle {
height: 28 height: 28
radius: height / 2 radius: height / 2
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05) return isThisNetworkPinned ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceText, 0.05);
} }
Row { Row {
@@ -550,21 +584,21 @@ Rectangle {
name: "push_pin" name: "push_pin"
size: 16 size: 16
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
StyledText { StyledText {
text: { text: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
return isThisNetworkPinned ? "Pinned" : "Pin" return isThisNetworkPinned ? "Pinned" : "Pin";
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: { color: {
const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid const isThisNetworkPinned = (SettingsData.wifiNetworkPins || {})["preferredWifi"] === modelData.ssid;
return isThisNetworkPinned ? Theme.primary : Theme.surfaceText return isThisNetworkPinned ? Theme.primary : Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -574,16 +608,16 @@ Rectangle {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {})) const pins = JSON.parse(JSON.stringify(SettingsData.wifiNetworkPins || {}));
const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid const isCurrentlyPinned = pins["preferredWifi"] === modelData.ssid;
if (isCurrentlyPinned) { if (isCurrentlyPinned) {
delete pins["preferredWifi"] delete pins["preferredWifi"];
} else { } else {
pins["preferredWifi"] = modelData.ssid pins["preferredWifi"] = modelData.ssid;
} }
SettingsData.set("wifiNetworkPins", pins) SettingsData.set("wifiNetworkPins", pins);
} }
} }
} }
@@ -594,22 +628,21 @@ Rectangle {
anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4 anchors.rightMargin: optionsButton.width + Theme.spacingM + Theme.spacingS + pinWifiRow.width + Theme.spacingS * 4
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: function(event) { onClicked: function (event) {
if (modelData.ssid !== NetworkService.currentWifiSSID) { if (modelData.ssid !== NetworkService.currentWifiSSID) {
if (modelData.secured && !modelData.saved) { if (modelData.secured && !modelData.saved) {
if (DMSService.apiVersion >= 7) { if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(modelData.ssid) NetworkService.connectToWifi(modelData.ssid);
} else if (PopoutService.wifiPasswordModal) { } else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(modelData.ssid) PopoutService.wifiPasswordModal.show(modelData.ssid);
} }
} else { } else {
NetworkService.connectToWifi(modelData.ssid) NetworkService.connectToWifi(modelData.ssid);
} }
} }
event.accepted = true event.accepted = true;
} }
} }
} }
} }
} }
@@ -628,7 +661,7 @@ Rectangle {
property bool currentAutoconnect: false property bool currentAutoconnect: false
onClosed: { onClosed: {
wifiContent.menuOpen = false wifiContent.menuOpen = false;
} }
background: Rectangle { background: Rectangle {
@@ -657,16 +690,16 @@ Rectangle {
onTriggered: { onTriggered: {
if (networkContextMenu.currentConnected) { if (networkContextMenu.currentConnected) {
NetworkService.disconnectWifi() NetworkService.disconnectWifi();
} else { } else {
if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) { if (networkContextMenu.currentSecured && !networkContextMenu.currentSaved) {
if (DMSService.apiVersion >= 7) { if (DMSService.apiVersion >= 7) {
NetworkService.connectToWifi(networkContextMenu.currentSSID) NetworkService.connectToWifi(networkContextMenu.currentSSID);
} else if (PopoutService.wifiPasswordModal) { } else if (PopoutService.wifiPasswordModal) {
PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID) PopoutService.wifiPasswordModal.show(networkContextMenu.currentSSID);
} }
} else { } else {
NetworkService.connectToWifi(networkContextMenu.currentSSID) NetworkService.connectToWifi(networkContextMenu.currentSSID);
} }
} }
} }
@@ -690,8 +723,8 @@ Rectangle {
} }
onTriggered: { onTriggered: {
let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID) let networkData = NetworkService.getNetworkInfo(networkContextMenu.currentSSID);
networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData) networkInfoModal.showNetworkInfo(networkContextMenu.currentSSID, networkData);
} }
} }
@@ -714,7 +747,7 @@ Rectangle {
} }
onTriggered: { onTriggered: {
NetworkService.setWifiAutoconnect(networkContextMenu.currentSSID, !networkContextMenu.currentAutoconnect) NetworkService.setWifiAutoconnect(networkContextMenu.currentSSID, !networkContextMenu.currentAutoconnect);
} }
} }
@@ -737,7 +770,7 @@ Rectangle {
} }
onTriggered: { onTriggered: {
NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID) NetworkService.forgetWifiNetwork(networkContextMenu.currentSSID);
} }
} }
} }

View File

@@ -17,17 +17,17 @@ QtObject {
} }
onItemChanged: { onItemChanged: {
root.vpnBuiltinInstance = item root.vpnBuiltinInstance = item;
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onControlCenterWidgetsChanged() { function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn") const hasVpnWidget = widgets.some(w => w.id === "builtin_vpn");
if (!hasVpnWidget && vpnLoader.active) { if (!hasVpnWidget && vpnLoader.active) {
console.log("VpnWidget: No VPN widget in control center, deactivating loader") console.log("VpnWidget: No VPN widget in control center, deactivating loader");
vpnLoader.active = false vpnLoader.active = false;
} }
} }
} }
@@ -40,35 +40,36 @@ QtObject {
} }
onItemChanged: { onItemChanged: {
root.cupsBuiltinInstance = item root.cupsBuiltinInstance = item;
if (item && !DMSService.activeSubscriptions.includes("cups") && !DMSService.activeSubscriptions.includes("all")) { if (item && !DMSService.activeSubscriptions.includes("cups") && !DMSService.activeSubscriptions.includes("all")) {
DMSService.addSubscription("cups") DMSService.addSubscription("cups");
} }
} }
onActiveChanged: { onActiveChanged: {
if (!active) { if (!active) {
if (DMSService.activeSubscriptions.includes("cups")) { if (DMSService.activeSubscriptions.includes("cups")) {
DMSService.removeSubscription("cups") DMSService.removeSubscription("cups");
} }
root.cupsBuiltinInstance = null root.cupsBuiltinInstance = null;
} }
} }
Connections { Connections {
target: SettingsData target: SettingsData
function onControlCenterWidgetsChanged() { function onControlCenterWidgetsChanged() {
const widgets = SettingsData.controlCenterWidgets || [] const widgets = SettingsData.controlCenterWidgets || [];
const hasCupsWidget = widgets.some(w => w.id === "builtin_cups") const hasCupsWidget = widgets.some(w => w.id === "builtin_cups");
if (!hasCupsWidget && cupsLoader.active) { if (!hasCupsWidget && cupsLoader.active) {
console.log("CupsWidget: No CUPS widget in control center, deactivating loader") console.log("CupsWidget: No CUPS widget in control center, deactivating loader");
cupsLoader.active = false cupsLoader.active = false;
} }
} }
} }
} }
readonly property var coreWidgetDefinitions: [{ readonly property var coreWidgetDefinitions: [
{
"id": "nightMode", "id": "nightMode",
"text": "Night Mode", "text": "Night Mode",
"description": "Blue light filter", "description": "Blue light filter",
@@ -76,28 +77,32 @@ QtObject {
"type": "toggle", "type": "toggle",
"enabled": DisplayService.automationAvailable, "enabled": DisplayService.automationAvailable,
"warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined "warning": !DisplayService.automationAvailable ? "Requires night mode support" : undefined
}, { },
{
"id": "darkMode", "id": "darkMode",
"text": "Dark Mode", "text": "Dark Mode",
"description": "System theme toggle", "description": "System theme toggle",
"icon": "contrast", "icon": "contrast",
"type": "toggle", "type": "toggle",
"enabled": true "enabled": true
}, { },
{
"id": "doNotDisturb", "id": "doNotDisturb",
"text": "Do Not Disturb", "text": "Do Not Disturb",
"description": "Block notifications", "description": "Block notifications",
"icon": "do_not_disturb_on", "icon": "do_not_disturb_on",
"type": "toggle", "type": "toggle",
"enabled": true "enabled": true
}, { },
{
"id": "idleInhibitor", "id": "idleInhibitor",
"text": "Keep Awake", "text": "Keep Awake",
"description": "Prevent screen timeout", "description": "Prevent screen timeout",
"icon": "motion_sensor_active", "icon": "motion_sensor_active",
"type": "toggle", "type": "toggle",
"enabled": true "enabled": true
}, { },
{
"id": "wifi", "id": "wifi",
"text": "Network", "text": "Network",
"description": "Wi-Fi and Ethernet connection", "description": "Wi-Fi and Ethernet connection",
@@ -105,7 +110,8 @@ QtObject {
"type": "connection", "type": "connection",
"enabled": NetworkService.wifiAvailable, "enabled": NetworkService.wifiAvailable,
"warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined "warning": !NetworkService.wifiAvailable ? "Wi-Fi not available" : undefined
}, { },
{
"id": "bluetooth", "id": "bluetooth",
"text": "Bluetooth", "text": "Bluetooth",
"description": "Device connections", "description": "Device connections",
@@ -113,28 +119,32 @@ QtObject {
"type": "connection", "type": "connection",
"enabled": BluetoothService.available, "enabled": BluetoothService.available,
"warning": !BluetoothService.available ? "Bluetooth not available" : undefined "warning": !BluetoothService.available ? "Bluetooth not available" : undefined
}, { },
{
"id": "audioOutput", "id": "audioOutput",
"text": "Audio Output", "text": "Audio Output",
"description": "Speaker settings", "description": "Speaker settings",
"icon": "volume_up", "icon": "volume_up",
"type": "connection", "type": "connection",
"enabled": true "enabled": true
}, { },
{
"id": "audioInput", "id": "audioInput",
"text": "Audio Input", "text": "Audio Input",
"description": "Microphone settings", "description": "Microphone settings",
"icon": "mic", "icon": "mic",
"type": "connection", "type": "connection",
"enabled": true "enabled": true
}, { },
{
"id": "volumeSlider", "id": "volumeSlider",
"text": "Volume Slider", "text": "Volume Slider",
"description": "Audio volume control", "description": "Audio volume control",
"icon": "volume_up", "icon": "volume_up",
"type": "slider", "type": "slider",
"enabled": true "enabled": true
}, { },
{
"id": "brightnessSlider", "id": "brightnessSlider",
"text": "Brightness Slider", "text": "Brightness Slider",
"description": "Display brightness control", "description": "Display brightness control",
@@ -143,21 +153,24 @@ QtObject {
"enabled": DisplayService.brightnessAvailable, "enabled": DisplayService.brightnessAvailable,
"warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined, "warning": !DisplayService.brightnessAvailable ? "Brightness control not available" : undefined,
"allowMultiple": true "allowMultiple": true
}, { },
{
"id": "inputVolumeSlider", "id": "inputVolumeSlider",
"text": "Input Volume Slider", "text": "Input Volume Slider",
"description": "Microphone volume control", "description": "Microphone volume control",
"icon": "mic", "icon": "mic",
"type": "slider", "type": "slider",
"enabled": true "enabled": true
}, { },
{
"id": "battery", "id": "battery",
"text": "Battery", "text": "Battery",
"description": "Battery and power management", "description": "Battery and power management",
"icon": "battery_std", "icon": "battery_std",
"type": "action", "type": "action",
"enabled": true "enabled": true
}, { },
{
"id": "diskUsage", "id": "diskUsage",
"text": "Disk Usage", "text": "Disk Usage",
"description": "Filesystem usage monitoring", "description": "Filesystem usage monitoring",
@@ -166,14 +179,16 @@ QtObject {
"enabled": DgopService.dgopAvailable, "enabled": DgopService.dgopAvailable,
"warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined, "warning": !DgopService.dgopAvailable ? "Requires 'dgop' tool" : undefined,
"allowMultiple": true "allowMultiple": true
}, { },
{
"id": "colorPicker", "id": "colorPicker",
"text": "Color Picker", "text": "Color Picker",
"description": "Choose colors from palette", "description": "Choose colors from palette",
"icon": "palette", "icon": "palette",
"type": "action", "type": "action",
"enabled": true "enabled": true
}, { },
{
"id": "builtin_vpn", "id": "builtin_vpn",
"text": "VPN", "text": "VPN",
"description": "VPN connections", "description": "VPN connections",
@@ -182,7 +197,8 @@ QtObject {
"enabled": DMSNetworkService.available, "enabled": DMSNetworkService.available,
"warning": !DMSNetworkService.available ? "VPN not available" : undefined, "warning": !DMSNetworkService.available ? "VPN not available" : undefined,
"isBuiltinPlugin": true "isBuiltinPlugin": true
}, { },
{
"id": "builtin_cups", "id": "builtin_cups",
"text": "Printers", "text": "Printers",
"description": "Print Server Management", "description": "Print Server Management",
@@ -191,78 +207,79 @@ QtObject {
"enabled": CupsService.available, "enabled": CupsService.available,
"warning": !CupsService.available ? "CUPS not available" : undefined, "warning": !CupsService.available ? "CUPS not available" : undefined,
"isBuiltinPlugin": true "isBuiltinPlugin": true
}] }
]
function getPluginWidgets() { function getPluginWidgets() {
const plugins = [] const plugins = [];
const loadedPlugins = PluginService.getLoadedPlugins() const loadedPlugins = PluginService.getLoadedPlugins();
for (var i = 0; i < loadedPlugins.length; i++) { for (var i = 0; i < loadedPlugins.length; i++) {
const plugin = loadedPlugins[i] const plugin = loadedPlugins[i];
if (plugin.type === "daemon") { if (plugin.type === "daemon") {
continue continue;
} }
const pluginComponent = PluginService.pluginWidgetComponents[plugin.id] const pluginComponent = PluginService.pluginWidgetComponents[plugin.id];
if (!pluginComponent || typeof pluginComponent.createObject !== 'function') { if (!pluginComponent || typeof pluginComponent.createObject !== 'function') {
continue continue;
} }
const tempInstance = pluginComponent.createObject(null) const tempInstance = pluginComponent.createObject(null);
if (!tempInstance) { if (!tempInstance) {
continue continue;
} }
const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0 const hasCCWidget = tempInstance.ccWidgetIcon && tempInstance.ccWidgetIcon.length > 0;
tempInstance.destroy() tempInstance.destroy();
if (!hasCCWidget) { if (!hasCCWidget) {
continue continue;
} }
plugins.push({ plugins.push({
"id": "plugin_" + plugin.id, "id": "plugin_" + plugin.id,
"pluginId": plugin.id, "pluginId": plugin.id,
"text": plugin.name || "Plugin", "text": plugin.name || "Plugin",
"description": plugin.description || "", "description": plugin.description || "",
"icon": plugin.icon || "extension", "icon": plugin.icon || "extension",
"type": "plugin", "type": "plugin",
"enabled": true, "enabled": true,
"isPlugin": true "isPlugin": true
}) });
} }
return plugins return plugins;
} }
readonly property var baseWidgetDefinitions: coreWidgetDefinitions readonly property var baseWidgetDefinitions: coreWidgetDefinitions
function getWidgetForId(widgetId) { function getWidgetForId(widgetId) {
return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId) return WidgetUtils.getWidgetForId(baseWidgetDefinitions, widgetId);
} }
function addWidget(widgetId) { function addWidget(widgetId) {
WidgetUtils.addWidget(widgetId) WidgetUtils.addWidget(widgetId);
} }
function removeWidget(index) { function removeWidget(index) {
WidgetUtils.removeWidget(index) WidgetUtils.removeWidget(index);
} }
function toggleWidgetSize(index) { function toggleWidgetSize(index) {
WidgetUtils.toggleWidgetSize(index) WidgetUtils.toggleWidgetSize(index);
} }
function moveWidget(fromIndex, toIndex) { function moveWidget(fromIndex, toIndex) {
WidgetUtils.moveWidget(fromIndex, toIndex) WidgetUtils.moveWidget(fromIndex, toIndex);
} }
function resetToDefault() { function resetToDefault() {
WidgetUtils.resetToDefault() WidgetUtils.resetToDefault();
} }
function clearAll() { function clearAll() {
WidgetUtils.clearAll() WidgetUtils.clearAll();
} }
} }

View File

@@ -1144,6 +1144,8 @@ Item {
return controlCenterLoader.item; return controlCenterLoader.item;
} }
parentScreen: barWindow.screen parentScreen: barWindow.screen
screenName: barWindow.screen?.name || ""
screenModel: barWindow.screen?.model || ""
widgetData: parent.widgetData widgetData: parent.widgetData
Component.onCompleted: { Component.onCompleted: {

View File

@@ -155,35 +155,56 @@ Loader {
} }
onLoaded: { onLoaded: {
if (item) { if (!item)
contentItemReady(item); return;
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical;
} catch (e) {}
}
if (item.pluginService !== undefined) { contentItemReady(item);
var parts = widgetId.split(":");
var pluginId = parts[0];
var variantId = parts.length > 1 ? parts[1] : null;
if (item.pluginId !== undefined) { if (axis && "isVertical" in item) {
item.pluginId = pluginId; try {
} item.isVertical = axis.isVertical;
if (item.variantId !== undefined) { } catch (e) {}
item.variantId = variantId;
}
if (item.variantData !== undefined && variantId) {
item.variantData = PluginService.getPluginVariantData(pluginId, variantId);
}
item.pluginService = PluginService;
}
if (item.popoutService !== undefined) {
item.popoutService = PopoutService;
}
} }
if (item.pluginService !== undefined) {
var parts = widgetId.split(":");
var pluginId = parts[0];
var variantId = parts.length > 1 ? parts[1] : null;
if (item.pluginId !== undefined)
item.pluginId = pluginId;
if (item.variantId !== undefined)
item.variantId = variantId;
if (item.variantData !== undefined && variantId)
item.variantData = PluginService.getPluginVariantData(pluginId, variantId);
item.pluginService = PluginService;
}
if (item.popoutService !== undefined)
item.popoutService = PopoutService;
registerWidgetIfEligible();
}
Component.onDestruction: {
unregisterWidget();
}
function registerWidgetIfEligible() {
if (!item || !widgetId || !parentScreen?.name)
return;
const hasPopout = item.popoutTarget !== undefined || typeof item.triggerPopout === "function" || typeof item.clicked === "function";
if (!hasPopout)
return;
BarWidgetService.registerWidget(widgetId, parentScreen.name, item);
}
function unregisterWidget() {
if (!widgetId || !parentScreen?.name)
return;
BarWidgetService.unregisterWidget(widgetId, parentScreen.name);
} }
function getWidgetComponent(widgetId, components) { function getWidgetComponent(widgetId, components) {

View File

@@ -10,9 +10,138 @@ BasePill {
property bool isActive: false property bool isActive: false
property var popoutTarget: null property var popoutTarget: null
property var widgetData: null property var widgetData: null
property string screenName: ""
property string screenModel: ""
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon property bool showAudioIcon: SettingsData.controlCenterShowAudioIcon
property bool showVpnIcon: SettingsData.controlCenterShowVpnIcon
property bool showBrightnessIcon: SettingsData.controlCenterShowBrightnessIcon
property bool showMicIcon: SettingsData.controlCenterShowMicIcon
property bool showBatteryIcon: SettingsData.controlCenterShowBatteryIcon
property bool showPrinterIcon: SettingsData.controlCenterShowPrinterIcon
function getNetworkIconName() {
if (NetworkService.wifiToggling)
return "sync";
switch (NetworkService.networkStatus) {
case "ethernet":
return "lan";
case "vpn":
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon;
default:
return NetworkService.wifiSignalIcon;
}
}
function getNetworkIconColor() {
if (NetworkService.wifiToggling)
return Theme.primary;
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton;
}
function getVolumeIconName() {
if (!AudioService.sink?.audio)
return "volume_up";
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0)
return "volume_off";
if (AudioService.sink.audio.volume * 100 < 33)
return "volume_down";
return "volume_up";
}
function getMicIconName() {
if (!AudioService.source?.audio)
return "mic";
if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0)
return "mic_off";
return "mic";
}
function getMicIconColor() {
if (!AudioService.source?.audio)
return Theme.outlineButton;
if (AudioService.source.audio.muted || AudioService.source.audio.volume === 0)
return Theme.outlineButton;
return Theme.widgetIconColor;
}
function getBrightnessIconName() {
const deviceName = getPinnedBrightnessDevice();
if (!deviceName)
return "brightness_medium";
const level = DisplayService.getDeviceBrightness(deviceName);
if (level <= 33)
return "brightness_low";
if (level <= 66)
return "brightness_medium";
return "brightness_high";
}
function getScreenPinKey() {
if (SettingsData.displayNameMode === "model" && root.screenModel && root.screenModel.length > 0) {
return root.screenModel;
}
return root.screenName || "";
}
function getPinnedBrightnessDevice() {
const pinKey = getScreenPinKey();
if (!pinKey)
return "";
const pins = SettingsData.brightnessDevicePins || {};
return pins[pinKey] || "";
}
function hasPinnedBrightnessDevice() {
return getPinnedBrightnessDevice().length > 0;
}
function handleVolumeWheel(delta) {
if (!AudioService.sink?.audio)
return;
const currentVolume = AudioService.sink.audio.volume * 100;
const newVolume = delta > 0 ? Math.min(100, currentVolume + 5) : Math.max(0, currentVolume - 5);
AudioService.sink.audio.muted = false;
AudioService.sink.audio.volume = newVolume / 100;
AudioService.playVolumeChangeSoundIfEnabled();
}
function handleMicWheel(delta) {
if (!AudioService.source?.audio)
return;
const currentVolume = AudioService.source.audio.volume * 100;
const newVolume = delta > 0 ? Math.min(100, currentVolume + 5) : Math.max(0, currentVolume - 5);
AudioService.source.audio.muted = false;
AudioService.source.audio.volume = newVolume / 100;
}
function handleBrightnessWheel(delta) {
const deviceName = getPinnedBrightnessDevice();
if (!deviceName)
return;
const currentBrightness = DisplayService.getDeviceBrightness(deviceName);
const newBrightness = delta > 0 ? Math.min(100, currentBrightness + 5) : Math.max(1, currentBrightness - 5);
DisplayService.setBrightness(newBrightness, deviceName, false);
}
function getBatteryIconColor() {
if (!BatteryService.batteryAvailable)
return Theme.widgetIconColor;
if (BatteryService.isLowBattery && !BatteryService.isCharging)
return Theme.error;
if (BatteryService.isCharging || BatteryService.isPluggedIn)
return Theme.primary;
return Theme.widgetIconColor;
}
function hasPrintJobs() {
return CupsService.getTotalJobsNum() > 0;
}
function hasNoVisibleIcons() {
return !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon && !root.showVpnIcon && !root.showBrightnessIcon && !root.showMicIcon && !root.showBatteryIcon && !root.showPrinterIcon;
}
content: Component { content: Component {
Item { Item {
@@ -26,34 +155,21 @@ BasePill {
spacing: Theme.spacingXS spacing: Theme.spacingXS
DankIcon { DankIcon {
name: { name: root.getNetworkIconName()
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: { color: root.getNetworkIconColor()
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable visible: root.showNetworkIcon && NetworkService.networkAvailable
} }
DankIcon {
name: "vpn_lock"
size: Theme.barIconSize(root.barThickness)
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
}
DankIcon { DankIcon {
name: "bluetooth" name: "bluetooth"
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
@@ -71,19 +187,7 @@ BasePill {
DankIcon { DankIcon {
id: audioIconV id: audioIconV
name: root.getVolumeIconName()
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off"
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down"
} else {
return "volume_up"
}
}
return "volume_up"
}
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.centerIn: parent anchors.centerIn: parent
@@ -92,31 +196,85 @@ BasePill {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) { onWheel: function (wheelEvent) {
let delta = wheelEvent.angleDelta.y root.handleVolumeWheel(wheelEvent.angleDelta.y);
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0 wheelEvent.accepted = true;
let newVolume
if (delta > 0) {
newVolume = Math.min(100, currentVolume + 5)
} else {
newVolume = Math.max(0, currentVolume - 5)
}
if (AudioService.sink && AudioService.sink.audio) {
AudioService.sink.audio.muted = false
AudioService.sink.audio.volume = newVolume / 100
AudioService.playVolumeChangeSoundIfEnabled()
}
wheelEvent.accepted = true
} }
} }
} }
Rectangle {
width: micIconV.implicitWidth + 4
height: micIconV.implicitHeight + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showMicIcon
DankIcon {
id: micIconV
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness)
color: root.getMicIconColor()
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function (wheelEvent) {
root.handleMicWheel(wheelEvent.angleDelta.y);
wheelEvent.accepted = true;
}
}
}
Rectangle {
width: brightnessIconV.implicitWidth + 4
height: brightnessIconV.implicitHeight + 4
color: "transparent"
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIconV
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function (wheelEvent) {
root.handleBrightnessWheel(wheelEvent.angleDelta.y);
wheelEvent.accepted = true;
}
}
}
DankIcon {
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness)
color: root.getBatteryIconColor()
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
name: "print"
size: Theme.barIconSize(root.barThickness)
color: Theme.primary
anchors.horizontalCenter: parent.horizontalCenter
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
}
DankIcon { DankIcon {
name: "settings" name: "settings"
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon visible: root.hasNoVisibleIcons()
} }
} }
@@ -128,38 +286,24 @@ BasePill {
DankIcon { DankIcon {
id: networkIcon id: networkIcon
name: root.getNetworkIconName()
name: {
if (NetworkService.wifiToggling) {
return "sync"
}
const status = NetworkService.networkStatus
if (status === "ethernet") {
return "lan"
}
if (status === "vpn") {
return NetworkService.ethernetConnected ? "lan" : NetworkService.wifiSignalIcon
}
return NetworkService.wifiSignalIcon
}
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: { color: root.getNetworkIconColor()
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable visible: root.showNetworkIcon && NetworkService.networkAvailable
} }
DankIcon { DankIcon {
id: bluetoothIcon id: vpnIcon
name: "vpn_lock"
size: Theme.barIconSize(root.barThickness)
color: NetworkService.vpnConnected ? Theme.primary : Theme.outlineButton
anchors.verticalCenter: parent.verticalCenter
visible: root.showVpnIcon && NetworkService.vpnAvailable && NetworkService.vpnConnected
}
DankIcon {
id: bluetoothIcon
name: "bluetooth" name: "bluetooth"
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
@@ -176,19 +320,7 @@ BasePill {
DankIcon { DankIcon {
id: audioIcon id: audioIcon
name: root.getVolumeIconName()
name: {
if (AudioService.sink && AudioService.sink.audio) {
if (AudioService.sink.audio.muted || AudioService.sink.audio.volume === 0) {
return "volume_off";
} else if (AudioService.sink.audio.volume * 100 < 33) {
return "volume_down";
} else {
return "volume_up";
}
}
return "volume_up";
}
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor color: Theme.widgetIconColor
anchors.centerIn: parent anchors.centerIn: parent
@@ -196,34 +328,83 @@ BasePill {
MouseArea { MouseArea {
id: audioWheelArea id: audioWheelArea
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.NoButton acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) { onWheel: function (wheelEvent) {
let delta = wheelEvent.angleDelta.y; root.handleVolumeWheel(wheelEvent.angleDelta.y);
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0; wheelEvent.accepted = true;
let newVolume; }
if (delta > 0) { }
newVolume = Math.min(100, currentVolume + 5); }
} else {
newVolume = Math.max(0, currentVolume - 5); Rectangle {
} width: micIcon.implicitWidth + 4
if (AudioService.sink && AudioService.sink.audio) { height: micIcon.implicitHeight + 4
AudioService.sink.audio.muted = false; color: "transparent"
AudioService.sink.audio.volume = newVolume / 100; anchors.verticalCenter: parent.verticalCenter
AudioService.playVolumeChangeSoundIfEnabled(); visible: root.showMicIcon
}
DankIcon {
id: micIcon
name: root.getMicIconName()
size: Theme.barIconSize(root.barThickness)
color: root.getMicIconColor()
anchors.centerIn: parent
}
MouseArea {
id: micWheelArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function (wheelEvent) {
root.handleMicWheel(wheelEvent.angleDelta.y);
wheelEvent.accepted = true;
}
}
}
Rectangle {
width: brightnessIcon.implicitWidth + 4
height: brightnessIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
visible: root.showBrightnessIcon && DisplayService.brightnessAvailable && root.hasPinnedBrightnessDevice()
DankIcon {
id: brightnessIcon
name: root.getBrightnessIconName()
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor
anchors.centerIn: parent
}
MouseArea {
id: brightnessWheelArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function (wheelEvent) {
root.handleBrightnessWheel(wheelEvent.angleDelta.y);
wheelEvent.accepted = true; wheelEvent.accepted = true;
} }
} }
} }
DankIcon { DankIcon {
name: "mic" id: batteryIcon
name: Theme.getBatteryIcon(BatteryService.batteryLevel, BatteryService.isCharging, BatteryService.batteryAvailable)
size: Theme.barIconSize(root.barThickness)
color: root.getBatteryIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showBatteryIcon && BatteryService.batteryAvailable
}
DankIcon {
id: printerIcon
name: "print"
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: Theme.primary color: Theme.primary
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: false visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
} }
DankIcon { DankIcon {
@@ -231,9 +412,15 @@ BasePill {
size: Theme.barIconSize(root.barThickness) size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.widgetIconColor color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon visible: root.hasNoVisibleIcons()
} }
} }
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
acceptedButtons: Qt.NoButton
}
} }
} }
} }

View File

@@ -1,13 +1,8 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services
import qs.Widgets import qs.Widgets
import qs.Modules.DankDash
DankPopout { DankPopout {
id: root id: root
@@ -27,42 +22,130 @@ DankPopout {
property bool __focusArmed: false property bool __focusArmed: false
property bool __contentReady: false property bool __contentReady: false
property var __mediaTabRef: null
property int __dropdownType: 0
property point __dropdownAnchor: Qt.point(0, 0)
property bool __dropdownRightEdge: false
property var __dropdownPlayer: null
property var __dropdownPlayers: []
function __showVolumeDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 1;
}
function __showAudioDevicesDropdown(pos, rightEdge) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownType = 2;
}
function __showPlayersDropdown(pos, rightEdge, player, players) {
__dropdownAnchor = pos;
__dropdownRightEdge = rightEdge;
__dropdownPlayer = player;
__dropdownPlayers = players;
__dropdownType = 3;
}
function __hideDropdowns() {
__volumeCloseTimer.stop();
__dropdownType = 0;
__mediaTabRef?.resetDropdownStates();
}
function __startCloseTimer() {
__volumeCloseTimer.restart();
}
function __stopCloseTimer() {
__volumeCloseTimer.stop();
}
Timer {
id: __volumeCloseTimer
interval: 400
onTriggered: {
if (__dropdownType === 1) {
__hideDropdowns();
}
}
}
overlayContent: Component {
MediaDropdownOverlay {
dropdownType: root.__dropdownType
anchorPos: root.__dropdownAnchor
isRightEdge: root.__dropdownRightEdge
activePlayer: root.__dropdownPlayer
allPlayers: root.__dropdownPlayers
onCloseRequested: root.__hideDropdowns()
onPanelEntered: root.__stopCloseTimer()
onPanelExited: root.__startCloseTimer()
onVolumeChanged: volume => {
const player = root.__dropdownPlayer;
const isChrome = player?.identity?.toLowerCase().includes("chrome") || player?.identity?.toLowerCase().includes("chromium");
const usePlayerVolume = player && player.volumeSupported && !isChrome;
if (usePlayerVolume) {
player.volume = volume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = volume;
}
}
onPlayerSelected: player => {
const currentPlayer = MprisController.activePlayer;
if (currentPlayer && currentPlayer !== player && currentPlayer.canPause) {
currentPlayer.pause();
}
MprisController.activePlayer = player;
root.__hideDropdowns();
}
onDeviceSelected: device => {
root.__hideDropdowns();
}
}
}
function __tryFocusOnce() { function __tryFocusOnce() {
if (!__focusArmed) if (!__focusArmed)
return return;
const win = root.window const win = root.window;
if (!win || !win.visible) if (!win || !win.visible)
return return;
if (!contentLoader.item) if (!contentLoader.item)
return return;
if (win.requestActivate) if (win.requestActivate)
win.requestActivate() win.requestActivate();
contentLoader.item.forceActiveFocus(Qt.TabFocusReason) contentLoader.item.forceActiveFocus(Qt.TabFocusReason);
if (contentLoader.item.activeFocus) if (contentLoader.item.activeFocus)
__focusArmed = false __focusArmed = false;
} }
onDashVisibleChanged: { onDashVisibleChanged: {
if (dashVisible) { if (dashVisible) {
__focusArmed = true __focusArmed = true;
__contentReady = !!contentLoader.item __contentReady = !!contentLoader.item;
open() open();
__tryFocusOnce() __tryFocusOnce();
} else { } else {
__focusArmed = false __focusArmed = false;
__contentReady = false __contentReady = false;
close() __hideDropdowns();
close();
} }
} }
Connections { Connections {
target: contentLoader target: contentLoader
function onLoaded() { function onLoaded() {
__contentReady = true __contentReady = true;
if (__focusArmed) if (__focusArmed)
__tryFocusOnce() __tryFocusOnce();
} }
} }
@@ -71,12 +154,12 @@ DankPopout {
enabled: !!root.window enabled: !!root.window
function onVisibleChanged() { function onVisibleChanged() {
if (__focusArmed) if (__focusArmed)
__tryFocusOnce() __tryFocusOnce();
} }
} }
onBackgroundClicked: { onBackgroundClicked: {
dashVisible = false dashVisible = false;
} }
content: Component { content: Component {
@@ -90,7 +173,7 @@ DankPopout {
Component.onCompleted: { Component.onCompleted: {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
mainContainer.forceActiveFocus() mainContainer.forceActiveFocus();
} }
} }
@@ -99,54 +182,54 @@ DankPopout {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
Qt.callLater(function () { Qt.callLater(function () {
mainContainer.forceActiveFocus() mainContainer.forceActiveFocus();
}) });
} }
} }
} }
Keys.onPressed: function (event) { Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) { if (event.key === Qt.Key_Escape) {
root.dashVisible = false root.dashVisible = false;
event.accepted = true event.accepted = true;
return return;
} }
if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) { if (event.key === Qt.Key_Tab && !(event.modifiers & Qt.ShiftModifier)) {
let nextIndex = root.currentTabIndex + 1 let nextIndex = root.currentTabIndex + 1;
while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) { while (nextIndex < tabBar.model.length && tabBar.model[nextIndex] && tabBar.model[nextIndex].isAction) {
nextIndex++ nextIndex++;
} }
if (nextIndex >= tabBar.model.length) { if (nextIndex >= tabBar.model.length) {
nextIndex = 0 nextIndex = 0;
} }
root.currentTabIndex = nextIndex root.currentTabIndex = nextIndex;
event.accepted = true event.accepted = true;
return return;
} }
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) { if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier))) {
let prevIndex = root.currentTabIndex - 1 let prevIndex = root.currentTabIndex - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex-- prevIndex--;
} }
if (prevIndex < 0) { if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1 prevIndex = tabBar.model.length - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) { while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex-- prevIndex--;
} }
} }
if (prevIndex >= 0) { if (prevIndex >= 0) {
root.currentTabIndex = prevIndex root.currentTabIndex = prevIndex;
} }
event.accepted = true event.accepted = true;
return return;
} }
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) { if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) { if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true event.accepted = true;
return return;
} }
} }
} }
@@ -171,50 +254,54 @@ DankPopout {
focus: false focus: false
activeFocusOnTab: false activeFocusOnTab: false
nextFocusTarget: { nextFocusTarget: {
const item = pages.currentItem const item = pages.currentItem;
if (!item) if (!item)
return null return null;
if (item.focusTarget) if (item.focusTarget)
return item.focusTarget return item.focusTarget;
return item return item;
} }
model: { model: {
let tabs = [{ let tabs = [
"icon": "dashboard", {
"text": I18n.tr("Overview") "icon": "dashboard",
}, { "text": I18n.tr("Overview")
"icon": "music_note", },
"text": I18n.tr("Media") {
}, { "icon": "music_note",
"icon": "wallpaper", "text": I18n.tr("Media")
"text": I18n.tr("Wallpapers") },
}] {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}
];
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabs.push({ tabs.push({
"icon": "wb_sunny", "icon": "wb_sunny",
"text": I18n.tr("Weather") "text": I18n.tr("Weather")
}) });
} }
tabs.push({ tabs.push({
"icon": "settings", "icon": "settings",
"text": I18n.tr("Settings"), "text": I18n.tr("Settings"),
"isAction": true "isAction": true
}) });
return tabs return tabs;
} }
onTabClicked: function (index) { onTabClicked: function (index) {
root.currentTabIndex = index root.currentTabIndex = index;
} }
onActionTriggered: function (index) { onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3 let settingsIndex = SettingsData.weatherEnabled ? 4 : 3;
if (index === settingsIndex) { if (index === settingsIndex) {
dashVisible = false dashVisible = false;
settingsModal.show() settingsModal.show();
} }
} }
} }
@@ -229,14 +316,14 @@ DankPopout {
width: parent.width width: parent.width
implicitHeight: { implicitHeight: {
if (currentIndex === 0) if (currentIndex === 0)
return overviewTab.implicitHeight return overviewTab.implicitHeight;
if (currentIndex === 1) if (currentIndex === 1)
return mediaTab.implicitHeight return mediaTab.implicitHeight;
if (currentIndex === 2) if (currentIndex === 2)
return wallpaperTab.implicitHeight return wallpaperTab.implicitHeight;
if (SettingsData.weatherEnabled && currentIndex === 3) if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight return weatherTab.implicitHeight;
return overviewTab.implicitHeight return overviewTab.implicitHeight;
} }
currentIndex: root.currentTabIndex currentIndex: root.currentTabIndex
@@ -244,24 +331,42 @@ DankPopout {
id: overviewTab id: overviewTab
onCloseDash: { onCloseDash: {
root.dashVisible = false root.dashVisible = false;
} }
onSwitchToWeatherTab: { onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) { if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3 tabBar.currentIndex = 3;
tabBar.tabClicked(3) tabBar.tabClicked(3);
} }
} }
onSwitchToMediaTab: { onSwitchToMediaTab: {
tabBar.currentIndex = 1 tabBar.currentIndex = 1;
tabBar.tabClicked(1) tabBar.tabClicked(1);
} }
} }
MediaPlayerTab { MediaPlayerTab {
id: mediaTab id: mediaTab
targetScreen: root.screen
popoutX: root.alignedX
popoutY: root.alignedY
popoutWidth: root.alignedWidth
popoutHeight: root.alignedHeight
contentOffsetY: Theme.spacingM + 48 + Theme.spacingS + Theme.spacingXS
Component.onCompleted: root.__mediaTabRef = this
onShowVolumeDropdown: (pos, screen, rightEdge, player, players) => {
root.__showVolumeDropdown(pos, rightEdge, player, players);
}
onShowAudioDevicesDropdown: (pos, screen, rightEdge) => {
root.__showAudioDevicesDropdown(pos, rightEdge);
}
onShowPlayersDropdown: (pos, screen, rightEdge, player, players) => {
root.__showPlayersDropdown(pos, rightEdge, player, players);
}
onHideDropdowns: root.__hideDropdowns()
onVolumeButtonExited: root.__startCloseTimer()
} }
WallpaperTab { WallpaperTab {

View File

@@ -0,0 +1,491 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.Pipewire
import qs.Common
import qs.Services
import qs.Widgets
Item {
id: root
property int dropdownType: 0
property var activePlayer: null
property var allPlayers: []
property point anchorPos: Qt.point(0, 0)
property bool isRightEdge: false
property bool __isChromeBrowser: {
if (!activePlayer?.identity)
return false;
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
property var availableDevices: Pipewire.nodes.values.filter(node => node.audio && node.isSink && !node.isStream)
signal closeRequested
signal deviceSelected(var device)
signal playerSelected(var player)
signal volumeChanged(real volume)
signal panelEntered
signal panelExited
property int __volumeHoverCount: 0
function volumeAreaEntered() {
__volumeHoverCount++;
panelEntered();
}
function volumeAreaExited() {
__volumeHoverCount--;
Qt.callLater(() => {
if (__volumeHoverCount <= 0)
panelExited();
});
}
Rectangle {
id: volumePanel
visible: dropdownType === 1 && volumeAvailable
width: 60
height: 180
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
opacity: dropdownType === 1 ? 1 : 0
scale: dropdownType === 1 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
hoverEnabled: true
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Item {
id: volumeSlider
width: parent.width * 0.5
height: parent.height - Theme.spacingXL * 2
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
Rectangle {
width: parent.width
height: parent.height
anchors.centerIn: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
Rectangle {
width: parent.width
height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
bottomLeftRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
}
Rectangle {
width: parent.width + 8
height: 8
radius: Theme.cornerRadius
y: {
const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
border.width: 3
border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
}
MouseArea {
anchors.fill: parent
anchors.margins: -12
enabled: volumeAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onEntered: volumeAreaEntered()
onExited: volumeAreaExited()
onPressed: mouse => updateVolume(mouse)
onPositionChanged: mouse => {
if (pressed)
updateVolume(mouse);
}
onClicked: mouse => updateVolume(mouse)
function updateVolume(mouse) {
if (!volumeAvailable)
return;
const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(1, ratio));
root.volumeChanged(volume);
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Theme.spacingL
text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
}
}
}
Rectangle {
id: audioDevicesPanel
visible: dropdownType === 2
width: 280
height: Math.max(200, Math.min(280, availableDevices.length * 50 + 100))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 2 ? 1 : 0
scale: dropdownType === 2 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Audio Output Devices (") + availableDevices.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: deviceColumn.height
clip: true
Column {
id: deviceColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: availableDevices
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: deviceMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: getAudioDeviceIcon(modelData)
size: 20
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
function getAudioDeviceIcon(device) {
if (!device?.name)
return "speaker";
const name = device.name.toLowerCase();
if (name.includes("bluez") || name.includes("bluetooth"))
return "headset";
if (name.includes("hdmi"))
return "tv";
if (name.includes("usb"))
return "headset";
return "speaker";
}
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
}
}
}
MouseArea {
id: deviceMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
root.deviceSelected(modelData);
}
}
}
}
}
}
}
}
}
Rectangle {
id: playersPanel
visible: dropdownType === 3
width: 240
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
x: isRightEdge ? anchorPos.x : anchorPos.x - width
y: anchorPos.y - height / 2
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
opacity: dropdownType === 3 ? 1 : 0
scale: dropdownType === 3 ? 1 : 0.96
transformOrigin: isRightEdge ? Item.Left : Item.Right
Behavior on opacity {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
Behavior on scale {
NumberAnimation {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: playerColumn.height
clip: true
Column {
id: playerColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: allPlayers || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: "music_note"
size: 20
color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: {
if (!modelData)
return "Unknown Player";
const identity = modelData.identity || "Unknown Player";
const trackTitle = modelData.trackTitle || "";
return trackTitle.length > 0 ? identity + " - " + trackTitle : identity;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
StyledText {
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0)
return artist + (isActive ? " (Active)" : "");
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
wrapMode: Text.NoWrap
width: parent.width
}
}
}
MouseArea {
id: playerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData?.identity) {
root.playerSelected(modelData);
}
}
}
}
}
}
}
}
}
MouseArea {
anchors.fill: parent
z: -1
enabled: dropdownType !== 0
onClicked: closeRequested()
}
}

View File

@@ -1,9 +1,7 @@
import QtQuick import QtQuick
import QtQuick.Controls
import QtQuick.Effects import QtQuick.Effects
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell.Services.Mpris import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import Quickshell.Io import Quickshell.Io
import Quickshell import Quickshell
import qs.Common import qs.Common
@@ -15,14 +13,42 @@ Item {
property MprisPlayer activePlayer: MprisController.activePlayer property MprisPlayer activePlayer: MprisController.activePlayer
property var allPlayers: MprisController.availablePlayers property var allPlayers: MprisController.availablePlayers
property var targetScreen: null
property real popoutX: 0
property real popoutY: 0
property real popoutWidth: 0
property real popoutHeight: 0
property real contentOffsetY: 0
signal showVolumeDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal showAudioDevicesDropdown(point pos, var screen, bool rightEdge)
signal showPlayersDropdown(point pos, var screen, bool rightEdge, var player, var players)
signal hideDropdowns
signal volumeButtonExited
property bool volumeExpanded: false
property bool devicesExpanded: false
property bool playersExpanded: false
function resetDropdownStates() {
volumeExpanded = false;
devicesExpanded = false;
playersExpanded = false;
}
DankTooltipV2 { DankTooltipV2 {
id: sharedTooltip id: sharedTooltip
} }
readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right readonly property bool isRightEdge: (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Right
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported) || (AudioService.sink && AudioService.sink.audio) readonly property bool __isChromeBrowser: {
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported if (!activePlayer?.identity)
return false;
const id = activePlayer.identity.toLowerCase();
return id.includes("chrome") || id.includes("chromium");
}
readonly property bool volumeAvailable: (activePlayer && activePlayer.volumeSupported && !__isChromeBrowser) || (AudioService.sink && AudioService.sink.audio)
readonly property bool usePlayerVolume: activePlayer && activePlayer.volumeSupported && !__isChromeBrowser
readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0) readonly property real currentVolume: usePlayerVolume ? activePlayer.volume : (AudioService.sink?.audio?.volume ?? 0)
// Palette that stays stable across track switches until new colors are ready // Palette that stays stable across track switches until new colors are ready
@@ -334,383 +360,6 @@ Item {
anchors.fill: parent anchors.fill: parent
clip: false clip: false
visible: !_noneAvailable && (!showNoPlayerNow) visible: !_noneAvailable && (!showNoPlayerNow)
MouseArea {
anchors.fill: parent
enabled: audioDevicesButton.devicesExpanded || volumeButton.volumeExpanded || playerSelectorButton.playersExpanded
onClicked: function (mouse) {
const clickOutside = item => {
return mouse.x < item.x || mouse.x > item.x + item.width || mouse.y < item.y || mouse.y > item.y + item.height;
};
if (playerSelectorButton.playersExpanded && clickOutside(playerSelectorDropdown)) {
playerSelectorButton.playersExpanded = false;
}
if (audioDevicesButton.devicesExpanded && clickOutside(audioDevicesDropdown)) {
audioDevicesButton.devicesExpanded = false;
}
if (volumeButton.volumeExpanded && clickOutside(volumeSliderPanel) && clickOutside(volumeButton)) {
volumeButton.volumeExpanded = false;
}
}
}
Popup {
id: audioDevicesDropdown
width: 280
height: audioDevicesButton.devicesExpanded ? Math.max(200, Math.min(280, audioDevicesDropdown.availableDevices.length * 50 + 100)) : 0
x: isRightEdge ? audioDevicesButton.x + audioDevicesButton.width + Theme.spacingS : audioDevicesButton.x - width - Theme.spacingS
y: audioDevicesButton.y - 50
visible: audioDevicesButton.devicesExpanded
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
property var availableDevices: Pipewire.nodes.values.filter(node => {
return node.audio && node.isSink && !node.isStream;
})
background: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
radius: Theme.cornerRadius * 2
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Audio Output Devices (") + audioDevicesDropdown.availableDevices.length + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: deviceColumn.height
clip: true
Column {
id: deviceColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: audioDevicesDropdown.availableDevices
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: deviceMouseAreaLeft.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === AudioService.sink ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === AudioService.sink ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: getAudioDeviceIcon(modelData)
size: 20
color: modelData === AudioService.sink ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: AudioService.displayName(modelData)
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === AudioService.sink ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: modelData === AudioService.sink ? "Active" : "Available"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: deviceMouseAreaLeft
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData) {
Pipewire.preferredDefaultAudioSink = modelData;
}
audioDevicesButton.devicesExpanded = false;
}
}
Behavior on border.color {
ColorAnimation {
duration: Anims.durShort
}
}
}
}
}
}
}
}
Popup {
id: playerSelectorDropdown
width: 240
height: playerSelectorButton.playersExpanded ? Math.max(180, Math.min(240, (root.allPlayers?.length || 0) * 50 + 80)) : 0
x: isRightEdge ? playerSelectorButton.x + playerSelectorButton.width + Theme.spacingS : playerSelectorButton.x - width - Theme.spacingS
y: playerSelectorButton.y - 50
visible: playerSelectorButton.playersExpanded
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
background: Rectangle {
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.98)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.6)
border.width: 2
radius: Theme.cornerRadius * 2
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
Behavior on height {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasizedDecel
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingM
StyledText {
text: I18n.tr("Media Players (") + (allPlayers?.length || 0) + ")"
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: Theme.surfaceText
width: parent.width
horizontalAlignment: Text.AlignHCenter
bottomPadding: Theme.spacingM
}
DankFlickable {
width: parent.width
height: parent.height - 40
contentHeight: playerColumn.height
clip: true
Column {
id: playerColumn
width: parent.width
spacing: Theme.spacingS
Repeater {
model: allPlayers || []
delegate: Rectangle {
required property var modelData
required property int index
width: parent.width
height: 48
radius: Theme.cornerRadius
color: playerMouseArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: modelData === activePlayer ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: modelData === activePlayer ? 2 : 1
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
width: parent.width - Theme.spacingM * 2
DankIcon {
name: "music_note"
size: 20
color: modelData === activePlayer ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
Column {
anchors.verticalCenter: parent.verticalCenter
width: parent.width - 20 - Theme.spacingM * 2
StyledText {
text: {
if (!modelData)
return "Unknown Player";
const identity = modelData.identity || "Unknown Player";
const trackTitle = modelData.trackTitle || "";
if (trackTitle.length > 0) {
return identity + " - " + trackTitle;
}
return identity;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: modelData === activePlayer ? Font.Medium : Font.Normal
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
StyledText {
text: {
if (!modelData)
return "";
const artist = modelData.trackArtist || "";
const isActive = modelData === activePlayer;
if (artist.length > 0) {
return artist + (isActive ? " (Active)" : "");
}
return isActive ? "Active" : "Available";
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
elide: Text.ElideRight
width: parent.width
wrapMode: Text.NoWrap
}
}
}
MouseArea {
id: playerMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (modelData && modelData.identity) {
if (activePlayer && activePlayer !== modelData && activePlayer.canPause) {
activePlayer.pause();
}
MprisController.activePlayer = modelData;
}
playerSelectorButton.playersExpanded = false;
}
}
Behavior on border.color {
ColorAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
}
}
}
}
}
// Center Column: Main Media Content
ColumnLayout { ColumnLayout {
x: 72 x: 72
y: 20 y: 20
@@ -1051,14 +700,12 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 185 y: 185
color: playerSelectorArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: playerSelectorArea.containsMouse || playersExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
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
z: 100 z: 100
visible: (allPlayers?.length || 0) >= 1 visible: (allPlayers?.length || 0) >= 1
property bool playersExpanded: false
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: "assistant_device" name: "assistant_device"
@@ -1072,14 +719,20 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
parent.playersExpanded = !parent.playersExpanded; if (playersExpanded) {
} hideDropdowns();
onEntered: { return;
sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left"); }
} hideDropdowns();
onExited: { playersExpanded = true;
sharedTooltip.hide(); const buttonsOnRight = !isRightEdge;
const btnY = playerSelectorButton.y + playerSelectorButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showPlayersDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
} }
onEntered: sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
} }
} }
@@ -1090,21 +743,14 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 130 y: 130
color: volumeButtonArea.containsMouse && volumeAvailable ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: volumeButtonArea.containsMouse && volumeAvailable || volumeExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, volumeAvailable ? 0.3 : 0.15)
border.width: 1 border.width: 1
z: 101 z: 101
enabled: volumeAvailable enabled: volumeAvailable
property bool volumeExpanded: false
property real previousVolume: 0.0 property real previousVolume: 0.0
Timer {
id: volumeHideTimer
interval: 500
onTriggered: volumeButton.volumeExpanded = false
}
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: getVolumeIcon() name: getVolumeIcon()
@@ -1118,11 +764,19 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: { onEntered: {
volumeButton.volumeExpanded = true; if (volumeExpanded)
volumeHideTimer.stop(); return;
hideDropdowns();
volumeExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = volumeButton.y + volumeButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showVolumeDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight, activePlayer, allPlayers);
} }
onExited: { onExited: {
volumeHideTimer.restart(); if (volumeExpanded)
volumeButtonExited();
} }
onClicked: { onClicked: {
if (currentVolume > 0) { if (currentVolume > 0) {
@@ -1142,22 +796,15 @@ Item {
} }
} }
onWheel: wheelEvent => { onWheel: wheelEvent => {
let delta = wheelEvent.angleDelta.y; const delta = wheelEvent.angleDelta.y;
let current = (currentVolume * 100) || 0; const current = (currentVolume * 100) || 0;
let newVolume; const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (delta > 0) {
newVolume = Math.min(100, current + 5);
} else {
newVolume = Math.max(0, current - 5);
}
if (usePlayerVolume) { if (usePlayerVolume) {
activePlayer.volume = newVolume / 100; activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) { } else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100; AudioService.sink.audio.volume = newVolume / 100;
} }
volumeButton.volumeExpanded = true;
wheelEvent.accepted = true; wheelEvent.accepted = true;
} }
} }
@@ -1170,16 +817,14 @@ Item {
radius: 20 radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
y: 240 y: 240
color: audioDevicesArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent" color: audioDevicesArea.containsMouse || devicesExpanded ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.2) : "transparent"
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
z: 100 z: 100
property bool devicesExpanded: false
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: parent.devicesExpanded ? "expand_less" : "speaker" name: devicesExpanded ? "expand_less" : "speaker"
size: 18 size: 18
color: Theme.surfaceText color: Theme.surfaceText
} }
@@ -1190,247 +835,20 @@ Item {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
parent.devicesExpanded = !parent.devicesExpanded; if (devicesExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
devicesExpanded = true;
const buttonsOnRight = !isRightEdge;
const btnY = audioDevicesButton.y + audioDevicesButton.height / 2;
const screenX = buttonsOnRight ? (popoutX + popoutWidth) : popoutX;
const screenY = popoutY + contentOffsetY + btnY;
showAudioDevicesDropdown(Qt.point(screenX, screenY), targetScreen, buttonsOnRight);
} }
onEntered: { onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left"); onExited: sharedTooltip.hide()
}
onExited: {
sharedTooltip.hide();
}
}
}
}
Popup {
id: volumeSliderPanel
width: 60
height: 180
x: isRightEdge ? volumeButton.x + volumeButton.width + Theme.spacingS : volumeButton.x - width - Theme.spacingS
y: volumeButton.y - 50
visible: volumeButton.volumeExpanded && volumeAvailable
closePolicy: Popup.NoAutoClose
modal: false
dim: false
padding: 0
background: Rectangle {
radius: Theme.cornerRadius * 2
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 0.95)
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
border.width: 1
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowHorizontalOffset: 0
shadowVerticalOffset: 8
shadowBlur: 1.0
shadowColor: Qt.rgba(0, 0, 0, 0.4)
shadowOpacity: 0.7
}
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
Item {
id: volumeSlider
width: parent.width * 0.5
height: parent.height - Theme.spacingXL * 2
anchors.top: parent.top
anchors.topMargin: Theme.spacingS
anchors.horizontalCenter: parent.horizontalCenter
property bool dragging: false
property bool containsMouse: volumeSliderArea.containsMouse
property bool active: volumeSliderArea.containsMouse || volumeSliderArea.pressed || dragging
Rectangle {
id: sliderTrack
width: parent.width
height: parent.height
anchors.centerIn: parent
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
}
Rectangle {
id: sliderFill
width: parent.width
height: volumeAvailable ? (Math.min(1.0, currentVolume) * parent.height) : 0
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
bottomLeftRadius: Theme.cornerRadius
bottomRightRadius: Theme.cornerRadius
topLeftRadius: 0
topRightRadius: 0
}
Rectangle {
id: sliderHandle
width: parent.width + 8
height: 8
radius: Theme.cornerRadius
y: {
const ratio = volumeAvailable ? Math.min(1.0, currentVolume) : 0;
const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio)));
}
anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary
border.width: 3
border.color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1.0)
Rectangle {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.onPrimary
opacity: volumeSliderArea.pressed ? 0.16 : (volumeSliderArea.containsMouse ? 0.08 : 0)
visible: opacity > 0
}
Rectangle {
id: ripple
anchors.centerIn: parent
width: 0
height: 0
radius: width / 2
color: Theme.onPrimary
opacity: 0
function start() {
opacity = 0.16;
width = 0;
height = 0;
rippleAnimation.start();
}
SequentialAnimation {
id: rippleAnimation
NumberAnimation {
target: ripple
properties: "width,height"
to: 28
duration: 180
}
NumberAnimation {
target: ripple
property: "opacity"
to: 0
duration: 150
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onPressedChanged: {
if (pressed) {
ripple.start();
}
}
}
scale: volumeSlider.active ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
}
MouseArea {
id: volumeSliderArea
anchors.fill: parent
anchors.margins: -12
enabled: volumeAvailable
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
preventStealing: true
onEntered: {
volumeHideTimer.stop();
}
onExited: {
volumeHideTimer.restart();
}
onPressed: function (mouse) {
parent.dragging = true;
updateVolume(mouse);
}
onReleased: {
parent.dragging = false;
}
onPositionChanged: function (mouse) {
if (pressed) {
updateVolume(mouse);
}
}
onClicked: function (mouse) {
updateVolume(mouse);
}
onWheel: wheelEvent => {
const step = 1;
adjustVolume(wheelEvent.angleDelta.y > 0 ? step : -step);
wheelEvent.accepted = true;
}
function updateVolume(mouse) {
if (volumeAvailable) {
const ratio = 1.0 - (mouse.y / height);
const volume = Math.max(0, Math.min(1, ratio));
if (usePlayerVolume) {
activePlayer.volume = volume;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = volume;
}
}
}
}
}
StyledText {
anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: Theme.spacingL
text: volumeAvailable ? Math.round(currentVolume * 100) + "%" : "0%"
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
} }
} }
} }

View File

@@ -7,6 +7,10 @@ DankOSD {
id: root id: root
readonly property bool useVertical: isVerticalLayout readonly property bool useVertical: isVerticalLayout
property int targetBrightness: {
DisplayService.brightnessVersion;
return DisplayService.brightnessLevel;
}
osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, Screen.width - Theme.spacingM * 2) osdWidth: useVertical ? (40 + Theme.spacingS * 2) : Math.min(260, Screen.width - Theme.spacingM * 2)
osdHeight: useVertical ? Math.min(260, Screen.height - Theme.spacingM * 2) : (40 + Theme.spacingS * 2) osdHeight: useVertical ? Math.min(260, Screen.height - Theme.spacingM * 2) : (40 + Theme.spacingS * 2)
@@ -17,7 +21,7 @@ DankOSD {
target: DisplayService target: DisplayService
function onBrightnessChanged(showOsd) { function onBrightnessChanged(showOsd) {
if (showOsd && SettingsData.osdBrightnessEnabled) { if (showOsd && SettingsData.osdBrightnessEnabled) {
root.show() root.show();
} }
} }
} }
@@ -48,13 +52,13 @@ DankOSD {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: { name: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") { if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
return "brightness_medium" return "brightness_medium";
} else if (deviceInfo.name.includes("kbd")) { } else if (deviceInfo.name.includes("kbd")) {
return "keyboard" return "keyboard";
} else { } else {
return "lightbulb" return "lightbulb";
} }
} }
size: Theme.iconSize size: Theme.iconSize
@@ -70,74 +74,50 @@ DankOSD {
x: parent.gap * 2 + Theme.iconSize x: parent.gap * 2 + Theme.iconSize
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
minimum: { minimum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo) return 1 if (!deviceInfo)
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id) return 1;
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id);
if (isExponential) { if (isExponential) {
return 1 return 1;
} }
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0 return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0;
} }
maximum: { maximum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo) return 100 if (!deviceInfo)
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id) return 100;
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id);
if (isExponential) { if (isExponential) {
return 100 return 100;
} }
return deviceInfo.displayMax || 100 return deviceInfo.displayMax || 100;
} }
enabled: DisplayService.brightnessAvailable enabled: DisplayService.brightnessAvailable
showValue: true showValue: true
unit: { unit: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo) return "%" if (!deviceInfo)
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id) return "%";
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id);
if (isExponential) { if (isExponential) {
return "%" return "%";
} }
return deviceInfo.class === "ddc" ? "" : "%" return deviceInfo.class === "ddc" ? "" : "%";
} }
thumbOutlineColor: Theme.surfaceContainer thumbOutlineColor: Theme.surfaceContainer
alwaysShowValue: SettingsData.osdAlwaysShowValue alwaysShowValue: SettingsData.osdAlwaysShowValue
value: !isDragging ? root.targetBrightness : value
Component.onCompleted: {
if (DisplayService.brightnessAvailable) {
value = DisplayService.brightnessLevel
}
}
onSliderValueChanged: newValue => { onSliderValueChanged: newValue => {
if (DisplayService.brightnessAvailable) { if (DisplayService.brightnessAvailable) {
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true) DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true);
resetHideTimer() resetHideTimer();
} }
}
onContainsMouseChanged: {
setChildHovered(containsMouse)
} }
onSliderDragFinished: finalValue => { onContainsMouseChanged: {
if (DisplayService.brightnessAvailable) { setChildHovered(containsMouse);
DisplayService.setBrightness(finalValue, DisplayService.lastIpcDevice, true)
}
}
Connections {
target: DisplayService
function onBrightnessChanged(showOsd) {
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
brightnessSlider.value = DisplayService.brightnessLevel
}
}
function onDeviceSwitched() {
if (!brightnessSlider.pressed && brightnessSlider.value !== DisplayService.brightnessLevel) {
brightnessSlider.value = DisplayService.brightnessLevel
}
}
} }
} }
} }
@@ -161,13 +141,13 @@ DankOSD {
DankIcon { DankIcon {
anchors.centerIn: parent anchors.centerIn: parent
name: { name: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") { if (!deviceInfo || deviceInfo.class === "backlight" || deviceInfo.class === "ddc") {
return "brightness_medium" return "brightness_medium";
} else if (deviceInfo.name.includes("kbd")) { } else if (deviceInfo.name.includes("kbd")) {
return "keyboard" return "keyboard";
} else { } else {
return "lightbulb" return "lightbulb";
} }
} }
size: Theme.iconSize size: Theme.iconSize
@@ -183,22 +163,26 @@ DankOSD {
y: gap * 2 + Theme.iconSize y: gap * 2 + Theme.iconSize
property bool dragging: false property bool dragging: false
property int value: DisplayService.brightnessAvailable ? DisplayService.brightnessLevel : 0 property int value: !dragging ? root.targetBrightness : value
readonly property int minimum: { readonly property int minimum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo) return 1 if (!deviceInfo)
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id) return 1;
if (isExponential) return 1 const isExponential = SessionData.getBrightnessExponential(deviceInfo.id);
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0 if (isExponential)
return 1;
return (deviceInfo.class === "backlight" || deviceInfo.class === "ddc") ? 1 : 0;
} }
readonly property int maximum: { readonly property int maximum: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
if (!deviceInfo) return 100 if (!deviceInfo)
const isExponential = SessionData.getBrightnessExponential(deviceInfo.id) return 100;
if (isExponential) return 100 const isExponential = SessionData.getBrightnessExponential(deviceInfo.id);
return deviceInfo.displayMax || 100 if (isExponential)
return 100;
return deviceInfo.displayMax || 100;
} }
Rectangle { Rectangle {
@@ -214,8 +198,8 @@ DankOSD {
id: vertFill id: vertFill
width: parent.width width: parent.width
height: { height: {
const ratio = (vertSlider.value - vertSlider.minimum) / (vertSlider.maximum - vertSlider.minimum) const ratio = (vertSlider.value - vertSlider.minimum) / (vertSlider.maximum - vertSlider.minimum);
return ratio * parent.height return ratio * parent.height;
} }
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -229,9 +213,9 @@ DankOSD {
height: 8 height: 8
radius: Theme.cornerRadius radius: Theme.cornerRadius
y: { y: {
const ratio = (vertSlider.value - vertSlider.minimum) / (vertSlider.maximum - vertSlider.minimum) const ratio = (vertSlider.value - vertSlider.minimum) / (vertSlider.maximum - vertSlider.minimum);
const travel = parent.height - height const travel = parent.height - height;
return Math.max(0, Math.min(travel, travel * (1 - ratio))) return Math.max(0, Math.min(travel, travel * (1 - ratio)));
} }
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
color: Theme.primary color: Theme.primary
@@ -248,50 +232,34 @@ DankOSD {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onContainsMouseChanged: { onContainsMouseChanged: {
setChildHovered(containsMouse) setChildHovered(containsMouse);
} }
onPressed: mouse => { onPressed: mouse => {
vertSlider.dragging = true vertSlider.dragging = true;
updateBrightness(mouse) updateBrightness(mouse);
} }
onReleased: { onReleased: {
vertSlider.dragging = false vertSlider.dragging = false;
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
updateBrightness(mouse) updateBrightness(mouse);
} }
} }
onClicked: mouse => { onClicked: mouse => {
updateBrightness(mouse) updateBrightness(mouse);
} }
function updateBrightness(mouse) { function updateBrightness(mouse) {
if (DisplayService.brightnessAvailable) { if (DisplayService.brightnessAvailable) {
const ratio = 1.0 - (mouse.y / height) const ratio = 1.0 - (mouse.y / height);
const newValue = Math.round(vertSlider.minimum + ratio * (vertSlider.maximum - vertSlider.minimum)) const newValue = Math.round(vertSlider.minimum + ratio * (vertSlider.maximum - vertSlider.minimum));
DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true) DisplayService.setBrightness(newValue, DisplayService.lastIpcDevice, true);
resetHideTimer() resetHideTimer();
}
}
}
Connections {
target: DisplayService
function onBrightnessChanged(showOsd) {
if (!vertSlider.dragging && vertSlider.value !== DisplayService.brightnessLevel) {
vertSlider.value = DisplayService.brightnessLevel
}
}
function onDeviceSwitched() {
if (!vertSlider.dragging && vertSlider.value !== DisplayService.brightnessLevel) {
vertSlider.value = DisplayService.brightnessLevel
} }
} }
} }
@@ -302,10 +270,10 @@ DankOSD {
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.bottomMargin: gap anchors.bottomMargin: gap
text: { text: {
const deviceInfo = DisplayService.getCurrentDeviceInfo() const deviceInfo = DisplayService.getCurrentDeviceInfo();
const isExponential = deviceInfo ? SessionData.getBrightnessExponential(deviceInfo.id) : false const isExponential = deviceInfo ? SessionData.getBrightnessExponential(deviceInfo.id) : false;
const unit = (deviceInfo && deviceInfo.class === "ddc" && !isExponential) ? "" : "%" const unit = (deviceInfo && deviceInfo.class === "ddc" && !isExponential) ? "" : "%";
return vertSlider.value + unit return vertSlider.value + unit;
} }
font.pixelSize: Theme.fontSizeSmall font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText color: Theme.surfaceText

View File

@@ -186,6 +186,32 @@ Item {
} }
} }
function triggerPopout() {
if (pillClickAction) {
if (pillClickAction.length === 0) {
pillClickAction();
return;
}
const pill = isVertical ? verticalPill : horizontalPill;
const globalPos = pill.mapToGlobal(0, 0);
const currentScreen = parentScreen || Screen;
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.width);
pillClickAction(pos.x, pos.y, pos.width, section, currentScreen);
return;
}
if (!hasPopout)
return;
const pill = isVertical ? verticalPill : horizontalPill;
const globalPos = pill.visualContent.mapToGlobal(0, 0);
const currentScreen = parentScreen || Screen;
const barPosition = axis?.edge === "left" ? 2 : (axis?.edge === "right" ? 3 : (axis?.edge === "top" ? 0 : 1));
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, pill.visualWidth, barSpacing, barPosition, barConfig);
pluginPopout.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, barSpacing, barConfig);
pluginPopout.toggle();
}
PluginPopout { PluginPopout {
id: pluginPopout id: pluginPopout
contentWidth: root.popoutWidth contentWidth: root.popoutWidth

View File

@@ -55,7 +55,7 @@ Column {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onPressed: { onPressed: {
if (root.closePopout) { if (root.closePopout) {
root.closePopout() root.closePopout();
} }
} }
} }

View File

@@ -784,13 +784,31 @@ Item {
} }
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) { function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
// Control Center settings are global, not per-widget instance switch (settingName) {
if (settingName === "showNetworkIcon") { case "showNetworkIcon":
SettingsData.set("controlCenterShowNetworkIcon", value); SettingsData.set("controlCenterShowNetworkIcon", value)
} else if (settingName === "showBluetoothIcon") { break
SettingsData.set("controlCenterShowBluetoothIcon", value); case "showBluetoothIcon":
} else if (settingName === "showAudioIcon") { SettingsData.set("controlCenterShowBluetoothIcon", value)
SettingsData.set("controlCenterShowAudioIcon", value); break
case "showAudioIcon":
SettingsData.set("controlCenterShowAudioIcon", value)
break
case "showVpnIcon":
SettingsData.set("controlCenterShowVpnIcon", value)
break
case "showBrightnessIcon":
SettingsData.set("controlCenterShowBrightnessIcon", value)
break
case "showMicIcon":
SettingsData.set("controlCenterShowMicIcon", value)
break
case "showBatteryIcon":
SettingsData.set("controlCenterShowBatteryIcon", value)
break
case "showPrinterIcon":
SettingsData.set("controlCenterShowPrinterIcon", value)
break
} }
} }

View File

@@ -497,39 +497,69 @@ Column {
} }
DankActionButton { DankActionButton {
id: ccMenuButton
visible: modelData.id === "controlCenterButton" visible: modelData.id === "controlCenterButton"
buttonSize: 32 buttonSize: 32
iconName: "more_vert" iconName: "more_vert"
iconSize: 18 iconSize: 18
iconColor: Theme.outline iconColor: Theme.outline
onClicked: { onClicked: {
console.log("Control Center three-dot button clicked for widget:", modelData.id);
controlCenterContextMenu.widgetData = modelData; controlCenterContextMenu.widgetData = modelData;
controlCenterContextMenu.sectionId = root.sectionId; controlCenterContextMenu.sectionId = root.sectionId;
controlCenterContextMenu.widgetIndex = index; controlCenterContextMenu.widgetIndex = index;
// Position relative to the action buttons row, not the specific button
var parentPos = parent.mapToItem(root, 0, 0); var buttonPos = ccMenuButton.mapToItem(root, 0, 0);
controlCenterContextMenu.x = parentPos.x - 210; // Position to the left with margin var popupWidth = controlCenterContextMenu.width;
controlCenterContextMenu.y = parentPos.y - 10; // Slightly above var popupHeight = controlCenterContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0) {
xPos = buttonPos.x + ccMenuButton.width + Theme.spacingS;
}
var yPos = buttonPos.y - popupHeight / 2 + ccMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
controlCenterContextMenu.x = xPos;
controlCenterContextMenu.y = yPos;
controlCenterContextMenu.open(); controlCenterContextMenu.open();
} }
} }
DankActionButton { DankActionButton {
id: privacyMenuButton
visible: modelData.id === "privacyIndicator" visible: modelData.id === "privacyIndicator"
buttonSize: 32 buttonSize: 32
iconName: "more_vert" iconName: "more_vert"
iconSize: 18 iconSize: 18
iconColor: Theme.outline iconColor: Theme.outline
onClicked: { onClicked: {
console.log("Privacy three-dot button clicked for widget:", modelData.id);
privacyContextMenu.widgetData = modelData; privacyContextMenu.widgetData = modelData;
privacyContextMenu.sectionId = root.sectionId; privacyContextMenu.sectionId = root.sectionId;
privacyContextMenu.widgetIndex = index; privacyContextMenu.widgetIndex = index;
// Position relative to the action buttons row, not the specific button
var parentPos = parent.mapToItem(root, 0, 0); var buttonPos = privacyMenuButton.mapToItem(root, 0, 0);
privacyContextMenu.x = parentPos.x - 210; // Position to the left with margin var popupWidth = privacyContextMenu.width;
privacyContextMenu.y = parentPos.y - 10; // Slightly above var popupHeight = privacyContextMenu.height;
var xPos = buttonPos.x - popupWidth - Theme.spacingS;
if (xPos < 0) {
xPos = buttonPos.x + privacyMenuButton.width + Theme.spacingS;
}
var yPos = buttonPos.y - popupHeight / 2 + privacyMenuButton.height / 2;
if (yPos < 0) {
yPos = Theme.spacingS;
} else if (yPos + popupHeight > root.height) {
yPos = root.height - popupHeight - Theme.spacingS;
}
privacyContextMenu.x = xPos;
privacyContextMenu.y = yPos;
privacyContextMenu.open(); privacyContextMenu.open();
} }
} }
@@ -699,21 +729,13 @@ Column {
property string sectionId: "" property string sectionId: ""
property int widgetIndex: -1 property int widgetIndex: -1
width: 200 width: 220
height: 120 height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0 padding: 0
modal: true modal: true
focus: true focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
console.log("Control Center context menu opened");
}
onClosed: {
console.log("Control Center context menu closed");
}
background: Rectangle { background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius radius: Theme.cornerRadius
@@ -722,168 +744,117 @@ Column {
} }
contentItem: Item { contentItem: Item {
Column { Column {
id: menuColumn id: menuColumn
anchors.fill: parent anchors.fill: parent
anchors.margins: Theme.spacingS anchors.margins: Theme.spacingS
spacing: 2 spacing: 2
Rectangle { Repeater {
width: parent.width model: [
height: 32 {
radius: Theme.cornerRadius icon: "lan",
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" label: I18n.tr("Network"),
setting: "showNetworkIcon",
checked: SettingsData.controlCenterShowNetworkIcon
},
{
icon: "vpn_lock",
label: I18n.tr("VPN"),
setting: "showVpnIcon",
checked: SettingsData.controlCenterShowVpnIcon
},
{
icon: "bluetooth",
label: I18n.tr("Bluetooth"),
setting: "showBluetoothIcon",
checked: SettingsData.controlCenterShowBluetoothIcon
},
{
icon: "volume_up",
label: I18n.tr("Audio"),
setting: "showAudioIcon",
checked: SettingsData.controlCenterShowAudioIcon
},
{
icon: "mic",
label: I18n.tr("Microphone"),
setting: "showMicIcon",
checked: SettingsData.controlCenterShowMicIcon
},
{
icon: "brightness_high",
label: I18n.tr("Brightness"),
setting: "showBrightnessIcon",
checked: SettingsData.controlCenterShowBrightnessIcon
},
{
icon: "battery_full",
label: I18n.tr("Battery"),
setting: "showBatteryIcon",
checked: SettingsData.controlCenterShowBatteryIcon
},
{
icon: "print",
label: I18n.tr("Printer"),
setting: "showPrinterIcon",
checked: SettingsData.controlCenterShowPrinterIcon
}
]
Row { delegate: Rectangle {
anchors.left: parent.left required property var modelData
anchors.leftMargin: Theme.spacingS required property int index
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon { width: menuColumn.width
name: "lan" height: 32
size: 16 radius: Theme.cornerRadius
color: Theme.surfaceText color: toggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: modelData.icon
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: modelData.label
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
} }
StyledText { DankToggle {
text: I18n.tr("Network Icon") id: toggle
font.pixelSize: Theme.fontSizeSmall anchors.right: parent.right
color: Theme.surfaceText anchors.rightMargin: Theme.spacingS
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: networkToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: SettingsData.controlCenterShowNetworkIcon
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", toggled);
}
}
MouseArea {
id: networkToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
networkToggle.checked = !networkToggle.checked;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showNetworkIcon", networkToggle.checked);
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: bluetoothToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "bluetooth"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: modelData.checked
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}
} }
StyledText { MouseArea {
text: I18n.tr("Bluetooth Icon") id: toggleArea
font.pixelSize: Theme.fontSizeSmall anchors.fill: parent
color: Theme.surfaceText hoverEnabled: true
font.weight: Font.Normal cursorShape: Qt.PointingHandCursor
anchors.verticalCenter: parent.verticalCenter onPressed: {
} toggle.checked = !toggle.checked;
} root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggle.checked);
}
DankToggle {
id: bluetoothToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: SettingsData.controlCenterShowBluetoothIcon
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", toggled);
}
}
MouseArea {
id: bluetoothToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
bluetoothToggle.checked = !bluetoothToggle.checked;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showBluetoothIcon", bluetoothToggle.checked);
}
}
}
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: audioToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankIcon {
name: "volume_up"
size: 16
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Audio Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
DankToggle {
id: audioToggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: SettingsData.controlCenterShowAudioIcon
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", toggled);
}
}
MouseArea {
id: audioToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
audioToggle.checked = !audioToggle.checked;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, "showAudioIcon", audioToggle.checked);
} }
} }
} }
@@ -932,7 +903,7 @@ Column {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -954,7 +925,7 @@ Column {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: micToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -1006,7 +977,7 @@ Column {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: cameraToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left
@@ -1058,7 +1029,7 @@ Column {
width: parent.width width: parent.width
height: 32 height: 32
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent" color: screenshareToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Row { Row {
anchors.left: parent.left anchors.left: parent.left

View File

@@ -0,0 +1,166 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.I3
Singleton {
id: root
property var widgetRegistry: ({})
property var dankBarRepeater: null
signal widgetRegistered(string widgetId, string screenName)
signal widgetUnregistered(string widgetId, string screenName)
function registerWidget(widgetId, screenName, widgetRef) {
if (!widgetId || !screenName || !widgetRef)
return;
if (!widgetRegistry[widgetId])
widgetRegistry[widgetId] = {};
widgetRegistry[widgetId][screenName] = widgetRef;
widgetRegistered(widgetId, screenName);
}
function unregisterWidget(widgetId, screenName) {
if (!widgetId || !screenName)
return;
if (!widgetRegistry[widgetId])
return;
delete widgetRegistry[widgetId][screenName];
if (Object.keys(widgetRegistry[widgetId]).length === 0)
delete widgetRegistry[widgetId];
widgetUnregistered(widgetId, screenName);
}
function getWidget(widgetId, screenName) {
if (!widgetRegistry[widgetId])
return null;
if (screenName)
return widgetRegistry[widgetId][screenName] || null;
const screens = Object.keys(widgetRegistry[widgetId]);
return screens.length > 0 ? widgetRegistry[widgetId][screens[0]] : null;
}
function getWidgetOnFocusedScreen(widgetId) {
if (!widgetRegistry[widgetId])
return null;
const focusedScreen = getFocusedScreenName();
if (focusedScreen && widgetRegistry[widgetId][focusedScreen])
return widgetRegistry[widgetId][focusedScreen];
const screens = Object.keys(widgetRegistry[widgetId]);
return screens.length > 0 ? widgetRegistry[widgetId][screens[0]] : null;
}
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace?.monitor)
return Hyprland.focusedWorkspace.monitor.name;
if (CompositorService.isNiri && NiriService.currentOutput)
return NiriService.currentOutput;
if (CompositorService.isSway) {
const focusedWs = I3.workspaces?.values?.find(ws => ws.focused === true);
return focusedWs?.monitor?.name || "";
}
return "";
}
function getRegisteredWidgetIds() {
return Object.keys(widgetRegistry);
}
function hasWidget(widgetId) {
return widgetRegistry[widgetId] && Object.keys(widgetRegistry[widgetId]).length > 0;
}
function triggerWidgetPopout(widgetId) {
const widget = getWidgetOnFocusedScreen(widgetId);
if (!widget)
return false;
if (typeof widget.triggerPopout === "function") {
widget.triggerPopout();
return true;
}
const signalMap = {
"battery": "toggleBatteryPopup",
"vpn": "toggleVpnPopup",
"layout": "toggleLayoutPopup",
"clock": "clockClicked",
"cpuUsage": "cpuClicked",
"memUsage": "ramClicked",
"cpuTemp": "cpuTempClicked",
"gpuTemp": "gpuTempClicked"
};
const signalName = signalMap[widgetId];
if (signalName && typeof widget[signalName] === "function") {
widget[signalName]();
return true;
}
if (typeof widget.clicked === "function") {
widget.clicked();
return true;
}
if (widget.popoutTarget?.toggle) {
widget.popoutTarget.toggle();
return true;
}
return false;
}
function getBarWindowForScreen(screenName) {
if (!dankBarRepeater)
return null;
for (var i = 0; i < dankBarRepeater.count; i++) {
const loader = dankBarRepeater.itemAt(i);
if (!loader?.item)
continue;
const barItem = loader.item;
if (!barItem.barVariants?.instances)
continue;
for (var j = 0; j < barItem.barVariants.instances.length; j++) {
const barInstance = barItem.barVariants.instances[j];
if (barInstance.modelData?.name === screenName)
return barInstance;
}
}
return null;
}
function getBarWindowOnFocusedScreen() {
const focusedScreen = getFocusedScreenName();
if (!focusedScreen)
return getFirstBarWindow();
return getBarWindowForScreen(focusedScreen) || getFirstBarWindow();
}
function getFirstBarWindow() {
if (!dankBarRepeater || dankBarRepeater.count === 0)
return null;
const loader = dankBarRepeater.itemAt(0);
if (!loader?.item)
return null;
const barItem = loader.item;
if (!barItem.barVariants?.instances || barItem.barVariants.instances.length === 0)
return null;
return barItem.barVariants.instances[0];
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,8 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io
import qs.Common
Singleton { Singleton {
id: root id: root
@@ -29,6 +26,9 @@ Singleton {
property string wifiConnectionUuid: activeService?.wifiConnectionUuid ?? "" property string wifiConnectionUuid: activeService?.wifiConnectionUuid ?? ""
property string wifiDevicePath: activeService?.wifiDevicePath ?? "" property string wifiDevicePath: activeService?.wifiDevicePath ?? ""
property string activeAccessPointPath: activeService?.activeAccessPointPath ?? "" property string activeAccessPointPath: activeService?.activeAccessPointPath ?? ""
property var wifiDevices: activeService?.wifiDevices ?? []
property string wifiDeviceOverride: activeService?.wifiDeviceOverride ?? ""
property string connectingDevice: activeService?.connectingDevice ?? ""
property string currentWifiSSID: activeService?.currentWifiSSID ?? "" property string currentWifiSSID: activeService?.currentWifiSSID ?? ""
property int wifiSignalStrength: activeService?.wifiSignalStrength ?? 0 property int wifiSignalStrength: activeService?.wifiSignalStrength ?? 0
@@ -70,6 +70,14 @@ Singleton {
property bool subscriptionConnected: activeService?.subscriptionConnected ?? false property bool subscriptionConnected: activeService?.subscriptionConnected ?? false
property var vpnProfiles: activeService?.vpnProfiles ?? []
property var vpnActive: activeService?.vpnActive ?? []
property bool vpnAvailable: activeService?.vpnAvailable ?? false
property bool vpnIsBusy: activeService?.vpnIsBusy ?? false
property bool vpnConnected: activeService?.vpnConnected ?? false
property string vpnActiveUuid: activeService?.activeUuid ?? ""
property string vpnActiveName: activeService?.activeName ?? ""
property string credentialsToken: activeService?.credentialsToken ?? "" property string credentialsToken: activeService?.credentialsToken ?? ""
property string credentialsSSID: activeService?.credentialsSSID ?? "" property string credentialsSSID: activeService?.credentialsSSID ?? ""
property string credentialsSetting: activeService?.credentialsSetting ?? "" property string credentialsSetting: activeService?.credentialsSetting ?? ""
@@ -88,12 +96,12 @@ Singleton {
readonly property string socketPath: Quickshell.env("DMS_SOCKET") readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Component.onCompleted: { Component.onCompleted: {
console.info("NetworkService: Initializing...") console.info("NetworkService: Initializing...");
if (!socketPath || socketPath.length === 0) { if (!socketPath || socketPath.length === 0) {
console.info("NetworkService: DMS_SOCKET not set, using LegacyNetworkService") console.info("NetworkService: DMS_SOCKET not set, using LegacyNetworkService");
useLegacyService() useLegacyService();
} else { } else {
console.log("NetworkService: DMS_SOCKET found, waiting for capabilities...") console.log("NetworkService: DMS_SOCKET found, waiting for capabilities...");
} }
} }
@@ -102,191 +110,197 @@ Singleton {
function onNetworkAvailableChanged() { function onNetworkAvailableChanged() {
if (!activeService && DMSNetworkService.networkAvailable) { if (!activeService && DMSNetworkService.networkAvailable) {
console.info("NetworkService: Network capability detected, using DMSNetworkService") console.info("NetworkService: Network capability detected, using DMSNetworkService");
activeService = DMSNetworkService activeService = DMSNetworkService;
usingLegacy = false usingLegacy = false;
console.info("NetworkService: Switched to DMSNetworkService, networkAvailable:", networkAvailable) console.info("NetworkService: Switched to DMSNetworkService, networkAvailable:", networkAvailable);
connectSignals() connectSignals();
} else if (!activeService && !DMSNetworkService.networkAvailable && socketPath && socketPath.length > 0) { } else if (!activeService && !DMSNetworkService.networkAvailable && socketPath && socketPath.length > 0) {
console.info("NetworkService: Network capability not available in DMS, using LegacyNetworkService") console.info("NetworkService: Network capability not available in DMS, using LegacyNetworkService");
useLegacyService() useLegacyService();
} }
} }
} }
function useLegacyService() { function useLegacyService() {
activeService = LegacyNetworkService activeService = LegacyNetworkService;
usingLegacy = true usingLegacy = true;
console.info("NetworkService: Switched to LegacyNetworkService, networkAvailable:", networkAvailable) console.info("NetworkService: Switched to LegacyNetworkService, networkAvailable:", networkAvailable);
if (LegacyNetworkService.activate) { if (LegacyNetworkService.activate) {
LegacyNetworkService.activate() LegacyNetworkService.activate();
} }
connectSignals() connectSignals();
} }
function connectSignals() { function connectSignals() {
if (activeService) { if (activeService) {
if (activeService.networksUpdated) { if (activeService.networksUpdated) {
activeService.networksUpdated.connect(root.networksUpdated) activeService.networksUpdated.connect(root.networksUpdated);
} }
if (activeService.connectionChanged) { if (activeService.connectionChanged) {
activeService.connectionChanged.connect(root.connectionChanged) activeService.connectionChanged.connect(root.connectionChanged);
} }
if (activeService.credentialsNeeded) { if (activeService.credentialsNeeded) {
activeService.credentialsNeeded.connect(root.credentialsNeeded) activeService.credentialsNeeded.connect(root.credentialsNeeded);
} }
} }
} }
function addRef() { function addRef() {
if (activeService && activeService.addRef) { if (activeService && activeService.addRef) {
activeService.addRef() activeService.addRef();
} }
} }
function removeRef() { function removeRef() {
if (activeService && activeService.removeRef) { if (activeService && activeService.removeRef) {
activeService.removeRef() activeService.removeRef();
} }
} }
function getState() { function getState() {
if (activeService && activeService.getState) { if (activeService && activeService.getState) {
activeService.getState() activeService.getState();
} }
} }
function scanWifi() { function scanWifi() {
if (activeService && activeService.scanWifi) { if (activeService && activeService.scanWifi) {
activeService.scanWifi() activeService.scanWifi();
} }
} }
function scanWifiNetworks() { function scanWifiNetworks() {
if (activeService && activeService.scanWifiNetworks) { if (activeService && activeService.scanWifiNetworks) {
activeService.scanWifiNetworks() activeService.scanWifiNetworks();
} }
} }
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") { function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
if (activeService && activeService.connectToWifi) { if (activeService && activeService.connectToWifi) {
activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch) activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch);
} }
} }
function disconnectWifi() { function disconnectWifi() {
if (activeService && activeService.disconnectWifi) { if (activeService && activeService.disconnectWifi) {
activeService.disconnectWifi() activeService.disconnectWifi();
} }
} }
function forgetWifiNetwork(ssid) { function forgetWifiNetwork(ssid) {
if (activeService && activeService.forgetWifiNetwork) { if (activeService && activeService.forgetWifiNetwork) {
activeService.forgetWifiNetwork(ssid) activeService.forgetWifiNetwork(ssid);
} }
} }
function toggleWifiRadio() { function toggleWifiRadio() {
if (activeService && activeService.toggleWifiRadio) { if (activeService && activeService.toggleWifiRadio) {
activeService.toggleWifiRadio() activeService.toggleWifiRadio();
} }
} }
function enableWifiDevice() { function enableWifiDevice() {
if (activeService && activeService.enableWifiDevice) { if (activeService && activeService.enableWifiDevice) {
activeService.enableWifiDevice() activeService.enableWifiDevice();
} }
} }
function setNetworkPreference(preference) { function setNetworkPreference(preference) {
if (activeService && activeService.setNetworkPreference) { if (activeService && activeService.setNetworkPreference) {
activeService.setNetworkPreference(preference) activeService.setNetworkPreference(preference);
} }
} }
function setConnectionPriority(type) { function setConnectionPriority(type) {
if (activeService && activeService.setConnectionPriority) { if (activeService && activeService.setConnectionPriority) {
activeService.setConnectionPriority(type) activeService.setConnectionPriority(type);
} }
} }
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") { function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
if (activeService && activeService.connectToWifiAndSetPreference) { if (activeService && activeService.connectToWifiAndSetPreference) {
activeService.connectToWifiAndSetPreference(ssid, password, username, anonymousIdentity, domainSuffixMatch) activeService.connectToWifiAndSetPreference(ssid, password, username, anonymousIdentity, domainSuffixMatch);
} }
} }
function toggleNetworkConnection(type) { function toggleNetworkConnection(type) {
if (activeService && activeService.toggleNetworkConnection) { if (activeService && activeService.toggleNetworkConnection) {
activeService.toggleNetworkConnection(type) activeService.toggleNetworkConnection(type);
} }
} }
function startAutoScan() { function startAutoScan() {
if (activeService && activeService.startAutoScan) { if (activeService && activeService.startAutoScan) {
activeService.startAutoScan() activeService.startAutoScan();
} }
} }
function stopAutoScan() { function stopAutoScan() {
if (activeService && activeService.stopAutoScan) { if (activeService && activeService.stopAutoScan) {
activeService.stopAutoScan() activeService.stopAutoScan();
} }
} }
function fetchNetworkInfo(ssid) { function fetchNetworkInfo(ssid) {
if (activeService && activeService.fetchNetworkInfo) { if (activeService && activeService.fetchNetworkInfo) {
activeService.fetchNetworkInfo(ssid) activeService.fetchNetworkInfo(ssid);
} }
} }
function fetchWiredNetworkInfo(uuid) { function fetchWiredNetworkInfo(uuid) {
if (activeService && activeService.fetchWiredNetworkInfo) { if (activeService && activeService.fetchWiredNetworkInfo) {
activeService.fetchWiredNetworkInfo(uuid) activeService.fetchWiredNetworkInfo(uuid);
} }
} }
function getNetworkInfo(ssid) { function getNetworkInfo(ssid) {
if (activeService && activeService.getNetworkInfo) { if (activeService && activeService.getNetworkInfo) {
return activeService.getNetworkInfo(ssid) return activeService.getNetworkInfo(ssid);
} }
return null return null;
} }
function getWiredNetworkInfo(uuid) { function getWiredNetworkInfo(uuid) {
if (activeService && activeService.getWiredNetworkInfo) { if (activeService && activeService.getWiredNetworkInfo) {
return activeService.getWiredNetworkInfo(uuid) return activeService.getWiredNetworkInfo(uuid);
} }
return null return null;
} }
function refreshNetworkState() { function refreshNetworkState() {
if (activeService && activeService.refreshNetworkState) { if (activeService && activeService.refreshNetworkState) {
activeService.refreshNetworkState() activeService.refreshNetworkState();
} }
} }
function connectToSpecificWiredConfig(uuid) { function connectToSpecificWiredConfig(uuid) {
if (activeService && activeService.connectToSpecificWiredConfig) { if (activeService && activeService.connectToSpecificWiredConfig) {
activeService.connectToSpecificWiredConfig(uuid) activeService.connectToSpecificWiredConfig(uuid);
} }
} }
function submitCredentials(token, secrets, save) { function submitCredentials(token, secrets, save) {
if (activeService && activeService.submitCredentials) { if (activeService && activeService.submitCredentials) {
activeService.submitCredentials(token, secrets, save) activeService.submitCredentials(token, secrets, save);
} }
} }
function cancelCredentials(token) { function cancelCredentials(token) {
if (activeService && activeService.cancelCredentials) { if (activeService && activeService.cancelCredentials) {
activeService.cancelCredentials(token) activeService.cancelCredentials(token);
} }
} }
function setWifiAutoconnect(ssid, autoconnect) { function setWifiAutoconnect(ssid, autoconnect) {
if (activeService && activeService.setWifiAutoconnect) { if (activeService && activeService.setWifiAutoconnect) {
activeService.setWifiAutoconnect(ssid, autoconnect) activeService.setWifiAutoconnect(ssid, autoconnect);
}
}
function setWifiDeviceOverride(deviceName) {
if (activeService && activeService.setWifiDeviceOverride) {
activeService.setWifiDeviceOverride(deviceName);
} }
} }
} }

View File

@@ -4,12 +4,21 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import qs.Common import Quickshell.Wayland
Singleton { Singleton {
id: root id: root
property bool cyclingActive: false property bool cyclingActive: false
readonly property bool anyFullscreen: {
if (!ToplevelManager.toplevels?.values)
return false;
for (const toplevel of ToplevelManager.toplevels.values) {
if (toplevel.fullscreen)
return true;
}
return false;
}
property string cachedCyclingTime: SessionData.wallpaperCyclingTime property string cachedCyclingTime: SessionData.wallpaperCyclingTime
property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval property int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: "" property string lastTimeCheck: ""
@@ -24,8 +33,8 @@ Singleton {
running: false running: false
repeat: true repeat: true
onTriggered: { onTriggered: {
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "") { if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.anyFullscreen) {
WallpaperCyclingService.cycleNextForMonitor(targetScreen) WallpaperCyclingService.cycleNextForMonitor(targetScreen);
} }
} }
} }
@@ -41,24 +50,26 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text && text.trim()) { if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0) const files = text.trim().split('\n').filter(file => file.length > 0);
if (files.length <= 1) return if (files.length <= 1)
const wallpaperList = files.sort() return;
const currentPath = currentWallpaper const wallpaperList = files.sort();
let currentIndex = wallpaperList.findIndex(path => path === currentPath) const currentPath = currentWallpaper;
if (currentIndex === -1) currentIndex = 0 let currentIndex = wallpaperList.findIndex(path => path === currentPath);
let targetIndex if (currentIndex === -1)
currentIndex = 0;
let targetIndex;
if (goToPrevious) { if (goToPrevious) {
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1 targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
} else { } else {
targetIndex = (currentIndex + 1) % wallpaperList.length targetIndex = (currentIndex + 1) % wallpaperList.length;
} }
const targetWallpaper = wallpaperList[targetIndex] const targetWallpaper = wallpaperList[targetIndex];
if (targetWallpaper && targetWallpaper !== currentPath) { if (targetWallpaper && targetWallpaper !== currentPath) {
if (targetScreenName) { if (targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper) SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper);
} else { } else {
SessionData.setWallpaper(targetWallpaper) SessionData.setWallpaper(targetWallpaper);
} }
} }
} }
@@ -71,205 +82,205 @@ Singleton {
target: SessionData target: SessionData
function onWallpaperCyclingEnabledChanged() { function onWallpaperCyclingEnabledChanged() {
updateCyclingState() updateCyclingState();
} }
function onWallpaperCyclingModeChanged() { function onWallpaperCyclingModeChanged() {
updateCyclingState() updateCyclingState();
} }
function onWallpaperCyclingIntervalChanged() { function onWallpaperCyclingIntervalChanged() {
cachedCyclingInterval = SessionData.wallpaperCyclingInterval cachedCyclingInterval = SessionData.wallpaperCyclingInterval;
if (SessionData.wallpaperCyclingMode === "interval") { if (SessionData.wallpaperCyclingMode === "interval") {
updateCyclingState() updateCyclingState();
} }
} }
function onWallpaperCyclingTimeChanged() { function onWallpaperCyclingTimeChanged() {
cachedCyclingTime = SessionData.wallpaperCyclingTime cachedCyclingTime = SessionData.wallpaperCyclingTime;
if (SessionData.wallpaperCyclingMode === "time") { if (SessionData.wallpaperCyclingMode === "time") {
updateCyclingState() updateCyclingState();
} }
} }
function onPerMonitorWallpaperChanged() { function onPerMonitorWallpaperChanged() {
updateCyclingState() updateCyclingState();
} }
function onMonitorCyclingSettingsChanged() { function onMonitorCyclingSettingsChanged() {
updateCyclingState() updateCyclingState();
} }
} }
function updateCyclingState() { function updateCyclingState() {
if (SessionData.perMonitorWallpaper) { if (SessionData.perMonitorWallpaper) {
stopCycling() stopCycling();
updatePerMonitorCycling() updatePerMonitorCycling();
} else if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) { } else if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
startCycling() startCycling();
stopAllMonitorCycling() stopAllMonitorCycling();
} else { } else {
stopCycling() stopCycling();
stopAllMonitorCycling() stopAllMonitorCycling();
} }
} }
function updatePerMonitorCycling() { function updatePerMonitorCycling() {
if (typeof Quickshell === "undefined") return if (typeof Quickshell === "undefined")
return;
var screens = Quickshell.screens var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) { for (var i = 0; i < screens.length; i++) {
var screenName = screens[i].name var screenName = screens[i].name;
var settings = SessionData.getMonitorCyclingSettings(screenName) var settings = SessionData.getMonitorCyclingSettings(screenName);
var wallpaper = SessionData.getMonitorWallpaper(screenName) var wallpaper = SessionData.getMonitorWallpaper(screenName);
if (settings.enabled && wallpaper && !wallpaper.startsWith("#")) { if (settings.enabled && wallpaper && !wallpaper.startsWith("#")) {
startMonitorCycling(screenName, settings) startMonitorCycling(screenName, settings);
} else { } else {
stopMonitorCycling(screenName) stopMonitorCycling(screenName);
} }
} }
} }
function stopAllMonitorCycling() { function stopAllMonitorCycling() {
var screenNames = Object.keys(monitorTimers) var screenNames = Object.keys(monitorTimers);
for (var i = 0; i < screenNames.length; i++) { for (var i = 0; i < screenNames.length; i++) {
stopMonitorCycling(screenNames[i]) stopMonitorCycling(screenNames[i]);
} }
} }
function startCycling() { function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") { if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000 intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.start() intervalTimer.start();
cyclingActive = true cyclingActive = true;
} else if (SessionData.wallpaperCyclingMode === "time") { } else if (SessionData.wallpaperCyclingMode === "time") {
cyclingActive = true cyclingActive = true;
checkTimeBasedCycling() checkTimeBasedCycling();
} }
} }
function stopCycling() { function stopCycling() {
intervalTimer.stop() intervalTimer.stop();
cyclingActive = false cyclingActive = false;
} }
function startMonitorCycling(screenName, settings) { function startMonitorCycling(screenName, settings) {
if (settings.mode === "interval") { if (settings.mode === "interval") {
var timer = monitorTimers[screenName] var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) { if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers) var newTimers = Object.assign({}, monitorTimers);
newTimers[screenName] = monitorTimerComponent.createObject(root) newTimers[screenName] = monitorTimerComponent.createObject(root);
newTimers[screenName].targetScreen = screenName newTimers[screenName].targetScreen = screenName;
monitorTimers = newTimers monitorTimers = newTimers;
timer = monitorTimers[screenName] timer = monitorTimers[screenName];
} }
if (timer) { if (timer) {
timer.interval = settings.interval * 1000 timer.interval = settings.interval * 1000;
timer.start() timer.start();
} }
} else if (settings.mode === "time") { } else if (settings.mode === "time") {
var newChecks = Object.assign({}, monitorLastTimeChecks) var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = "" newChecks[screenName] = "";
monitorLastTimeChecks = newChecks monitorLastTimeChecks = newChecks;
} }
} }
function stopMonitorCycling(screenName) { function stopMonitorCycling(screenName) {
var timer = monitorTimers[screenName] var timer = monitorTimers[screenName];
if (timer) { if (timer) {
timer.stop() timer.stop();
timer.destroy() timer.destroy();
var newTimers = Object.assign({}, monitorTimers) var newTimers = Object.assign({}, monitorTimers);
delete newTimers[screenName] delete newTimers[screenName];
monitorTimers = newTimers monitorTimers = newTimers;
} }
var process = monitorProcesses[screenName] var process = monitorProcesses[screenName];
if (process) { if (process) {
process.destroy() process.destroy();
var newProcesses = Object.assign({}, monitorProcesses) var newProcesses = Object.assign({}, monitorProcesses);
delete newProcesses[screenName] delete newProcesses[screenName];
monitorProcesses = newProcesses monitorProcesses = newProcesses;
} }
var newChecks = Object.assign({}, monitorLastTimeChecks) var newChecks = Object.assign({}, monitorLastTimeChecks);
delete newChecks[screenName] delete newChecks[screenName];
monitorLastTimeChecks = newChecks monitorLastTimeChecks = newChecks;
} }
function cycleToNextWallpaper(screenName, wallpaperPath) { function cycleToNextWallpaper(screenName, wallpaperPath) {
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
if (!currentWallpaper) return if (!currentWallpaper)
return;
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) { if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
// Use per-monitor process // Use per-monitor process
var process = monitorProcesses[screenName] var process = monitorProcesses[screenName];
if (!process) { if (!process) {
var newProcesses = Object.assign({}, monitorProcesses) var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root) newProcesses[screenName] = monitorProcessComponent.createObject(root);
monitorProcesses = newProcesses monitorProcesses = newProcesses;
process = monitorProcesses[screenName] process = monitorProcesses[screenName];
} }
if (process) { if (process) {
process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`];
process.targetScreenName = screenName process.targetScreenName = screenName;
process.currentWallpaper = currentWallpaper process.currentWallpaper = currentWallpaper;
process.goToPrevious = false process.goToPrevious = false;
process.running = true process.running = true;
} }
} else { } else {
// Use global process for fallback // Use global process for fallback
cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] cyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`];
cyclingProcess.targetScreenName = screenName || "" cyclingProcess.targetScreenName = screenName || "";
cyclingProcess.currentWallpaper = currentWallpaper cyclingProcess.currentWallpaper = currentWallpaper;
cyclingProcess.running = true cyclingProcess.running = true;
} }
} }
function cycleToPrevWallpaper(screenName, wallpaperPath) { function cycleToPrevWallpaper(screenName, wallpaperPath) {
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
if (!currentWallpaper) return if (!currentWallpaper)
return;
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/')) const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) { if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
// Use per-monitor process (same as next, but with prev flag) // Use per-monitor process (same as next, but with prev flag)
var process = monitorProcesses[screenName] var process = monitorProcesses[screenName];
if (!process) { if (!process) {
var newProcesses = Object.assign({}, monitorProcesses) var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root) newProcesses[screenName] = monitorProcessComponent.createObject(root);
monitorProcesses = newProcesses monitorProcesses = newProcesses;
process = monitorProcesses[screenName] process = monitorProcesses[screenName];
} }
if (process) { if (process) {
process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] process.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`];
process.targetScreenName = screenName process.targetScreenName = screenName;
process.currentWallpaper = currentWallpaper process.currentWallpaper = currentWallpaper;
process.goToPrevious = true process.goToPrevious = true;
process.running = true process.running = true;
} }
} else { } else {
// Use global process for fallback // Use global process for fallback
prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`] prevCyclingProcess.command = ["sh", "-c", `ls -1 "${wallpaperDir}"/*.jpg "${wallpaperDir}"/*.jpeg "${wallpaperDir}"/*.png "${wallpaperDir}"/*.bmp "${wallpaperDir}"/*.gif "${wallpaperDir}"/*.webp 2>/dev/null | sort`];
prevCyclingProcess.targetScreenName = screenName || "" prevCyclingProcess.targetScreenName = screenName || "";
prevCyclingProcess.currentWallpaper = currentWallpaper prevCyclingProcess.currentWallpaper = currentWallpaper;
prevCyclingProcess.running = true prevCyclingProcess.running = true;
} }
} }
function cycleNextManually() { function cycleNextManually() {
if (SessionData.wallpaperPath) { if (SessionData.wallpaperPath) {
cycleToNextWallpaper() cycleToNextWallpaper();
// Restart timers if cycling is active // Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) { if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") { if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000 intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.restart() intervalTimer.restart();
} }
} }
} }
@@ -277,71 +288,73 @@ Singleton {
function cyclePrevManually() { function cyclePrevManually() {
if (SessionData.wallpaperPath) { if (SessionData.wallpaperPath) {
cycleToPrevWallpaper() cycleToPrevWallpaper();
// Restart timers if cycling is active // Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) { if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") { if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000 intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.restart() intervalTimer.restart();
} }
} }
} }
} }
function cycleNextForMonitor(screenName) { function cycleNextForMonitor(screenName) {
if (!screenName) return if (!screenName)
return;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
if (currentWallpaper) { if (currentWallpaper) {
cycleToNextWallpaper(screenName, currentWallpaper) cycleToNextWallpaper(screenName, currentWallpaper);
} }
} }
function cyclePrevForMonitor(screenName) { function cyclePrevForMonitor(screenName) {
if (!screenName) return if (!screenName)
return;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName) var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
if (currentWallpaper) { if (currentWallpaper) {
cycleToPrevWallpaper(screenName, currentWallpaper) cycleToPrevWallpaper(screenName, currentWallpaper);
} }
} }
function checkTimeBasedCycling() { function checkTimeBasedCycling() {
const currentTime = Qt.formatTime(systemClock.date, "hh:mm") if (anyFullscreen)
return;
const currentTime = Qt.formatTime(systemClock.date, "hh:mm");
if (!SessionData.perMonitorWallpaper) { if (!SessionData.perMonitorWallpaper) {
if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) { if (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) {
lastTimeCheck = currentTime lastTimeCheck = currentTime;
cycleToNextWallpaper() cycleToNextWallpaper();
} else if (currentTime !== cachedCyclingTime) { } else if (currentTime !== cachedCyclingTime) {
lastTimeCheck = "" lastTimeCheck = "";
} }
} else { } else {
checkPerMonitorTimeBasedCycling(currentTime) checkPerMonitorTimeBasedCycling(currentTime);
} }
} }
function checkPerMonitorTimeBasedCycling(currentTime) { function checkPerMonitorTimeBasedCycling(currentTime) {
if (typeof Quickshell === "undefined") return if (typeof Quickshell === "undefined")
return;
var screens = Quickshell.screens var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) { for (var i = 0; i < screens.length; i++) {
var screenName = screens[i].name var screenName = screens[i].name;
var settings = SessionData.getMonitorCyclingSettings(screenName) var settings = SessionData.getMonitorCyclingSettings(screenName);
var wallpaper = SessionData.getMonitorWallpaper(screenName) var wallpaper = SessionData.getMonitorWallpaper(screenName);
if (settings.enabled && settings.mode === "time" && wallpaper && !wallpaper.startsWith("#")) { if (settings.enabled && settings.mode === "time" && wallpaper && !wallpaper.startsWith("#")) {
var lastCheck = monitorLastTimeChecks[screenName] || "" var lastCheck = monitorLastTimeChecks[screenName] || "";
if (currentTime === settings.time && currentTime !== lastCheck) { if (currentTime === settings.time && currentTime !== lastCheck) {
var newChecks = Object.assign({}, monitorLastTimeChecks) var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = currentTime newChecks[screenName] = currentTime;
monitorLastTimeChecks = newChecks monitorLastTimeChecks = newChecks;
cycleNextForMonitor(screenName) cycleNextForMonitor(screenName);
} else if (currentTime !== settings.time) { } else if (currentTime !== settings.time) {
var newChecks = Object.assign({}, monitorLastTimeChecks) var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = "" newChecks[screenName] = "";
monitorLastTimeChecks = newChecks monitorLastTimeChecks = newChecks;
} }
} }
} }
@@ -352,7 +365,11 @@ Singleton {
interval: cachedCyclingInterval * 1000 interval: cachedCyclingInterval * 1000
running: false running: false
repeat: true repeat: true
onTriggered: cycleToNextWallpaper() onTriggered: {
if (anyFullscreen)
return;
cycleToNextWallpaper();
}
} }
SystemClock { SystemClock {
@@ -360,7 +377,7 @@ Singleton {
precision: SystemClock.Minutes precision: SystemClock.Minutes
onDateChanged: { onDateChanged: {
if ((SessionData.wallpaperCyclingMode === "time" && cyclingActive) || SessionData.perMonitorWallpaper) { if ((SessionData.wallpaperCyclingMode === "time" && cyclingActive) || SessionData.perMonitorWallpaper) {
checkTimeBasedCycling() checkTimeBasedCycling();
} }
} }
} }
@@ -376,22 +393,23 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text && text.trim()) { if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0) const files = text.trim().split('\n').filter(file => file.length > 0);
if (files.length <= 1) return if (files.length <= 1)
return;
const wallpaperList = files.sort();
const currentPath = cyclingProcess.currentWallpaper;
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
if (currentIndex === -1)
currentIndex = 0;
const wallpaperList = files.sort() const nextIndex = (currentIndex + 1) % wallpaperList.length;
const currentPath = cyclingProcess.currentWallpaper const nextWallpaper = wallpaperList[nextIndex];
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
const nextIndex = (currentIndex + 1) % wallpaperList.length
const nextWallpaper = wallpaperList[nextIndex]
if (nextWallpaper && nextWallpaper !== currentPath) { if (nextWallpaper && nextWallpaper !== currentPath) {
if (cyclingProcess.targetScreenName) { if (cyclingProcess.targetScreenName) {
SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper) SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper);
} else { } else {
SessionData.setWallpaper(nextWallpaper) SessionData.setWallpaper(nextWallpaper);
} }
} }
} }
@@ -410,27 +428,27 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
if (text && text.trim()) { if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0) const files = text.trim().split('\n').filter(file => file.length > 0);
if (files.length <= 1) return if (files.length <= 1)
return;
const wallpaperList = files.sort();
const currentPath = prevCyclingProcess.currentWallpaper;
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
if (currentIndex === -1)
currentIndex = 0;
const wallpaperList = files.sort() const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
const currentPath = prevCyclingProcess.currentWallpaper const prevWallpaper = wallpaperList[prevIndex];
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1
const prevWallpaper = wallpaperList[prevIndex]
if (prevWallpaper && prevWallpaper !== currentPath) { if (prevWallpaper && prevWallpaper !== currentPath) {
if (prevCyclingProcess.targetScreenName) { if (prevCyclingProcess.targetScreenName) {
SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper) SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper);
} else { } else {
SessionData.setWallpaper(prevWallpaper) SessionData.setWallpaper(prevWallpaper);
} }
} }
} }
} }
} }
} }
} }

View File

@@ -12,6 +12,8 @@ Item {
property string layerNamespace: "dms:popout" property string layerNamespace: "dms:popout"
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader property alias contentLoader: contentLoader
property Component overlayContent: null
property alias overlayLoader: overlayLoader
property real popupWidth: 400 property real popupWidth: 400
property real popupHeight: 300 property real popupHeight: 300
property real triggerX: 0 property real triggerX: 0
@@ -243,6 +245,13 @@ Item {
backgroundClicked(); backgroundClicked();
} }
} }
Loader {
id: overlayLoader
anchors.fill: parent
active: root.overlayContent !== null && backgroundWindow.visible
sourceComponent: root.overlayContent
}
} }
PanelWindow { PanelWindow {