1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-01-30 16:32:50 -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
interfaces:
DBusConn:
github.com/AvengeMedia/danklinux/internal/server/network:
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
config:
dir: "internal/mocks/network"
outpkg: mocks_network

View File

@@ -509,6 +509,52 @@ func (_c *MockBackend_DisconnectWiFi_Call) RunAndReturn(run func() error) *MockB
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
func (_m *MockBackend) ForgetWiFiNetwork(ssid string) error {
ret := _m.Called(ssid)
@@ -659,6 +705,53 @@ func (_c *MockBackend_GetPromptBroker_Call) RunAndReturn(run func() network.Prom
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
func (_m *MockBackend) GetWiFiEnabled() (bool, error) {
ret := _m.Called()
@@ -1091,6 +1184,52 @@ func (_c *MockBackend_ScanWiFi_Call) RunAndReturn(run func() error) *MockBackend
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
func (_m *MockBackend) SetPromptBroker(broker network.PromptBroker) error {
ret := _m.Called(broker)

View File

@@ -8,10 +8,13 @@ type Backend interface {
SetWiFiEnabled(enabled bool) error
ScanWiFi() error
ScanWiFiDevice(device string) error
GetWiFiNetworkDetails(ssid string) (*NetworkInfoResponse, error)
GetWiFiDevices() []WiFiDevice
ConnectWiFi(req ConnectionRequest) error
DisconnectWiFi() error
DisconnectWiFiDevice(device string) error
ForgetWiFiNetwork(ssid string) error
SetWiFiAutoconnect(ssid string, autoconnect bool) error
@@ -54,11 +57,13 @@ type BackendState struct {
WiFiBSSID string
WiFiSignal uint8
WiFiNetworks []WiFiNetwork
WiFiDevices []WiFiDevice
WiredConnections []WiredConnection
VPNProfiles []VPNProfile
VPNActive []VPNActive
IsConnecting bool
ConnectingSSID string
ConnectingDevice string
IsConnectingVPN bool
ConnectingVPNUUID 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 {
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) {
b.stateMutex.RLock()
defer b.stateMutex.RUnlock()
state := *b.state
state.WiFiNetworks = append([]WiFiNetwork(nil), b.state.WiFiNetworks...)
state.WiredConnections = append([]WiredConnection(nil), b.state.WiredConnections...)
state.WiFiDevices = b.getWiFiDevicesLocked()
return &state, nil
}

View File

@@ -45,3 +45,38 @@ func (b *IWDBackend) DisconnectAllVPN() error {
func (b *IWDBackend) ClearVPNCredentials(uuidOrName string) error {
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 {
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
)
type wifiDeviceInfo struct {
device gonetworkmanager.Device
wireless gonetworkmanager.DeviceWireless
name string
hwAddress string
}
type NetworkManagerBackend struct {
nmConn interface{}
ethernetDevice interface{}
wifiDevice interface{}
settings interface{}
wifiDev interface{}
wifiDevices map[string]*wifiDeviceInfo
dbusConn *dbus.Conn
signals chan *dbus.Signal
@@ -71,8 +79,9 @@ func NewNetworkManagerBackend(nmConn ...gonetworkmanager.NetworkManager) (*Netwo
}
backend := &NetworkManagerBackend{
nmConn: nm,
stopChan: make(chan struct{}),
nmConn: nm,
stopChan: make(chan struct{}),
wifiDevices: make(map[string]*wifiDeviceInfo),
state: &BackendState{
Backend: "networkmanager",
},
@@ -114,27 +123,48 @@ func (b *NetworkManagerBackend) Initialize() error {
}
case gonetworkmanager.NmDeviceTypeWifi:
b.wifiDevice = dev
if w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath()); 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 {
iface, err := dev.GetPropertyInterface()
if err != nil {
continue
}
if wifiEnabled {
if _, err := b.updateWiFiNetworks(); err != nil {
log.Warnf("Failed to get initial networks: %v", err)
}
w, err := gonetworkmanager.NewDeviceWireless(dev.GetPath())
if err != nil {
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 {
return err
}
@@ -165,6 +195,7 @@ func (b *NetworkManagerBackend) GetCurrentState() (*BackendState, error) {
state := *b.state
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.VPNProfiles = append([]VPNProfile(nil), b.state.VPNProfiles...)
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 {
if b.wifiDevice == nil {
return fmt.Errorf("no WiFi device available")
devInfo, err := b.getWifiDeviceForConnection(req.Device)
if err != nil {
return err
}
b.stateMutex.RLock()
alreadyConnected := b.state.WiFiConnected && b.state.WiFiSSID == req.SSID
b.stateMutex.RUnlock()
if alreadyConnected && !req.Interactive {
if alreadyConnected && !req.Interactive && req.Device == "" {
return nil
}
b.stateMutex.Lock()
b.state.IsConnecting = true
b.state.ConnectingSSID = req.SSID
b.state.ConnectingDevice = req.Device
b.state.LastError = ""
b.stateMutex.Unlock()
@@ -223,14 +225,13 @@ func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
existingConn, err := b.findConnection(req.SSID)
if err == nil && existingConn != nil {
dev := b.wifiDevice.(gonetworkmanager.Device)
_, err := nm.ActivateConnection(existingConn, dev, nil)
_, err := nm.ActivateConnection(existingConn, devInfo.device, nil)
if err != nil {
log.Warnf("[ConnectWiFi] Failed to activate existing connection: %v", err)
b.stateMutex.Lock()
b.state.IsConnecting = false
b.state.ConnectingSSID = ""
b.state.ConnectingDevice = ""
b.state.LastError = fmt.Sprintf("failed to activate connection: %v", err)
b.stateMutex.Unlock()
if b.onStateChange != nil {
@@ -242,11 +243,12 @@ func (b *NetworkManagerBackend) ConnectWiFi(req ConnectionRequest) error {
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)
b.stateMutex.Lock()
b.state.IsConnecting = false
b.state.ConnectingSSID = ""
b.state.ConnectingDevice = ""
b.state.LastError = err.Error()
b.stateMutex.Unlock()
if b.onStateChange != nil {
@@ -502,19 +504,17 @@ func (b *NetworkManagerBackend) findConnection(ssid string) (gonetworkmanager.Co
}
func (b *NetworkManagerBackend) createAndConnectWiFi(req ConnectionRequest) error {
if b.wifiDevice == nil {
return fmt.Errorf("no WiFi device available")
}
nm := b.nmConn.(gonetworkmanager.NetworkManager)
dev := b.wifiDevice.(gonetworkmanager.Device)
if err := b.ensureWiFiDevice(); err != nil {
devInfo, err := b.getWifiDeviceForConnection(req.Device)
if err != nil {
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()
if err != nil {
return fmt.Errorf("failed to get access points: %w", err)
@@ -716,3 +716,254 @@ func (b *NetworkManagerBackend) SetWiFiAutoconnect(ssid string, autoconnect bool
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.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.state.WiFiConnected = true
backend.state.WiFiSSID = "TestNetwork"
backend.state.WiFiDevice = "wlan0"
backend.stateMutex.Unlock()
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) {
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())
return
}
@@ -163,6 +170,9 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
if username, ok := req.Params["username"].(string); ok {
connReq.Username = username
}
if device, ok := req.Params["device"].(string); ok {
connReq.Device = device
}
if interactive, ok := req.Params["interactive"].(bool); ok {
connReq.Interactive = interactive
@@ -170,7 +180,7 @@ func handleConnectWiFi(conn net.Conn, req Request, manager *Manager) {
state := manager.GetState()
alreadyConnected := state.WiFiConnected && state.WiFiSSID == ssid
if alreadyConnected {
if alreadyConnected && connReq.Device == "" {
connReq.Interactive = false
} else {
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) {
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())
return
}

View File

@@ -117,11 +117,13 @@ func (m *Manager) syncStateFromBackend() error {
m.state.WiFiBSSID = backendState.WiFiBSSID
m.state.WiFiSignal = backendState.WiFiSignal
m.state.WiFiNetworks = backendState.WiFiNetworks
m.state.WiFiDevices = backendState.WiFiDevices
m.state.WiredConnections = backendState.WiredConnections
m.state.VPNProfiles = backendState.VPNProfiles
m.state.VPNActive = backendState.VPNActive
m.state.IsConnecting = backendState.IsConnecting
m.state.ConnectingSSID = backendState.ConnectingSSID
m.state.ConnectingDevice = backendState.ConnectingDevice
m.state.LastError = backendState.LastError
m.stateMutex.Unlock()
@@ -151,6 +153,7 @@ func (m *Manager) snapshotState() NetworkState {
defer m.stateMutex.RUnlock()
s := *m.state
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.VPNProfiles = append([]VPNProfile(nil), m.state.VPNProfiles...)
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) {
return true
}
if len(old.WiFiDevices) != len(new.WiFiDevices) {
return true
}
if len(old.WiredConnections) != len(new.WiredConnections) {
return true
}
@@ -505,3 +511,19 @@ func (m *Manager) ClearVPNCredentials(uuidOrName string) error {
func (m *Manager) SetWiFiAutoconnect(ssid string, autoconnect bool) error {
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"`
Rate uint32 `json:"rate"`
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 {
@@ -76,11 +89,13 @@ type NetworkState struct {
WiFiBSSID string `json:"wifiBSSID"`
WiFiSignal uint8 `json:"wifiSignal"`
WiFiNetworks []WiFiNetwork `json:"wifiNetworks"`
WiFiDevices []WiFiDevice `json:"wifiDevices"`
WiredConnections []WiredConnection `json:"wiredConnections"`
VPNProfiles []VPNProfile `json:"vpnProfiles"`
VPNActive []VPNActive `json:"vpnActive"`
IsConnecting bool `json:"isConnecting"`
ConnectingSSID string `json:"connectingSSID"`
ConnectingDevice string `json:"connectingDevice,omitempty"`
LastError string `json:"lastError"`
}
@@ -91,6 +106,7 @@ type ConnectionRequest struct {
AnonymousIdentity string `json:"anonymousIdentity,omitempty"`
DomainSuffixMatch string `json:"domainSuffixMatch,omitempty"`
Interactive bool `json:"interactive,omitempty"`
Device string `json:"device,omitempty"`
}
type WiredConnection struct {

View File

@@ -31,7 +31,7 @@ import (
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
)
const APIVersion = 19
const APIVersion = 20
type Capabilities struct {
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("Network:")
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.connect - Connect to WiFi (params: ssid, password?, username?)")
log.Info(" network.wifi.disconnect - Disconnect WiFi")
log.Info(" network.wifi.connect - Connect to WiFi (params: ssid, password?, username?, device?)")
log.Info(" network.wifi.disconnect - Disconnect WiFi (params: device?)")
log.Info(" network.wifi.forget - Forget network (params: ssid)")
log.Info(" network.wifi.toggle - Toggle WiFi radio")
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 controlCenterShowBluetoothIcon: 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 privacyShowMicIcon: false
property bool privacyShowCameraIcon: false

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -155,35 +155,56 @@ Loader {
}
onLoaded: {
if (item) {
contentItemReady(item);
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical;
} catch (e) {}
}
if (!item)
return;
if (item.pluginService !== undefined) {
var parts = widgetId.split(":");
var pluginId = parts[0];
var variantId = parts.length > 1 ? parts[1] : null;
contentItemReady(item);
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;
}
if (axis && "isVertical" in item) {
try {
item.isVertical = axis.isVertical;
} catch (e) {}
}
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) {

View File

@@ -10,9 +10,138 @@ BasePill {
property bool isActive: false
property var popoutTarget: null
property var widgetData: null
property string screenName: ""
property string screenModel: ""
property bool showNetworkIcon: SettingsData.controlCenterShowNetworkIcon
property bool showBluetoothIcon: SettingsData.controlCenterShowBluetoothIcon
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 {
Item {
@@ -26,34 +155,21 @@ BasePill {
spacing: Theme.spacingXS
DankIcon {
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
}
name: root.getNetworkIconName()
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
color: root.getNetworkIconColor()
anchors.horizontalCenter: parent.horizontalCenter
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 {
name: "bluetooth"
size: Theme.barIconSize(root.barThickness)
@@ -71,19 +187,7 @@ BasePill {
DankIcon {
id: audioIconV
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"
}
name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor
anchors.centerIn: parent
@@ -92,31 +196,85 @@ BasePill {
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0
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
onWheel: function (wheelEvent) {
root.handleVolumeWheel(wheelEvent.angleDelta.y);
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 {
name: "settings"
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.widgetIconColor
anchors.horizontalCenter: parent.horizontalCenter
visible: !root.showNetworkIcon && !root.showBluetoothIcon && !root.showAudioIcon
visible: root.hasNoVisibleIcons()
}
}
@@ -128,38 +286,24 @@ BasePill {
DankIcon {
id: networkIcon
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
}
name: root.getNetworkIconName()
size: Theme.barIconSize(root.barThickness)
color: {
if (NetworkService.wifiToggling) {
return Theme.primary
}
return NetworkService.networkStatus !== "disconnected" ? Theme.primary : Theme.outlineButton
}
color: root.getNetworkIconColor()
anchors.verticalCenter: parent.verticalCenter
visible: root.showNetworkIcon && NetworkService.networkAvailable
}
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"
size: Theme.barIconSize(root.barThickness)
color: BluetoothService.connected ? Theme.primary : Theme.outlineButton
@@ -176,19 +320,7 @@ BasePill {
DankIcon {
id: audioIcon
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";
}
name: root.getVolumeIconName()
size: Theme.barIconSize(root.barThickness)
color: Theme.widgetIconColor
anchors.centerIn: parent
@@ -196,34 +328,83 @@ BasePill {
MouseArea {
id: audioWheelArea
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
let delta = wheelEvent.angleDelta.y;
let currentVolume = (AudioService.sink && AudioService.sink.audio && AudioService.sink.audio.volume * 100) || 0;
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();
}
onWheel: function (wheelEvent) {
root.handleVolumeWheel(wheelEvent.angleDelta.y);
wheelEvent.accepted = true;
}
}
}
Rectangle {
width: micIcon.implicitWidth + 4
height: micIcon.implicitHeight + 4
color: "transparent"
anchors.verticalCenter: parent.verticalCenter
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;
}
}
}
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)
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
visible: false
visible: root.showPrinterIcon && CupsService.cupsAvailable && root.hasPrintJobs()
}
DankIcon {
@@ -231,9 +412,15 @@ BasePill {
size: Theme.barIconSize(root.barThickness)
color: root.isActive ? Theme.primary : Theme.widgetIconColor
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.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Services.Mpris
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
import qs.Modules.DankDash
DankPopout {
id: root
@@ -27,42 +22,130 @@ DankPopout {
property bool __focusArmed: 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() {
if (!__focusArmed)
return
const win = root.window
return;
const win = root.window;
if (!win || !win.visible)
return
return;
if (!contentLoader.item)
return
return;
if (win.requestActivate)
win.requestActivate()
contentLoader.item.forceActiveFocus(Qt.TabFocusReason)
win.requestActivate();
contentLoader.item.forceActiveFocus(Qt.TabFocusReason);
if (contentLoader.item.activeFocus)
__focusArmed = false
__focusArmed = false;
}
onDashVisibleChanged: {
if (dashVisible) {
__focusArmed = true
__contentReady = !!contentLoader.item
open()
__tryFocusOnce()
__focusArmed = true;
__contentReady = !!contentLoader.item;
open();
__tryFocusOnce();
} else {
__focusArmed = false
__contentReady = false
close()
__focusArmed = false;
__contentReady = false;
__hideDropdowns();
close();
}
}
Connections {
target: contentLoader
function onLoaded() {
__contentReady = true
__contentReady = true;
if (__focusArmed)
__tryFocusOnce()
__tryFocusOnce();
}
}
@@ -71,12 +154,12 @@ DankPopout {
enabled: !!root.window
function onVisibleChanged() {
if (__focusArmed)
__tryFocusOnce()
__tryFocusOnce();
}
}
onBackgroundClicked: {
dashVisible = false
dashVisible = false;
}
content: Component {
@@ -90,7 +173,7 @@ DankPopout {
Component.onCompleted: {
if (root.shouldBeVisible) {
mainContainer.forceActiveFocus()
mainContainer.forceActiveFocus();
}
}
@@ -99,54 +182,54 @@ DankPopout {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) {
Qt.callLater(function () {
mainContainer.forceActiveFocus()
})
mainContainer.forceActiveFocus();
});
}
}
}
Keys.onPressed: function (event) {
if (event.key === Qt.Key_Escape) {
root.dashVisible = false
event.accepted = true
return
root.dashVisible = false;
event.accepted = true;
return;
}
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) {
nextIndex++
nextIndex++;
}
if (nextIndex >= tabBar.model.length) {
nextIndex = 0
nextIndex = 0;
}
root.currentTabIndex = nextIndex
event.accepted = true
return
root.currentTabIndex = nextIndex;
event.accepted = true;
return;
}
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) {
prevIndex--
prevIndex--;
}
if (prevIndex < 0) {
prevIndex = tabBar.model.length - 1
prevIndex = tabBar.model.length - 1;
while (prevIndex >= 0 && tabBar.model[prevIndex] && tabBar.model[prevIndex].isAction) {
prevIndex--
prevIndex--;
}
}
if (prevIndex >= 0) {
root.currentTabIndex = prevIndex
root.currentTabIndex = prevIndex;
}
event.accepted = true
return
event.accepted = true;
return;
}
if (root.currentTabIndex === 2 && wallpaperTab.handleKeyEvent) {
if (wallpaperTab.handleKeyEvent(event)) {
event.accepted = true
return
event.accepted = true;
return;
}
}
}
@@ -171,50 +254,54 @@ DankPopout {
focus: false
activeFocusOnTab: false
nextFocusTarget: {
const item = pages.currentItem
const item = pages.currentItem;
if (!item)
return null
return null;
if (item.focusTarget)
return item.focusTarget
return item
return item.focusTarget;
return item;
}
model: {
let tabs = [{
"icon": "dashboard",
"text": I18n.tr("Overview")
}, {
"icon": "music_note",
"text": I18n.tr("Media")
}, {
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}]
let tabs = [
{
"icon": "dashboard",
"text": I18n.tr("Overview")
},
{
"icon": "music_note",
"text": I18n.tr("Media")
},
{
"icon": "wallpaper",
"text": I18n.tr("Wallpapers")
}
];
if (SettingsData.weatherEnabled) {
tabs.push({
"icon": "wb_sunny",
"text": I18n.tr("Weather")
})
"icon": "wb_sunny",
"text": I18n.tr("Weather")
});
}
tabs.push({
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
})
return tabs
"icon": "settings",
"text": I18n.tr("Settings"),
"isAction": true
});
return tabs;
}
onTabClicked: function (index) {
root.currentTabIndex = index
root.currentTabIndex = index;
}
onActionTriggered: function (index) {
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3
let settingsIndex = SettingsData.weatherEnabled ? 4 : 3;
if (index === settingsIndex) {
dashVisible = false
settingsModal.show()
dashVisible = false;
settingsModal.show();
}
}
}
@@ -229,14 +316,14 @@ DankPopout {
width: parent.width
implicitHeight: {
if (currentIndex === 0)
return overviewTab.implicitHeight
return overviewTab.implicitHeight;
if (currentIndex === 1)
return mediaTab.implicitHeight
return mediaTab.implicitHeight;
if (currentIndex === 2)
return wallpaperTab.implicitHeight
return wallpaperTab.implicitHeight;
if (SettingsData.weatherEnabled && currentIndex === 3)
return weatherTab.implicitHeight
return overviewTab.implicitHeight
return weatherTab.implicitHeight;
return overviewTab.implicitHeight;
}
currentIndex: root.currentTabIndex
@@ -244,24 +331,42 @@ DankPopout {
id: overviewTab
onCloseDash: {
root.dashVisible = false
root.dashVisible = false;
}
onSwitchToWeatherTab: {
if (SettingsData.weatherEnabled) {
tabBar.currentIndex = 3
tabBar.tabClicked(3)
tabBar.currentIndex = 3;
tabBar.tabClicked(3);
}
}
onSwitchToMediaTab: {
tabBar.currentIndex = 1
tabBar.tabClicked(1)
tabBar.currentIndex = 1;
tabBar.tabClicked(1);
}
}
MediaPlayerTab {
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 {

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.Controls
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Services.Mpris
import Quickshell.Services.Pipewire
import Quickshell.Io
import Quickshell
import qs.Common
@@ -15,14 +13,42 @@ Item {
property MprisPlayer activePlayer: MprisController.activePlayer
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 {
id: sharedTooltip
}
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 usePlayerVolume: activePlayer && activePlayer.volumeSupported
readonly property bool __isChromeBrowser: {
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)
// Palette that stays stable across track switches until new colors are ready
@@ -334,383 +360,6 @@ Item {
anchors.fill: parent
clip: false
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 {
x: 72
y: 20
@@ -1051,14 +700,12 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 100
visible: (allPlayers?.length || 0) >= 1
property bool playersExpanded: false
DankIcon {
anchors.centerIn: parent
name: "assistant_device"
@@ -1072,14 +719,20 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
parent.playersExpanded = !parent.playersExpanded;
}
onEntered: {
sharedTooltip.show("Media Players", playerSelectorButton, 0, 0, isRightEdge ? "right" : "left");
}
onExited: {
sharedTooltip.hide();
if (playersExpanded) {
hideDropdowns();
return;
}
hideDropdowns();
playersExpanded = true;
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
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 101
enabled: volumeAvailable
property bool volumeExpanded: false
property real previousVolume: 0.0
Timer {
id: volumeHideTimer
interval: 500
onTriggered: volumeButton.volumeExpanded = false
}
DankIcon {
anchors.centerIn: parent
name: getVolumeIcon()
@@ -1118,11 +764,19 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onEntered: {
volumeButton.volumeExpanded = true;
volumeHideTimer.stop();
if (volumeExpanded)
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: {
volumeHideTimer.restart();
if (volumeExpanded)
volumeButtonExited();
}
onClicked: {
if (currentVolume > 0) {
@@ -1142,22 +796,15 @@ Item {
}
}
onWheel: wheelEvent => {
let delta = wheelEvent.angleDelta.y;
let current = (currentVolume * 100) || 0;
let newVolume;
if (delta > 0) {
newVolume = Math.min(100, current + 5);
} else {
newVolume = Math.max(0, current - 5);
}
const delta = wheelEvent.angleDelta.y;
const current = (currentVolume * 100) || 0;
const newVolume = delta > 0 ? Math.min(100, current + 5) : Math.max(0, current - 5);
if (usePlayerVolume) {
activePlayer.volume = newVolume / 100;
} else if (AudioService.sink?.audio) {
AudioService.sink.audio.volume = newVolume / 100;
}
volumeButton.volumeExpanded = true;
wheelEvent.accepted = true;
}
}
@@ -1170,16 +817,14 @@ Item {
radius: 20
x: isRightEdge ? Theme.spacingM : parent.width - 40 - Theme.spacingM
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.width: 1
z: 100
property bool devicesExpanded: false
DankIcon {
anchors.centerIn: parent
name: parent.devicesExpanded ? "expand_less" : "speaker"
name: devicesExpanded ? "expand_less" : "speaker"
size: 18
color: Theme.surfaceText
}
@@ -1190,247 +835,20 @@ Item {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
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: {
sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left");
}
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
onEntered: sharedTooltip.show("Output Device", audioDevicesButton, 0, 0, isRightEdge ? "right" : "left")
onExited: sharedTooltip.hide()
}
}
}

View File

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

View File

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

View File

@@ -784,13 +784,31 @@ Item {
}
function handleControlCenterSettingChanged(sectionId, widgetIndex, settingName, value) {
// Control Center settings are global, not per-widget instance
if (settingName === "showNetworkIcon") {
SettingsData.set("controlCenterShowNetworkIcon", value);
} else if (settingName === "showBluetoothIcon") {
SettingsData.set("controlCenterShowBluetoothIcon", value);
} else if (settingName === "showAudioIcon") {
SettingsData.set("controlCenterShowAudioIcon", value);
switch (settingName) {
case "showNetworkIcon":
SettingsData.set("controlCenterShowNetworkIcon", value)
break
case "showBluetoothIcon":
SettingsData.set("controlCenterShowBluetoothIcon", 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 {
id: ccMenuButton
visible: modelData.id === "controlCenterButton"
buttonSize: 32
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
console.log("Control Center three-dot button clicked for widget:", modelData.id);
controlCenterContextMenu.widgetData = modelData;
controlCenterContextMenu.sectionId = root.sectionId;
controlCenterContextMenu.widgetIndex = index;
// Position relative to the action buttons row, not the specific button
var parentPos = parent.mapToItem(root, 0, 0);
controlCenterContextMenu.x = parentPos.x - 210; // Position to the left with margin
controlCenterContextMenu.y = parentPos.y - 10; // Slightly above
var buttonPos = ccMenuButton.mapToItem(root, 0, 0);
var popupWidth = controlCenterContextMenu.width;
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();
}
}
DankActionButton {
id: privacyMenuButton
visible: modelData.id === "privacyIndicator"
buttonSize: 32
iconName: "more_vert"
iconSize: 18
iconColor: Theme.outline
onClicked: {
console.log("Privacy three-dot button clicked for widget:", modelData.id);
privacyContextMenu.widgetData = modelData;
privacyContextMenu.sectionId = root.sectionId;
privacyContextMenu.widgetIndex = index;
// Position relative to the action buttons row, not the specific button
var parentPos = parent.mapToItem(root, 0, 0);
privacyContextMenu.x = parentPos.x - 210; // Position to the left with margin
privacyContextMenu.y = parentPos.y - 10; // Slightly above
var buttonPos = privacyMenuButton.mapToItem(root, 0, 0);
var popupWidth = privacyContextMenu.width;
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();
}
}
@@ -699,21 +729,13 @@ Column {
property string sectionId: ""
property int widgetIndex: -1
width: 200
height: 120
width: 220
height: menuColumn.implicitHeight + Theme.spacingS * 2
padding: 0
modal: true
focus: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onOpened: {
console.log("Control Center context menu opened");
}
onClosed: {
console.log("Control Center context menu closed");
}
background: Rectangle {
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
radius: Theme.cornerRadius
@@ -722,168 +744,117 @@ Column {
}
contentItem: Item {
Column {
id: menuColumn
anchors.fill: parent
anchors.margins: Theme.spacingS
spacing: 2
Rectangle {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
Repeater {
model: [
{
icon: "lan",
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 {
anchors.left: parent.left
anchors.leftMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
delegate: Rectangle {
required property var modelData
required property int index
DankIcon {
name: "lan"
size: 16
color: Theme.surfaceText
width: menuColumn.width
height: 32
radius: Theme.cornerRadius
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
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 {
text: I18n.tr("Network Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
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
DankToggle {
id: toggle
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
width: 40
height: 20
checked: modelData.checked
onToggled: {
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggled);
}
}
StyledText {
text: I18n.tr("Bluetooth Icon")
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
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);
MouseArea {
id: toggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: {
toggle.checked = !toggle.checked;
root.controlCenterSettingChanged(controlCenterContextMenu.sectionId, controlCenterContextMenu.widgetIndex, modelData.setting, toggle.checked);
}
}
}
}
@@ -932,7 +903,7 @@ Column {
width: parent.width
height: 32
radius: Theme.cornerRadius
color: networkToggleArea.containsMouse ? Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) : "transparent"
color: "transparent"
Row {
anchors.left: parent.left
@@ -954,7 +925,7 @@ Column {
width: parent.width
height: 32
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 {
anchors.left: parent.left
@@ -1006,7 +977,7 @@ Column {
width: parent.width
height: 32
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 {
anchors.left: parent.left
@@ -1058,7 +1029,7 @@ Column {
width: parent.width
height: 32
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 {
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 ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Common
Singleton {
id: root
@@ -29,6 +26,9 @@ Singleton {
property string wifiConnectionUuid: activeService?.wifiConnectionUuid ?? ""
property string wifiDevicePath: activeService?.wifiDevicePath ?? ""
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 int wifiSignalStrength: activeService?.wifiSignalStrength ?? 0
@@ -70,6 +70,14 @@ Singleton {
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 credentialsSSID: activeService?.credentialsSSID ?? ""
property string credentialsSetting: activeService?.credentialsSetting ?? ""
@@ -88,12 +96,12 @@ Singleton {
readonly property string socketPath: Quickshell.env("DMS_SOCKET")
Component.onCompleted: {
console.info("NetworkService: Initializing...")
console.info("NetworkService: Initializing...");
if (!socketPath || socketPath.length === 0) {
console.info("NetworkService: DMS_SOCKET not set, using LegacyNetworkService")
useLegacyService()
console.info("NetworkService: DMS_SOCKET not set, using LegacyNetworkService");
useLegacyService();
} 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() {
if (!activeService && DMSNetworkService.networkAvailable) {
console.info("NetworkService: Network capability detected, using DMSNetworkService")
activeService = DMSNetworkService
usingLegacy = false
console.info("NetworkService: Switched to DMSNetworkService, networkAvailable:", networkAvailable)
connectSignals()
console.info("NetworkService: Network capability detected, using DMSNetworkService");
activeService = DMSNetworkService;
usingLegacy = false;
console.info("NetworkService: Switched to DMSNetworkService, networkAvailable:", networkAvailable);
connectSignals();
} else if (!activeService && !DMSNetworkService.networkAvailable && socketPath && socketPath.length > 0) {
console.info("NetworkService: Network capability not available in DMS, using LegacyNetworkService")
useLegacyService()
console.info("NetworkService: Network capability not available in DMS, using LegacyNetworkService");
useLegacyService();
}
}
}
function useLegacyService() {
activeService = LegacyNetworkService
usingLegacy = true
console.info("NetworkService: Switched to LegacyNetworkService, networkAvailable:", networkAvailable)
activeService = LegacyNetworkService;
usingLegacy = true;
console.info("NetworkService: Switched to LegacyNetworkService, networkAvailable:", networkAvailable);
if (LegacyNetworkService.activate) {
LegacyNetworkService.activate()
LegacyNetworkService.activate();
}
connectSignals()
connectSignals();
}
function connectSignals() {
if (activeService) {
if (activeService.networksUpdated) {
activeService.networksUpdated.connect(root.networksUpdated)
activeService.networksUpdated.connect(root.networksUpdated);
}
if (activeService.connectionChanged) {
activeService.connectionChanged.connect(root.connectionChanged)
activeService.connectionChanged.connect(root.connectionChanged);
}
if (activeService.credentialsNeeded) {
activeService.credentialsNeeded.connect(root.credentialsNeeded)
activeService.credentialsNeeded.connect(root.credentialsNeeded);
}
}
}
function addRef() {
if (activeService && activeService.addRef) {
activeService.addRef()
activeService.addRef();
}
}
function removeRef() {
if (activeService && activeService.removeRef) {
activeService.removeRef()
activeService.removeRef();
}
}
function getState() {
if (activeService && activeService.getState) {
activeService.getState()
activeService.getState();
}
}
function scanWifi() {
if (activeService && activeService.scanWifi) {
activeService.scanWifi()
activeService.scanWifi();
}
}
function scanWifiNetworks() {
if (activeService && activeService.scanWifiNetworks) {
activeService.scanWifiNetworks()
activeService.scanWifiNetworks();
}
}
function connectToWifi(ssid, password = "", username = "", anonymousIdentity = "", domainSuffixMatch = "") {
if (activeService && activeService.connectToWifi) {
activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch)
activeService.connectToWifi(ssid, password, username, anonymousIdentity, domainSuffixMatch);
}
}
function disconnectWifi() {
if (activeService && activeService.disconnectWifi) {
activeService.disconnectWifi()
activeService.disconnectWifi();
}
}
function forgetWifiNetwork(ssid) {
if (activeService && activeService.forgetWifiNetwork) {
activeService.forgetWifiNetwork(ssid)
activeService.forgetWifiNetwork(ssid);
}
}
function toggleWifiRadio() {
if (activeService && activeService.toggleWifiRadio) {
activeService.toggleWifiRadio()
activeService.toggleWifiRadio();
}
}
function enableWifiDevice() {
if (activeService && activeService.enableWifiDevice) {
activeService.enableWifiDevice()
activeService.enableWifiDevice();
}
}
function setNetworkPreference(preference) {
if (activeService && activeService.setNetworkPreference) {
activeService.setNetworkPreference(preference)
activeService.setNetworkPreference(preference);
}
}
function setConnectionPriority(type) {
if (activeService && activeService.setConnectionPriority) {
activeService.setConnectionPriority(type)
activeService.setConnectionPriority(type);
}
}
function connectToWifiAndSetPreference(ssid, password, username = "", anonymousIdentity = "", domainSuffixMatch = "") {
if (activeService && activeService.connectToWifiAndSetPreference) {
activeService.connectToWifiAndSetPreference(ssid, password, username, anonymousIdentity, domainSuffixMatch)
activeService.connectToWifiAndSetPreference(ssid, password, username, anonymousIdentity, domainSuffixMatch);
}
}
function toggleNetworkConnection(type) {
if (activeService && activeService.toggleNetworkConnection) {
activeService.toggleNetworkConnection(type)
activeService.toggleNetworkConnection(type);
}
}
function startAutoScan() {
if (activeService && activeService.startAutoScan) {
activeService.startAutoScan()
activeService.startAutoScan();
}
}
function stopAutoScan() {
if (activeService && activeService.stopAutoScan) {
activeService.stopAutoScan()
activeService.stopAutoScan();
}
}
function fetchNetworkInfo(ssid) {
if (activeService && activeService.fetchNetworkInfo) {
activeService.fetchNetworkInfo(ssid)
activeService.fetchNetworkInfo(ssid);
}
}
function fetchWiredNetworkInfo(uuid) {
if (activeService && activeService.fetchWiredNetworkInfo) {
activeService.fetchWiredNetworkInfo(uuid)
activeService.fetchWiredNetworkInfo(uuid);
}
}
function getNetworkInfo(ssid) {
if (activeService && activeService.getNetworkInfo) {
return activeService.getNetworkInfo(ssid)
return activeService.getNetworkInfo(ssid);
}
return null
return null;
}
function getWiredNetworkInfo(uuid) {
if (activeService && activeService.getWiredNetworkInfo) {
return activeService.getWiredNetworkInfo(uuid)
return activeService.getWiredNetworkInfo(uuid);
}
return null
return null;
}
function refreshNetworkState() {
if (activeService && activeService.refreshNetworkState) {
activeService.refreshNetworkState()
activeService.refreshNetworkState();
}
}
function connectToSpecificWiredConfig(uuid) {
if (activeService && activeService.connectToSpecificWiredConfig) {
activeService.connectToSpecificWiredConfig(uuid)
activeService.connectToSpecificWiredConfig(uuid);
}
}
function submitCredentials(token, secrets, save) {
if (activeService && activeService.submitCredentials) {
activeService.submitCredentials(token, secrets, save)
activeService.submitCredentials(token, secrets, save);
}
}
function cancelCredentials(token) {
if (activeService && activeService.cancelCredentials) {
activeService.cancelCredentials(token)
activeService.cancelCredentials(token);
}
}
function setWifiAutoconnect(ssid, autoconnect) {
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 Quickshell
import Quickshell.Io
import qs.Common
import Quickshell.Wayland
Singleton {
id: root
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 int cachedCyclingInterval: SessionData.wallpaperCyclingInterval
property string lastTimeCheck: ""
@@ -24,8 +33,8 @@ Singleton {
running: false
repeat: true
onTriggered: {
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "") {
WallpaperCyclingService.cycleNextForMonitor(targetScreen)
if (typeof WallpaperCyclingService !== "undefined" && targetScreen !== "" && !WallpaperCyclingService.anyFullscreen) {
WallpaperCyclingService.cycleNextForMonitor(targetScreen);
}
}
}
@@ -41,24 +50,26 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0)
if (files.length <= 1) return
const wallpaperList = files.sort()
const currentPath = currentWallpaper
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
let targetIndex
const files = text.trim().split('\n').filter(file => file.length > 0);
if (files.length <= 1)
return;
const wallpaperList = files.sort();
const currentPath = currentWallpaper;
let currentIndex = wallpaperList.findIndex(path => path === currentPath);
if (currentIndex === -1)
currentIndex = 0;
let targetIndex;
if (goToPrevious) {
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1
targetIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
} else {
targetIndex = (currentIndex + 1) % wallpaperList.length
targetIndex = (currentIndex + 1) % wallpaperList.length;
}
const targetWallpaper = wallpaperList[targetIndex]
const targetWallpaper = wallpaperList[targetIndex];
if (targetWallpaper && targetWallpaper !== currentPath) {
if (targetScreenName) {
SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper)
SessionData.setMonitorWallpaper(targetScreenName, targetWallpaper);
} else {
SessionData.setWallpaper(targetWallpaper)
SessionData.setWallpaper(targetWallpaper);
}
}
}
@@ -71,205 +82,205 @@ Singleton {
target: SessionData
function onWallpaperCyclingEnabledChanged() {
updateCyclingState()
updateCyclingState();
}
function onWallpaperCyclingModeChanged() {
updateCyclingState()
updateCyclingState();
}
function onWallpaperCyclingIntervalChanged() {
cachedCyclingInterval = SessionData.wallpaperCyclingInterval
cachedCyclingInterval = SessionData.wallpaperCyclingInterval;
if (SessionData.wallpaperCyclingMode === "interval") {
updateCyclingState()
updateCyclingState();
}
}
function onWallpaperCyclingTimeChanged() {
cachedCyclingTime = SessionData.wallpaperCyclingTime
cachedCyclingTime = SessionData.wallpaperCyclingTime;
if (SessionData.wallpaperCyclingMode === "time") {
updateCyclingState()
updateCyclingState();
}
}
function onPerMonitorWallpaperChanged() {
updateCyclingState()
updateCyclingState();
}
function onMonitorCyclingSettingsChanged() {
updateCyclingState()
updateCyclingState();
}
}
function updateCyclingState() {
if (SessionData.perMonitorWallpaper) {
stopCycling()
updatePerMonitorCycling()
stopCycling();
updatePerMonitorCycling();
} else if (SessionData.wallpaperCyclingEnabled && SessionData.wallpaperPath) {
startCycling()
stopAllMonitorCycling()
startCycling();
stopAllMonitorCycling();
} else {
stopCycling()
stopAllMonitorCycling()
stopCycling();
stopAllMonitorCycling();
}
}
function updatePerMonitorCycling() {
if (typeof Quickshell === "undefined") return
var screens = Quickshell.screens
if (typeof Quickshell === "undefined")
return;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
var screenName = screens[i].name
var settings = SessionData.getMonitorCyclingSettings(screenName)
var wallpaper = SessionData.getMonitorWallpaper(screenName)
var screenName = screens[i].name;
var settings = SessionData.getMonitorCyclingSettings(screenName);
var wallpaper = SessionData.getMonitorWallpaper(screenName);
if (settings.enabled && wallpaper && !wallpaper.startsWith("#")) {
startMonitorCycling(screenName, settings)
startMonitorCycling(screenName, settings);
} else {
stopMonitorCycling(screenName)
stopMonitorCycling(screenName);
}
}
}
function stopAllMonitorCycling() {
var screenNames = Object.keys(monitorTimers)
var screenNames = Object.keys(monitorTimers);
for (var i = 0; i < screenNames.length; i++) {
stopMonitorCycling(screenNames[i])
stopMonitorCycling(screenNames[i]);
}
}
function startCycling() {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.start()
cyclingActive = true
intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.start();
cyclingActive = true;
} else if (SessionData.wallpaperCyclingMode === "time") {
cyclingActive = true
checkTimeBasedCycling()
cyclingActive = true;
checkTimeBasedCycling();
}
}
function stopCycling() {
intervalTimer.stop()
cyclingActive = false
intervalTimer.stop();
cyclingActive = false;
}
function startMonitorCycling(screenName, settings) {
if (settings.mode === "interval") {
var timer = monitorTimers[screenName]
var timer = monitorTimers[screenName];
if (!timer && monitorTimerComponent && monitorTimerComponent.status === Component.Ready) {
var newTimers = Object.assign({}, monitorTimers)
newTimers[screenName] = monitorTimerComponent.createObject(root)
newTimers[screenName].targetScreen = screenName
monitorTimers = newTimers
timer = monitorTimers[screenName]
var newTimers = Object.assign({}, monitorTimers);
newTimers[screenName] = monitorTimerComponent.createObject(root);
newTimers[screenName].targetScreen = screenName;
monitorTimers = newTimers;
timer = monitorTimers[screenName];
}
if (timer) {
timer.interval = settings.interval * 1000
timer.start()
timer.interval = settings.interval * 1000;
timer.start();
}
} else if (settings.mode === "time") {
var newChecks = Object.assign({}, monitorLastTimeChecks)
newChecks[screenName] = ""
monitorLastTimeChecks = newChecks
var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = "";
monitorLastTimeChecks = newChecks;
}
}
function stopMonitorCycling(screenName) {
var timer = monitorTimers[screenName]
var timer = monitorTimers[screenName];
if (timer) {
timer.stop()
timer.destroy()
var newTimers = Object.assign({}, monitorTimers)
delete newTimers[screenName]
monitorTimers = newTimers
timer.stop();
timer.destroy();
var newTimers = Object.assign({}, monitorTimers);
delete newTimers[screenName];
monitorTimers = newTimers;
}
var process = monitorProcesses[screenName]
var process = monitorProcesses[screenName];
if (process) {
process.destroy()
var newProcesses = Object.assign({}, monitorProcesses)
delete newProcesses[screenName]
monitorProcesses = newProcesses
process.destroy();
var newProcesses = Object.assign({}, monitorProcesses);
delete newProcesses[screenName];
monitorProcesses = newProcesses;
}
var newChecks = Object.assign({}, monitorLastTimeChecks)
delete newChecks[screenName]
monitorLastTimeChecks = newChecks
var newChecks = Object.assign({}, monitorLastTimeChecks);
delete newChecks[screenName];
monitorLastTimeChecks = newChecks;
}
function cycleToNextWallpaper(screenName, wallpaperPath) {
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath
if (!currentWallpaper) return
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
if (!currentWallpaper)
return;
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
// Use per-monitor process
var process = monitorProcesses[screenName]
var process = monitorProcesses[screenName];
if (!process) {
var newProcesses = Object.assign({}, monitorProcesses)
newProcesses[screenName] = monitorProcessComponent.createObject(root)
monitorProcesses = newProcesses
process = monitorProcesses[screenName]
var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root);
monitorProcesses = newProcesses;
process = monitorProcesses[screenName];
}
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.targetScreenName = screenName
process.currentWallpaper = currentWallpaper
process.goToPrevious = false
process.running = true
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.currentWallpaper = currentWallpaper;
process.goToPrevious = false;
process.running = true;
}
} else {
// 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.targetScreenName = screenName || ""
cyclingProcess.currentWallpaper = currentWallpaper
cyclingProcess.running = true
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.currentWallpaper = currentWallpaper;
cyclingProcess.running = true;
}
}
function cycleToPrevWallpaper(screenName, wallpaperPath) {
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath
if (!currentWallpaper) return
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'))
const currentWallpaper = wallpaperPath || SessionData.wallpaperPath;
if (!currentWallpaper)
return;
const wallpaperDir = currentWallpaper.substring(0, currentWallpaper.lastIndexOf('/'));
if (screenName && monitorProcessComponent && monitorProcessComponent.status === Component.Ready) {
// Use per-monitor process (same as next, but with prev flag)
var process = monitorProcesses[screenName]
var process = monitorProcesses[screenName];
if (!process) {
var newProcesses = Object.assign({}, monitorProcesses)
newProcesses[screenName] = monitorProcessComponent.createObject(root)
monitorProcesses = newProcesses
process = monitorProcesses[screenName]
var newProcesses = Object.assign({}, monitorProcesses);
newProcesses[screenName] = monitorProcessComponent.createObject(root);
monitorProcesses = newProcesses;
process = monitorProcesses[screenName];
}
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.targetScreenName = screenName
process.currentWallpaper = currentWallpaper
process.goToPrevious = true
process.running = true
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.currentWallpaper = currentWallpaper;
process.goToPrevious = true;
process.running = true;
}
} else {
// 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.targetScreenName = screenName || ""
prevCyclingProcess.currentWallpaper = currentWallpaper
prevCyclingProcess.running = true
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.currentWallpaper = currentWallpaper;
prevCyclingProcess.running = true;
}
}
function cycleNextManually() {
if (SessionData.wallpaperPath) {
cycleToNextWallpaper()
cycleToNextWallpaper();
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.restart();
}
}
}
@@ -277,71 +288,73 @@ Singleton {
function cyclePrevManually() {
if (SessionData.wallpaperPath) {
cycleToPrevWallpaper()
cycleToPrevWallpaper();
// Restart timers if cycling is active
if (cyclingActive && SessionData.wallpaperCyclingEnabled) {
if (SessionData.wallpaperCyclingMode === "interval") {
intervalTimer.interval = cachedCyclingInterval * 1000
intervalTimer.restart()
intervalTimer.interval = cachedCyclingInterval * 1000;
intervalTimer.restart();
}
}
}
}
function cycleNextForMonitor(screenName) {
if (!screenName) return
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (!screenName)
return;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
if (currentWallpaper) {
cycleToNextWallpaper(screenName, currentWallpaper)
cycleToNextWallpaper(screenName, currentWallpaper);
}
}
function cyclePrevForMonitor(screenName) {
if (!screenName) return
var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
if (!screenName)
return;
var currentWallpaper = SessionData.getMonitorWallpaper(screenName);
if (currentWallpaper) {
cycleToPrevWallpaper(screenName, currentWallpaper)
cycleToPrevWallpaper(screenName, currentWallpaper);
}
}
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 (currentTime === cachedCyclingTime && currentTime !== lastTimeCheck) {
lastTimeCheck = currentTime
cycleToNextWallpaper()
lastTimeCheck = currentTime;
cycleToNextWallpaper();
} else if (currentTime !== cachedCyclingTime) {
lastTimeCheck = ""
lastTimeCheck = "";
}
} else {
checkPerMonitorTimeBasedCycling(currentTime)
checkPerMonitorTimeBasedCycling(currentTime);
}
}
function checkPerMonitorTimeBasedCycling(currentTime) {
if (typeof Quickshell === "undefined") return
var screens = Quickshell.screens
if (typeof Quickshell === "undefined")
return;
var screens = Quickshell.screens;
for (var i = 0; i < screens.length; i++) {
var screenName = screens[i].name
var settings = SessionData.getMonitorCyclingSettings(screenName)
var wallpaper = SessionData.getMonitorWallpaper(screenName)
var screenName = screens[i].name;
var settings = SessionData.getMonitorCyclingSettings(screenName);
var wallpaper = SessionData.getMonitorWallpaper(screenName);
if (settings.enabled && settings.mode === "time" && wallpaper && !wallpaper.startsWith("#")) {
var lastCheck = monitorLastTimeChecks[screenName] || ""
var lastCheck = monitorLastTimeChecks[screenName] || "";
if (currentTime === settings.time && currentTime !== lastCheck) {
var newChecks = Object.assign({}, monitorLastTimeChecks)
newChecks[screenName] = currentTime
monitorLastTimeChecks = newChecks
cycleNextForMonitor(screenName)
var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = currentTime;
monitorLastTimeChecks = newChecks;
cycleNextForMonitor(screenName);
} else if (currentTime !== settings.time) {
var newChecks = Object.assign({}, monitorLastTimeChecks)
newChecks[screenName] = ""
monitorLastTimeChecks = newChecks
var newChecks = Object.assign({}, monitorLastTimeChecks);
newChecks[screenName] = "";
monitorLastTimeChecks = newChecks;
}
}
}
@@ -352,7 +365,11 @@ Singleton {
interval: cachedCyclingInterval * 1000
running: false
repeat: true
onTriggered: cycleToNextWallpaper()
onTriggered: {
if (anyFullscreen)
return;
cycleToNextWallpaper();
}
}
SystemClock {
@@ -360,7 +377,7 @@ Singleton {
precision: SystemClock.Minutes
onDateChanged: {
if ((SessionData.wallpaperCyclingMode === "time" && cyclingActive) || SessionData.perMonitorWallpaper) {
checkTimeBasedCycling()
checkTimeBasedCycling();
}
}
}
@@ -376,22 +393,23 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0)
if (files.length <= 1) return
const files = text.trim().split('\n').filter(file => file.length > 0);
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 currentPath = cyclingProcess.currentWallpaper
let currentIndex = wallpaperList.findIndex(path => path === currentPath)
if (currentIndex === -1) currentIndex = 0
const nextIndex = (currentIndex + 1) % wallpaperList.length
const nextWallpaper = wallpaperList[nextIndex]
const nextIndex = (currentIndex + 1) % wallpaperList.length;
const nextWallpaper = wallpaperList[nextIndex];
if (nextWallpaper && nextWallpaper !== currentPath) {
if (cyclingProcess.targetScreenName) {
SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper)
SessionData.setMonitorWallpaper(cyclingProcess.targetScreenName, nextWallpaper);
} else {
SessionData.setWallpaper(nextWallpaper)
SessionData.setWallpaper(nextWallpaper);
}
}
}
@@ -410,27 +428,27 @@ Singleton {
stdout: StdioCollector {
onStreamFinished: {
if (text && text.trim()) {
const files = text.trim().split('\n').filter(file => file.length > 0)
if (files.length <= 1) return
const files = text.trim().split('\n').filter(file => file.length > 0);
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 currentPath = prevCyclingProcess.currentWallpaper
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]
const prevIndex = currentIndex === 0 ? wallpaperList.length - 1 : currentIndex - 1;
const prevWallpaper = wallpaperList[prevIndex];
if (prevWallpaper && prevWallpaper !== currentPath) {
if (prevCyclingProcess.targetScreenName) {
SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper)
SessionData.setMonitorWallpaper(prevCyclingProcess.targetScreenName, prevWallpaper);
} else {
SessionData.setWallpaper(prevWallpaper)
SessionData.setWallpaper(prevWallpaper);
}
}
}
}
}
}
}

View File

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