mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-05-08 21:42:05 -04:00
Compare commits
75 Commits
2b7fd36322
...
anims
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62bf9c6efe | ||
|
|
61a77bd186 | ||
|
|
e04c919d78 | ||
|
|
246b6c44b0 | ||
|
|
847ddf7d38 | ||
|
|
16e8199f9e | ||
|
|
7d1519f546 | ||
|
|
1bf66ee482 | ||
|
|
39a43f4de5 | ||
|
|
971a511edb | ||
|
|
0f8e0bc2b4 | ||
|
|
537c44e354 | ||
|
|
db53a9a719 | ||
|
|
f4a10de790 | ||
|
|
8c9fe84d02 | ||
|
|
f0fcc77bdb | ||
|
|
cf4c4b7d69 | ||
|
|
7bb8499353 | ||
|
|
ee1a2bc7de | ||
|
|
20d383d4ab | ||
|
|
9cb0d8baf2 | ||
|
|
362ded3bc9 | ||
|
|
654f2ec7ad | ||
|
|
3600e034b8 | ||
|
|
d7c501e175 | ||
|
|
b9e9da579f | ||
|
|
7bea6b4a62 | ||
|
|
ab211266a6 | ||
|
|
4da22a4345 | ||
|
|
fbc1ff62c7 | ||
|
|
1fe72e1a66 | ||
|
|
f82d7610e3 | ||
|
|
bd6ad53875 | ||
|
|
5d09acca4c | ||
|
|
b4e7c4a4cd | ||
|
|
a6269084c0 | ||
|
|
8271d8423d | ||
|
|
c76e29c457 | ||
|
|
4750a7553b | ||
|
|
60786921a9 | ||
|
|
751bbcc127 | ||
|
|
58e8dd5456 | ||
|
|
1586c25847 | ||
|
|
cded5a7948 | ||
|
|
6238e065f2 | ||
|
|
72fbbfdd0d | ||
|
|
2796c1cd4d | ||
|
|
54c9886627 | ||
|
|
05713cb389 | ||
|
|
8bb3ee5f18 | ||
|
|
bc0b4825f1 | ||
|
|
ef7f17abf4 | ||
|
|
876cd21f0b | ||
|
|
5c92d49873 | ||
|
|
da47b573be | ||
|
|
2f04be8778 | ||
|
|
69178ddfd8 | ||
|
|
a310f6fff0 | ||
|
|
7474abe286 | ||
|
|
df2ba3a3c6 | ||
|
|
e536456236 | ||
|
|
8d77122da3 | ||
|
|
fb66effa51 | ||
|
|
5052e71c31 | ||
|
|
bfc78d16ca | ||
|
|
c425e3562b | ||
|
|
1f26092aa9 | ||
|
|
2849bb96f4 | ||
|
|
7b749f2a4c | ||
|
|
8803c94ce0 | ||
|
|
f5235c943b | ||
|
|
59fec889b5 | ||
|
|
f42f04a807 | ||
|
|
51f6f37925 | ||
|
|
9651a4ca98 |
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
8
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -7,7 +7,7 @@ body:
|
||||
attributes:
|
||||
value: |
|
||||
## DankMaterialShell Bug Report
|
||||
Limit your report to one issue per submission unless closely related
|
||||
Limit your report to one issue per submission unless similarly related
|
||||
- type: dropdown
|
||||
id: compositor
|
||||
attributes:
|
||||
@@ -53,9 +53,9 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: original_installation_method_different
|
||||
id: original_installation_method
|
||||
attributes:
|
||||
label: Was your original Installation method different?
|
||||
label: Was this your original Installation method?
|
||||
options:
|
||||
- "Yes"
|
||||
- No (specify below)
|
||||
@@ -73,7 +73,7 @@ body:
|
||||
id: dms_doctor
|
||||
attributes:
|
||||
label: dms doctor -vC
|
||||
description: Output of `dms doctor -vC` command — paste between the lines below to keep it collapsed in the issue
|
||||
description: Output of `dms doctor -vC` command — paste between the details tags below to keep it collapsed in the issue
|
||||
placeholder: Paste the output of `dms doctor -vC` here
|
||||
value: |
|
||||
<details>
|
||||
|
||||
13
.github/workflows/run-obs.yml
vendored
13
.github/workflows/run-obs.yml
vendored
@@ -9,8 +9,8 @@ on:
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-git
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
@@ -119,9 +119,8 @@ jobs:
|
||||
echo "🔄 Manual rebuild requested: $PKG (db$REBUILD)"
|
||||
|
||||
elif [[ "$PKG" == "all" ]]; then
|
||||
# Check each package and build list of those needing updates
|
||||
# Check each stable package and build list of those needing updates
|
||||
PACKAGES_TO_UPDATE=()
|
||||
check_dms_git && PACKAGES_TO_UPDATE+=("dms-git")
|
||||
if check_dms_stable; then
|
||||
PACKAGES_TO_UPDATE+=("dms")
|
||||
if [[ -n "$LATEST_TAG" ]]; then
|
||||
@@ -140,7 +139,7 @@ jobs:
|
||||
else
|
||||
echo "packages=" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=false" >> $GITHUB_OUTPUT
|
||||
echo "✓ All packages up to date"
|
||||
echo "✓ Both packages up to date"
|
||||
fi
|
||||
|
||||
elif [[ "$PKG" == "dms-git" ]]; then
|
||||
@@ -245,7 +244,7 @@ jobs:
|
||||
fi
|
||||
|
||||
- name: Update dms-git spec version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -266,7 +265,7 @@ jobs:
|
||||
} > distro/opensuse/dms-git.spec
|
||||
|
||||
- name: Update Debian dms-git changelog version
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git') || steps.packages.outputs.packages == 'all'
|
||||
if: contains(steps.packages.outputs.packages, 'dms-git')
|
||||
run: |
|
||||
COMMIT_HASH=$(git rev-parse --short=8 HEAD)
|
||||
COMMIT_COUNT=$(git rev-list --count HEAD)
|
||||
@@ -389,7 +388,7 @@ jobs:
|
||||
UPLOADED_PACKAGES=()
|
||||
SKIPPED_PACKAGES=()
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms dms-greeter" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
echo ""
|
||||
|
||||
16
.github/workflows/run-ppa.yml
vendored
16
.github/workflows/run-ppa.yml
vendored
@@ -4,9 +4,15 @@ on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
package:
|
||||
description: "Package to upload (dms, dms-git, dms-greeter, or all)"
|
||||
required: false
|
||||
default: "dms-git"
|
||||
description: "Package to upload"
|
||||
required: true
|
||||
type: choice
|
||||
options:
|
||||
- dms
|
||||
- dms-greeter
|
||||
- dms-git
|
||||
- all
|
||||
default: "dms"
|
||||
rebuild_release:
|
||||
description: "Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)"
|
||||
required: false
|
||||
@@ -139,7 +145,7 @@ jobs:
|
||||
fi
|
||||
else
|
||||
# Fallback
|
||||
echo "packages=dms-git" >> $GITHUB_OUTPUT
|
||||
echo "packages=dms" >> $GITHUB_OUTPUT
|
||||
echo "has_updates=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
@@ -209,7 +215,7 @@ jobs:
|
||||
echo "✓ Using rebuild release number: ppa$REBUILD_RELEASE"
|
||||
fi
|
||||
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms" from "all" check)
|
||||
# PACKAGES can be space-separated list (e.g., "dms-git dms dms-greeter" from "all" check)
|
||||
# Loop through each package and upload
|
||||
for PKG in $PACKAGES; do
|
||||
# Map package to PPA name
|
||||
|
||||
@@ -28,6 +28,12 @@ packages:
|
||||
outpkg: mocks_brightness
|
||||
interfaces:
|
||||
DBusConn:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation:
|
||||
config:
|
||||
dir: "internal/mocks/geolocation"
|
||||
outpkg: mocks_geolocation
|
||||
interfaces:
|
||||
Client:
|
||||
github.com/AvengeMedia/DankMaterialShell/core/internal/server/network:
|
||||
config:
|
||||
dir: "internal/mocks/network"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
repos:
|
||||
- repo: https://github.com/golangci/golangci-lint
|
||||
rev: v2.9.0
|
||||
rev: v2.10.1
|
||||
hooks:
|
||||
- id: golangci-lint-fmt
|
||||
require_serial: true
|
||||
|
||||
@@ -525,5 +525,6 @@ func getCommonCommands() []*cobra.Command {
|
||||
doctorCmd,
|
||||
configCmd,
|
||||
dlCmd,
|
||||
randrCmd,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,9 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -95,7 +97,11 @@ func buildMatugenOptions(cmd *cobra.Command) matugen.Options {
|
||||
|
||||
func runMatugenGenerate(cmd *cobra.Command, args []string) {
|
||||
opts := buildMatugenOptions(cmd)
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +135,11 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
if !wait {
|
||||
if err := sendServerRequestFireAndForget(request); err != nil {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
return
|
||||
@@ -146,11 +156,15 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
resp, ok := tryServerRequest(request)
|
||||
if !ok {
|
||||
log.Info("Server unavailable, running synchronously")
|
||||
if err := matugen.Run(opts); err != nil {
|
||||
err := matugen.Run(opts)
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
resultCh <- matugen.ErrNoChanges
|
||||
case err != nil:
|
||||
resultCh <- err
|
||||
return
|
||||
default:
|
||||
resultCh <- nil
|
||||
}
|
||||
resultCh <- nil
|
||||
return
|
||||
}
|
||||
if resp.Error != "" {
|
||||
@@ -162,7 +176,10 @@ func runMatugenQueue(cmd *cobra.Command, args []string) {
|
||||
|
||||
select {
|
||||
case err := <-resultCh:
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, matugen.ErrNoChanges):
|
||||
os.Exit(2)
|
||||
case err != nil:
|
||||
log.Fatalf("Theme generation failed: %v", err)
|
||||
}
|
||||
fmt.Println("Theme generation completed")
|
||||
|
||||
58
core/cmd/dms/commands_randr.go
Normal file
58
core/cmd/dms/commands_randr.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var randrCmd = &cobra.Command{
|
||||
Use: "randr",
|
||||
Short: "Query output display information",
|
||||
Long: "Query Wayland compositor for output names, scales, resolutions and refresh rates via zwlr-output-management",
|
||||
Run: runRandr,
|
||||
}
|
||||
|
||||
func init() {
|
||||
randrCmd.Flags().Bool("json", false, "Output in JSON format")
|
||||
}
|
||||
|
||||
type randrJSON struct {
|
||||
Outputs []randrOutput `json:"outputs"`
|
||||
}
|
||||
|
||||
func runRandr(cmd *cobra.Command, args []string) {
|
||||
outputs, err := queryRandr()
|
||||
if err != nil {
|
||||
log.Fatalf("%v", err)
|
||||
}
|
||||
|
||||
jsonFlag, _ := cmd.Flags().GetBool("json")
|
||||
|
||||
if jsonFlag {
|
||||
data, err := json.Marshal(randrJSON{Outputs: outputs})
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshal JSON: %v", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
return
|
||||
}
|
||||
|
||||
for i, out := range outputs {
|
||||
if i > 0 {
|
||||
fmt.Println()
|
||||
}
|
||||
status := "enabled"
|
||||
if !out.Enabled {
|
||||
status = "disabled"
|
||||
}
|
||||
fmt.Printf("%s (%s)\n", out.Name, status)
|
||||
fmt.Printf(" Scale: %.4g\n", out.Scale)
|
||||
fmt.Printf(" Resolution: %dx%d\n", out.Width, out.Height)
|
||||
if out.Refresh > 0 {
|
||||
fmt.Printf(" Refresh: %.2f Hz\n", float64(out.Refresh)/1000.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
172
core/cmd/dms/randr_client.go
Normal file
172
core/cmd/dms/randr_client.go
Normal file
@@ -0,0 +1,172 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_output_management"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
)
|
||||
|
||||
type randrOutput struct {
|
||||
Name string `json:"name"`
|
||||
Scale float64 `json:"scale"`
|
||||
Width int32 `json:"width"`
|
||||
Height int32 `json:"height"`
|
||||
Refresh int32 `json:"refresh"`
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type randrHead struct {
|
||||
name string
|
||||
enabled bool
|
||||
scale float64
|
||||
currentModeID uint32
|
||||
modeIDs []uint32
|
||||
}
|
||||
|
||||
type randrMode struct {
|
||||
width int32
|
||||
height int32
|
||||
refresh int32
|
||||
}
|
||||
|
||||
type randrClient struct {
|
||||
display *wlclient.Display
|
||||
ctx *wlclient.Context
|
||||
manager *wlr_output_management.ZwlrOutputManagerV1
|
||||
heads map[uint32]*randrHead
|
||||
modes map[uint32]*randrMode
|
||||
done bool
|
||||
err error
|
||||
}
|
||||
|
||||
func queryRandr() ([]randrOutput, error) {
|
||||
display, err := wlclient.Connect("")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to Wayland: %w", err)
|
||||
}
|
||||
|
||||
c := &randrClient{
|
||||
display: display,
|
||||
ctx: display.Context(),
|
||||
heads: make(map[uint32]*randrHead),
|
||||
modes: make(map[uint32]*randrMode),
|
||||
}
|
||||
defer c.ctx.Close()
|
||||
|
||||
registry, err := display.GetRegistry()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get registry: %w", err)
|
||||
}
|
||||
|
||||
registry.SetGlobalHandler(func(e wlclient.RegistryGlobalEvent) {
|
||||
if e.Interface == wlr_output_management.ZwlrOutputManagerV1InterfaceName {
|
||||
mgr := wlr_output_management.NewZwlrOutputManagerV1(c.ctx)
|
||||
version := min(e.Version, 4)
|
||||
|
||||
mgr.SetHeadHandler(func(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
c.handleHead(e)
|
||||
})
|
||||
|
||||
mgr.SetDoneHandler(func(e wlr_output_management.ZwlrOutputManagerV1DoneEvent) {
|
||||
c.done = true
|
||||
})
|
||||
|
||||
if err := registry.Bind(e.Name, e.Interface, version, mgr); err == nil {
|
||||
c.manager = mgr
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// First roundtrip: discover globals and bind manager
|
||||
syncCallback, err := display.Sync()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sync display: %w", err)
|
||||
}
|
||||
syncCallback.SetDoneHandler(func(e wlclient.CallbackDoneEvent) {
|
||||
if c.manager == nil {
|
||||
c.err = fmt.Errorf("zwlr_output_manager_v1 protocol not supported by compositor")
|
||||
c.done = true
|
||||
}
|
||||
// Otherwise wait for manager's DoneHandler
|
||||
})
|
||||
|
||||
for !c.done {
|
||||
if err := c.ctx.Dispatch(); err != nil {
|
||||
return nil, fmt.Errorf("dispatch error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if c.err != nil {
|
||||
return nil, c.err
|
||||
}
|
||||
|
||||
return c.buildOutputs(), nil
|
||||
}
|
||||
|
||||
func (c *randrClient) handleHead(e wlr_output_management.ZwlrOutputManagerV1HeadEvent) {
|
||||
handle := e.Head
|
||||
headID := handle.ID()
|
||||
|
||||
head := &randrHead{
|
||||
modeIDs: make([]uint32, 0),
|
||||
}
|
||||
c.heads[headID] = head
|
||||
|
||||
handle.SetNameHandler(func(e wlr_output_management.ZwlrOutputHeadV1NameEvent) {
|
||||
head.name = e.Name
|
||||
})
|
||||
|
||||
handle.SetEnabledHandler(func(e wlr_output_management.ZwlrOutputHeadV1EnabledEvent) {
|
||||
head.enabled = e.Enabled != 0
|
||||
})
|
||||
|
||||
handle.SetScaleHandler(func(e wlr_output_management.ZwlrOutputHeadV1ScaleEvent) {
|
||||
head.scale = e.Scale
|
||||
})
|
||||
|
||||
handle.SetCurrentModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1CurrentModeEvent) {
|
||||
head.currentModeID = e.Mode.ID()
|
||||
})
|
||||
|
||||
handle.SetModeHandler(func(e wlr_output_management.ZwlrOutputHeadV1ModeEvent) {
|
||||
modeHandle := e.Mode
|
||||
modeID := modeHandle.ID()
|
||||
|
||||
head.modeIDs = append(head.modeIDs, modeID)
|
||||
|
||||
mode := &randrMode{}
|
||||
c.modes[modeID] = mode
|
||||
|
||||
modeHandle.SetSizeHandler(func(e wlr_output_management.ZwlrOutputModeV1SizeEvent) {
|
||||
mode.width = e.Width
|
||||
mode.height = e.Height
|
||||
})
|
||||
|
||||
modeHandle.SetRefreshHandler(func(e wlr_output_management.ZwlrOutputModeV1RefreshEvent) {
|
||||
mode.refresh = e.Refresh
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (c *randrClient) buildOutputs() []randrOutput {
|
||||
outputs := make([]randrOutput, 0, len(c.heads))
|
||||
|
||||
for _, head := range c.heads {
|
||||
out := randrOutput{
|
||||
Name: head.name,
|
||||
Scale: head.scale,
|
||||
Enabled: head.enabled,
|
||||
}
|
||||
|
||||
if mode, ok := c.modes[head.currentModeID]; ok {
|
||||
out.Width = mode.width
|
||||
out.Height = mode.height
|
||||
out.Refresh = mode.refresh
|
||||
}
|
||||
|
||||
outputs = append(outputs, out)
|
||||
}
|
||||
|
||||
return outputs
|
||||
}
|
||||
42
core/internal/geolocation/client.go
Normal file
42
core/internal/geolocation/client.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package geolocation
|
||||
|
||||
import "github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
|
||||
func NewClient() Client {
|
||||
geoclueClient, err := newGeoClueClient()
|
||||
if err != nil {
|
||||
log.Warnf("GeoClue2 unavailable: %v", err)
|
||||
return newSeededIpClient()
|
||||
}
|
||||
|
||||
loc, _ := geoclueClient.GetLocation()
|
||||
if loc.Latitude != 0 || loc.Longitude != 0 {
|
||||
log.Info("Using GeoClue2 location")
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("GeoClue2 has no fix yet, seeding with IP location")
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location seed failed: %v", err)
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
log.Info("Seeded GeoClue2 with IP location")
|
||||
geoclueClient.SeedLocation(Location{Latitude: ipLoc.Latitude, Longitude: ipLoc.Longitude})
|
||||
return geoclueClient
|
||||
}
|
||||
|
||||
func newSeededIpClient() *IpClient {
|
||||
client := newIpClient()
|
||||
ipLoc, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
log.Warnf("IP location also failed: %v", err)
|
||||
return client
|
||||
}
|
||||
|
||||
log.Info("Using IP location")
|
||||
client.currLocation.Latitude = ipLoc.Latitude
|
||||
client.currLocation.Longitude = ipLoc.Longitude
|
||||
return client
|
||||
}
|
||||
243
core/internal/geolocation/client_geoclue.go
Normal file
243
core/internal/geolocation/client_geoclue.go
Normal file
@@ -0,0 +1,243 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/dbusutil"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
const (
|
||||
dbusGeoClueService = "org.freedesktop.GeoClue2"
|
||||
dbusGeoCluePath = "/org/freedesktop/GeoClue2"
|
||||
dbusGeoClueInterface = dbusGeoClueService
|
||||
|
||||
dbusGeoClueManagerPath = dbusGeoCluePath + "/Manager"
|
||||
dbusGeoClueManagerInterface = dbusGeoClueInterface + ".Manager"
|
||||
dbusGeoClueManagerGetClient = dbusGeoClueManagerInterface + ".GetClient"
|
||||
|
||||
dbusGeoClueClientInterface = dbusGeoClueInterface + ".Client"
|
||||
dbusGeoClueClientDesktopId = dbusGeoClueClientInterface + ".DesktopId"
|
||||
dbusGeoClueClientTimeThreshold = dbusGeoClueClientInterface + ".TimeThreshold"
|
||||
dbusGeoClueClientTimeStart = dbusGeoClueClientInterface + ".Start"
|
||||
dbusGeoClueClientTimeStop = dbusGeoClueClientInterface + ".Stop"
|
||||
dbusGeoClueClientLocationUpdated = dbusGeoClueClientInterface + ".LocationUpdated"
|
||||
|
||||
dbusGeoClueLocationInterface = dbusGeoClueInterface + ".Location"
|
||||
dbusGeoClueLocationLatitude = dbusGeoClueLocationInterface + ".Latitude"
|
||||
dbusGeoClueLocationLongitude = dbusGeoClueLocationInterface + ".Longitude"
|
||||
)
|
||||
|
||||
type GeoClueClient struct {
|
||||
currLocation *Location
|
||||
locationMutex sync.RWMutex
|
||||
|
||||
dbusConn *dbus.Conn
|
||||
clientPath dbus.ObjectPath
|
||||
signals chan *dbus.Signal
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan Location]
|
||||
}
|
||||
|
||||
func newGeoClueClient() (*GeoClueClient, error) {
|
||||
dbusConn, err := dbus.ConnectSystemBus()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("system bus connection failed: %w", err)
|
||||
}
|
||||
|
||||
c := &GeoClueClient{
|
||||
dbusConn: dbusConn,
|
||||
stopChan: make(chan struct{}),
|
||||
signals: make(chan *dbus.Signal, 256),
|
||||
|
||||
currLocation: &Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
},
|
||||
}
|
||||
|
||||
if err := c.setupClient(); err != nil {
|
||||
dbusConn.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := c.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Close() {
|
||||
close(c.stopChan)
|
||||
|
||||
c.sigWG.Wait()
|
||||
|
||||
if c.signals != nil {
|
||||
c.dbusConn.RemoveSignal(c.signals)
|
||||
close(c.signals)
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
close(ch)
|
||||
c.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
|
||||
if c.dbusConn != nil {
|
||||
c.dbusConn.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 64)
|
||||
c.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) Unsubscribe(id string) {
|
||||
if ch, ok := c.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) setupClient() error {
|
||||
managerObj := c.dbusConn.Object(dbusGeoClueService, dbusGeoClueManagerPath)
|
||||
|
||||
if err := managerObj.Call(dbusGeoClueManagerGetClient, 0).Store(&c.clientPath); err != nil {
|
||||
return fmt.Errorf("failed to create GeoClue2 client: %w", err)
|
||||
}
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientDesktopId, "dms"); err != nil {
|
||||
return fmt.Errorf("failed to set desktop ID: %w", err)
|
||||
}
|
||||
|
||||
if err := clientObj.SetProperty(dbusGeoClueClientTimeThreshold, uint(10)); err != nil {
|
||||
return fmt.Errorf("failed to set time threshold: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) startSignalPump() error {
|
||||
c.dbusConn.Signal(c.signals)
|
||||
|
||||
if err := c.dbusConn.AddMatchSignal(
|
||||
dbus.WithMatchObjectPath(c.clientPath),
|
||||
dbus.WithMatchInterface(dbusGeoClueClientInterface),
|
||||
dbus.WithMatchSender(dbusGeoClueClientLocationUpdated),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.sigWG.Add(1)
|
||||
go func() {
|
||||
defer c.sigWG.Done()
|
||||
|
||||
clientObj := c.dbusConn.Object(dbusGeoClueService, c.clientPath)
|
||||
clientObj.Call(dbusGeoClueClientTimeStart, 0)
|
||||
defer clientObj.Call(dbusGeoClueClientTimeStop, 0)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
return
|
||||
case sig, ok := <-c.signals:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if sig == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
c.handleSignal(sig)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleSignal(sig *dbus.Signal) {
|
||||
switch sig.Name {
|
||||
case dbusGeoClueClientLocationUpdated:
|
||||
if len(sig.Body) != 2 {
|
||||
return
|
||||
}
|
||||
|
||||
newLocationPath, ok := sig.Body[1].(dbus.ObjectPath)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
if err := c.handleLocationUpdated(newLocationPath); err != nil {
|
||||
log.Warn("GeoClue: Failed to handle location update: %v", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) handleLocationUpdated(path dbus.ObjectPath) error {
|
||||
locationObj := c.dbusConn.Object(dbusGeoClueService, path)
|
||||
|
||||
lat, err := locationObj.GetProperty(dbusGeoClueLocationLatitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
long, err := locationObj.GetProperty(dbusGeoClueLocationLongitude)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.locationMutex.Lock()
|
||||
c.currLocation.Latitude = dbusutil.AsOr(lat, 0.0)
|
||||
c.currLocation.Longitude = dbusutil.AsOr(long, 0.0)
|
||||
c.locationMutex.Unlock()
|
||||
|
||||
c.notifySubscribers()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) notifySubscribers() {
|
||||
currentLocation, err := c.GetLocation()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.subscribers.Range(func(key string, ch chan Location) bool {
|
||||
select {
|
||||
case ch <- currentLocation:
|
||||
default:
|
||||
log.Warn("GeoClue: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) SeedLocation(loc Location) {
|
||||
c.locationMutex.Lock()
|
||||
defer c.locationMutex.Unlock()
|
||||
c.currLocation.Latitude = loc.Latitude
|
||||
c.currLocation.Longitude = loc.Longitude
|
||||
}
|
||||
|
||||
func (c *GeoClueClient) GetLocation() (Location, error) {
|
||||
c.locationMutex.RLock()
|
||||
defer c.locationMutex.RUnlock()
|
||||
if c.currLocation == nil {
|
||||
return Location{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}, nil
|
||||
}
|
||||
stateCopy := *c.currLocation
|
||||
return stateCopy, nil
|
||||
}
|
||||
91
core/internal/geolocation/client_ip.go
Normal file
91
core/internal/geolocation/client_ip.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package geolocation
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type IpClient struct {
|
||||
currLocation *Location
|
||||
}
|
||||
|
||||
type ipLocationResult struct {
|
||||
Location
|
||||
City string
|
||||
}
|
||||
|
||||
type ipAPIResponse struct {
|
||||
Status string `json:"status"`
|
||||
Lat float64 `json:"lat"`
|
||||
Lon float64 `json:"lon"`
|
||||
City string `json:"city"`
|
||||
}
|
||||
|
||||
func newIpClient() *IpClient {
|
||||
return &IpClient{
|
||||
currLocation: &Location{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *IpClient) Subscribe(id string) chan Location {
|
||||
ch := make(chan Location, 1)
|
||||
if location, err := c.GetLocation(); err == nil {
|
||||
ch <- location
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (c *IpClient) Unsubscribe(id string) {}
|
||||
|
||||
func (c *IpClient) Close() {}
|
||||
|
||||
func (c *IpClient) GetLocation() (Location, error) {
|
||||
if c.currLocation.Latitude != 0 || c.currLocation.Longitude != 0 {
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
result, err := fetchIPLocation()
|
||||
if err != nil {
|
||||
return Location{}, err
|
||||
}
|
||||
|
||||
c.currLocation.Latitude = result.Latitude
|
||||
c.currLocation.Longitude = result.Longitude
|
||||
return *c.currLocation, nil
|
||||
}
|
||||
|
||||
func fetchIPLocation() (ipLocationResult, error) {
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
|
||||
resp, err := client.Get("http://ip-api.com/json/")
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to fetch IP location: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var data ipAPIResponse
|
||||
if err := json.Unmarshal(body, &data); err != nil {
|
||||
return ipLocationResult{}, fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if data.Status == "fail" || (data.Lat == 0 && data.Lon == 0) {
|
||||
return ipLocationResult{}, fmt.Errorf("ip-api.com returned no location data")
|
||||
}
|
||||
|
||||
return ipLocationResult{
|
||||
Location: Location{Latitude: data.Lat, Longitude: data.Lon},
|
||||
City: data.City,
|
||||
}, nil
|
||||
}
|
||||
15
core/internal/geolocation/types.go
Normal file
15
core/internal/geolocation/types.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package geolocation
|
||||
|
||||
type Location struct {
|
||||
Latitude float64
|
||||
Longitude float64
|
||||
}
|
||||
|
||||
type Client interface {
|
||||
GetLocation() (Location, error)
|
||||
|
||||
Subscribe(id string) chan Location
|
||||
Unsubscribe(id string)
|
||||
|
||||
Close()
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package matugen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
@@ -19,6 +21,8 @@ import (
|
||||
"github.com/lucasb-eyer/go-colorful"
|
||||
)
|
||||
|
||||
var ErrNoChanges = errors.New("no color changes")
|
||||
|
||||
type ColorMode string
|
||||
|
||||
const (
|
||||
@@ -160,39 +164,45 @@ func Run(opts Options) error {
|
||||
|
||||
log.Infof("Building theme: %s %s (%s)", opts.Kind, opts.Value, opts.Mode)
|
||||
|
||||
buildErr := buildOnce(&opts)
|
||||
changed, buildErr := buildOnce(&opts)
|
||||
if buildErr != nil {
|
||||
return buildErr
|
||||
}
|
||||
|
||||
if !changed {
|
||||
log.Info("No color changes detected, skipping refresh")
|
||||
return ErrNoChanges
|
||||
}
|
||||
|
||||
if opts.SyncModeWithPortal {
|
||||
syncColorScheme(opts.Mode)
|
||||
}
|
||||
|
||||
if buildErr != nil {
|
||||
return buildErr
|
||||
}
|
||||
|
||||
log.Info("Done")
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOnce(opts *Options) error {
|
||||
func buildOnce(opts *Options) (bool, error) {
|
||||
cfgFile, err := os.CreateTemp("", "matugen-config-*.toml")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp config: %w", err)
|
||||
return false, fmt.Errorf("failed to create temp config: %w", err)
|
||||
}
|
||||
defer os.Remove(cfgFile.Name())
|
||||
defer cfgFile.Close()
|
||||
|
||||
tmpDir, err := os.MkdirTemp("", "matugen-templates-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create temp dir: %w", err)
|
||||
return false, fmt.Errorf("failed to create temp dir: %w", err)
|
||||
}
|
||||
defer os.RemoveAll(tmpDir)
|
||||
|
||||
if err := buildMergedConfig(opts, cfgFile, tmpDir); err != nil {
|
||||
return fmt.Errorf("failed to build config: %w", err)
|
||||
return false, fmt.Errorf("failed to build config: %w", err)
|
||||
}
|
||||
cfgFile.Close()
|
||||
|
||||
oldColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
|
||||
var primaryDark, primaryLight, surface string
|
||||
var dank16JSON string
|
||||
var importArgs []string
|
||||
@@ -204,7 +214,7 @@ func buildOnce(opts *Options) error {
|
||||
surface = extractNestedColor(opts.StockColors, "surface", "dark")
|
||||
|
||||
if primaryDark == "" {
|
||||
return fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
return false, fmt.Errorf("failed to extract primary dark from stock colors")
|
||||
}
|
||||
if primaryLight == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -218,14 +228,14 @@ func buildOnce(opts *Options) error {
|
||||
args := []string{"color", "hex", primaryDark, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name()}
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
log.Infof("Using dynamic theme from %s: %s", opts.Kind, opts.Value)
|
||||
|
||||
matJSON, err := runMatugenDryRun(opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("matugen dry-run failed: %w", err)
|
||||
return false, fmt.Errorf("matugen dry-run failed: %w", err)
|
||||
}
|
||||
|
||||
primaryDark = extractMatugenColor(matJSON, "primary", "dark")
|
||||
@@ -233,7 +243,7 @@ func buildOnce(opts *Options) error {
|
||||
surface = extractMatugenColor(matJSON, "surface", "dark")
|
||||
|
||||
if primaryDark == "" {
|
||||
return fmt.Errorf("failed to extract primary color")
|
||||
return false, fmt.Errorf("failed to extract primary color")
|
||||
}
|
||||
if primaryLight == "" {
|
||||
primaryLight = primaryDark
|
||||
@@ -254,10 +264,15 @@ func buildOnce(opts *Options) error {
|
||||
args = append(args, "-m", string(opts.Mode), "-t", opts.MatugenType, "-c", cfgFile.Name())
|
||||
args = append(args, importArgs...)
|
||||
if err := runMatugen(args); err != nil {
|
||||
return err
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
newColors, _ := os.ReadFile(opts.ColorsOutput())
|
||||
if bytes.Equal(oldColors, newColors) && len(oldColors) > 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
if isDMSGTKActive(opts.ConfigDir) {
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
@@ -275,7 +290,7 @@ func buildOnce(opts *Options) error {
|
||||
|
||||
signalTerminals(opts)
|
||||
|
||||
return nil
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func buildMergedConfig(opts *Options, cfgFile *os.File, tmpDir string) error {
|
||||
|
||||
@@ -2,6 +2,7 @@ package matugen
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
@@ -93,10 +94,13 @@ func (q *Queue) runWorker() {
|
||||
err := Run(job.Options)
|
||||
|
||||
var result Result
|
||||
if err != nil {
|
||||
result = Result{Success: false, Error: err}
|
||||
} else {
|
||||
switch {
|
||||
case err == nil:
|
||||
result = Result{Success: true}
|
||||
case errors.Is(err, ErrNoChanges):
|
||||
result = Result{Success: true}
|
||||
default:
|
||||
result = Result{Success: false, Error: err}
|
||||
}
|
||||
|
||||
q.finishJob(result)
|
||||
|
||||
203
core/internal/mocks/geolocation/mock_Client.go
Normal file
203
core/internal/mocks/geolocation/mock_Client.go
Normal file
@@ -0,0 +1,203 @@
|
||||
// Code generated by mockery v2.53.5. DO NOT EDIT.
|
||||
|
||||
package mocks_geolocation
|
||||
|
||||
import (
|
||||
geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
// MockClient is an autogenerated mock type for the Client type
|
||||
type MockClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
type MockClient_Expecter struct {
|
||||
mock *mock.Mock
|
||||
}
|
||||
|
||||
func (_m *MockClient) EXPECT() *MockClient_Expecter {
|
||||
return &MockClient_Expecter{mock: &_m.Mock}
|
||||
}
|
||||
|
||||
// Close provides a mock function with no fields
|
||||
func (_m *MockClient) Close() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// MockClient_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
|
||||
type MockClient_Close_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Close is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) Close() *MockClient_Close_Call {
|
||||
return &MockClient_Close_Call{Call: _e.mock.On("Close")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Run(run func()) *MockClient_Close_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) Return() *MockClient_Close_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Close_Call) RunAndReturn(run func()) *MockClient_Close_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// GetLocation provides a mock function with no fields
|
||||
func (_m *MockClient) GetLocation() (geolocation.Location, error) {
|
||||
ret := _m.Called()
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for GetLocation")
|
||||
}
|
||||
|
||||
var r0 geolocation.Location
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(0).(func() (geolocation.Location, error)); ok {
|
||||
return rf()
|
||||
}
|
||||
if rf, ok := ret.Get(0).(func() geolocation.Location); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(geolocation.Location)
|
||||
}
|
||||
|
||||
if rf, ok := ret.Get(1).(func() error); ok {
|
||||
r1 = rf()
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// MockClient_GetLocation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetLocation'
|
||||
type MockClient_GetLocation_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// GetLocation is a helper method to define mock.On call
|
||||
func (_e *MockClient_Expecter) GetLocation() *MockClient_GetLocation_Call {
|
||||
return &MockClient_GetLocation_Call{Call: _e.mock.On("GetLocation")}
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Run(run func()) *MockClient_GetLocation_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run()
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) Return(_a0 geolocation.Location, _a1 error) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(_a0, _a1)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_GetLocation_Call) RunAndReturn(run func() (geolocation.Location, error)) *MockClient_GetLocation_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Subscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Subscribe(id string) chan geolocation.Location {
|
||||
ret := _m.Called(id)
|
||||
|
||||
if len(ret) == 0 {
|
||||
panic("no return value specified for Subscribe")
|
||||
}
|
||||
|
||||
var r0 chan geolocation.Location
|
||||
if rf, ok := ret.Get(0).(func(string) chan geolocation.Location); ok {
|
||||
r0 = rf(id)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(chan geolocation.Location)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// MockClient_Subscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Subscribe'
|
||||
type MockClient_Subscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Subscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Subscribe(id interface{}) *MockClient_Subscribe_Call {
|
||||
return &MockClient_Subscribe_Call{Call: _e.mock.On("Subscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Run(run func(id string)) *MockClient_Subscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) Return(_a0 chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(_a0)
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Subscribe_Call) RunAndReturn(run func(string) chan geolocation.Location) *MockClient_Subscribe_Call {
|
||||
_c.Call.Return(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// Unsubscribe provides a mock function with given fields: id
|
||||
func (_m *MockClient) Unsubscribe(id string) {
|
||||
_m.Called(id)
|
||||
}
|
||||
|
||||
// MockClient_Unsubscribe_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unsubscribe'
|
||||
type MockClient_Unsubscribe_Call struct {
|
||||
*mock.Call
|
||||
}
|
||||
|
||||
// Unsubscribe is a helper method to define mock.On call
|
||||
// - id string
|
||||
func (_e *MockClient_Expecter) Unsubscribe(id interface{}) *MockClient_Unsubscribe_Call {
|
||||
return &MockClient_Unsubscribe_Call{Call: _e.mock.On("Unsubscribe", id)}
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Run(run func(id string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Run(func(args mock.Arguments) {
|
||||
run(args[0].(string))
|
||||
})
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) Return() *MockClient_Unsubscribe_Call {
|
||||
_c.Call.Return()
|
||||
return _c
|
||||
}
|
||||
|
||||
func (_c *MockClient_Unsubscribe_Call) RunAndReturn(run func(string)) *MockClient_Unsubscribe_Call {
|
||||
_c.Run(run)
|
||||
return _c
|
||||
}
|
||||
|
||||
// NewMockClient creates a new instance of MockClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
|
||||
// The first argument is typically a *testing.T value.
|
||||
func NewMockClient(t interface {
|
||||
mock.TestingT
|
||||
Cleanup(func())
|
||||
}) *MockClient {
|
||||
mock := &MockClient{}
|
||||
mock.Mock.Test(t)
|
||||
|
||||
t.Cleanup(func() { mock.AssertExpectations(t) })
|
||||
|
||||
return mock
|
||||
}
|
||||
@@ -2,8 +2,10 @@ package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -275,13 +277,42 @@ func (m *Manager) GetClasses() ([]PrinterClass, error) {
|
||||
return classes, nil
|
||||
}
|
||||
|
||||
func createPrinterViaLpadmin(name, deviceURI, ppd, information, location string) error {
|
||||
args := []string{"-p", name, "-E", "-v", deviceURI, "-m", ppd}
|
||||
if information != "" {
|
||||
args = append(args, "-D", information)
|
||||
}
|
||||
if location != "" {
|
||||
args = append(args, "-L", location)
|
||||
}
|
||||
out, err := exec.Command("lpadmin", args...).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func deletePrinterViaLpadmin(name string) error {
|
||||
out, err := exec.Command("lpadmin", "-x", name).CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("lpadmin failed: %s: %w", strings.TrimSpace(string(out)), err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) CreatePrinter(name, deviceURI, ppd string, shared bool, errorPolicy, information, location string) error {
|
||||
usedPkHelper := false
|
||||
|
||||
err := m.client.CreatePrinter(name, deviceURI, ppd, shared, errorPolicy, information, location)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
if err = m.pkHelper.PrinterAdd(name, deviceURI, ppd, information, location); err != nil {
|
||||
return err
|
||||
// pkHelper failed (e.g., no polkit agent), try lpadmin as last resort.
|
||||
// lpadmin -E enables the printer, so no further setup needed.
|
||||
if lpadminErr := createPrinterViaLpadmin(name, deviceURI, ppd, information, location); lpadminErr != nil {
|
||||
return err
|
||||
}
|
||||
m.RefreshState()
|
||||
return nil
|
||||
}
|
||||
usedPkHelper = true
|
||||
} else if err != nil {
|
||||
@@ -308,6 +339,12 @@ func (m *Manager) DeletePrinter(printerName string) error {
|
||||
err := m.client.DeletePrinter(printerName)
|
||||
if isAuthError(err) && m.pkHelper != nil {
|
||||
err = m.pkHelper.PrinterDelete(printerName)
|
||||
if err != nil {
|
||||
// pkHelper failed, try lpadmin as last resort
|
||||
if lpadminErr := deletePrinterViaLpadmin(printerName); lpadminErr == nil {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
m.RefreshState()
|
||||
|
||||
@@ -70,6 +70,8 @@ func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
handleRestartJob(conn, req, manager)
|
||||
case "cups.holdJob":
|
||||
handleHoldJob(conn, req, manager)
|
||||
case "cups.testConnection":
|
||||
handleTestConnection(conn, req, manager)
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
@@ -464,3 +466,22 @@ func handleHoldJob(conn net.Conn, req models.Request, manager *Manager) {
|
||||
}
|
||||
models.Respond(conn, req.ID, models.SuccessResult{Success: true, Message: "job held"})
|
||||
}
|
||||
|
||||
func handleTestConnection(conn net.Conn, req models.Request, manager *Manager) {
|
||||
host, err := params.StringNonEmpty(req.Params, "host")
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
port := params.IntOpt(req.Params, "port", 631)
|
||||
protocol := params.StringOpt(req.Params, "protocol", "ipp")
|
||||
|
||||
result, err := manager.TestRemotePrinter(host, port, protocol)
|
||||
if err != nil {
|
||||
models.RespondError(conn, req.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
models.Respond(conn, req.ID, result)
|
||||
}
|
||||
|
||||
176
core/internal/server/cups/test_connection.go
Normal file
176
core/internal/server/cups/test_connection.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
)
|
||||
|
||||
var validProtocols = map[string]bool{
|
||||
"ipp": true,
|
||||
"ipps": true,
|
||||
"lpd": true,
|
||||
"socket": true,
|
||||
}
|
||||
|
||||
func validateTestConnectionParams(host string, port int, protocol string) error {
|
||||
if host == "" {
|
||||
return errors.New("host is required")
|
||||
}
|
||||
if strings.ContainsAny(host, " \t\n\r/\\") {
|
||||
return errors.New("host contains invalid characters")
|
||||
}
|
||||
if port < 1 || port > 65535 {
|
||||
return errors.New("port must be between 1 and 65535")
|
||||
}
|
||||
if protocol != "" && !validProtocols[protocol] {
|
||||
return errors.New("protocol must be one of: ipp, ipps, lpd, socket")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
const probeTimeout = 10 * time.Second
|
||||
|
||||
func probeRemotePrinter(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
|
||||
// Fast fail: TCP reachability check
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
// Create a temporary IPP client pointing at the remote host.
|
||||
// The TCP dial above provides fast-fail for unreachable hosts.
|
||||
// The IPP adapter's ResponseHeaderTimeout (90s) bounds stalling servers.
|
||||
client := ipp.NewIPPClient(host, port, "", "", useTLS)
|
||||
|
||||
// Try /ipp/print first (modern driverless printers), then / (legacy)
|
||||
info, err := probeIPPEndpoint(client, host, port, useTLS, "/ipp/print")
|
||||
if err != nil {
|
||||
// If we got an auth error, the printer exists but requires credentials.
|
||||
// Report it as reachable with the URI that triggered the auth challenge.
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
info, err = probeIPPEndpoint(client, host, port, useTLS, "/")
|
||||
}
|
||||
if err != nil {
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
// TCP reachable but not an IPP printer
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
Error: fmt.Sprintf("host is reachable but does not appear to be an IPP printer: %s", err.Error()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func probeIPPEndpoint(client *ipp.IPPClient, host string, port int, useTLS bool, resourcePath string) (*RemotePrinterInfo, error) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
printerURI := fmt.Sprintf("%s://%s:%d%s", proto, host, port, resourcePath)
|
||||
|
||||
httpProto := "http"
|
||||
if useTLS {
|
||||
httpProto = "https"
|
||||
}
|
||||
httpURL := fmt.Sprintf("%s://%s:%d%s", httpProto, host, port, resourcePath)
|
||||
|
||||
req := ipp.NewRequest(ipp.OperationGetPrinterAttributes, 1)
|
||||
req.OperationAttributes[ipp.AttributePrinterURI] = printerURI
|
||||
req.OperationAttributes[ipp.AttributeRequestedAttributes] = []string{
|
||||
ipp.AttributePrinterName,
|
||||
ipp.AttributePrinterMakeAndModel,
|
||||
ipp.AttributePrinterState,
|
||||
ipp.AttributePrinterInfo,
|
||||
ipp.AttributePrinterUriSupported,
|
||||
}
|
||||
|
||||
resp, err := client.SendRequest(httpURL, req, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(resp.PrinterAttributes) == 0 {
|
||||
return nil, errors.New("no printer attributes returned")
|
||||
}
|
||||
|
||||
attrs := resp.PrinterAttributes[0]
|
||||
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: getStringAttr(attrs, ipp.AttributePrinterMakeAndModel),
|
||||
Name: getStringAttr(attrs, ipp.AttributePrinterName),
|
||||
Info: getStringAttr(attrs, ipp.AttributePrinterInfo),
|
||||
State: parsePrinterState(attrs),
|
||||
URI: printerURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// TestRemotePrinter validates inputs and probes a remote printer via IPP.
|
||||
// For lpd/socket protocols, only TCP reachability is tested.
|
||||
func (m *Manager) TestRemotePrinter(host string, port int, protocol string) (*RemotePrinterInfo, error) {
|
||||
if protocol == "" {
|
||||
protocol = "ipp"
|
||||
}
|
||||
|
||||
if err := validateTestConnectionParams(host, port, protocol); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// For non-IPP protocols, only check TCP reachability
|
||||
if protocol == "lpd" || protocol == "socket" {
|
||||
addr := net.JoinHostPort(host, fmt.Sprintf("%d", port))
|
||||
conn, err := net.DialTimeout("tcp", addr, probeTimeout)
|
||||
if err != nil {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: fmt.Sprintf("cannot reach %s: %s", addr, err.Error()),
|
||||
}, nil
|
||||
}
|
||||
conn.Close()
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d", protocol, host, port),
|
||||
}, nil
|
||||
}
|
||||
|
||||
useTLS := protocol == "ipps"
|
||||
|
||||
probeFn := m.probeRemoteFn
|
||||
if probeFn == nil {
|
||||
probeFn = probeRemotePrinter
|
||||
}
|
||||
|
||||
return probeFn(host, port, useTLS)
|
||||
}
|
||||
397
core/internal/server/cups/test_connection_test.go
Normal file
397
core/internal/server/cups/test_connection_test.go
Normal file
@@ -0,0 +1,397 @@
|
||||
package cups
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/ipp"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestValidateTestConnectionParams(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "valid ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid ipps",
|
||||
host: "printer.local",
|
||||
port: 443,
|
||||
protocol: "ipps",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid lpd",
|
||||
host: "10.0.0.1",
|
||||
port: 515,
|
||||
protocol: "lpd",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "valid socket",
|
||||
host: "10.0.0.1",
|
||||
port: 9100,
|
||||
protocol: "socket",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "empty host",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "port too low",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "port too high",
|
||||
host: "192.168.0.5",
|
||||
port: 70000,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
{
|
||||
name: "empty protocol treated as ipp",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "",
|
||||
wantErr: "",
|
||||
},
|
||||
{
|
||||
name: "host with slash",
|
||||
host: "192.168.0.5/admin",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with space",
|
||||
host: "192.168.0.5 ",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
{
|
||||
name: "host with newline",
|
||||
host: "192.168.0.5\n",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host contains invalid characters",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateTestConnectionParams(tt.host, tt.port, tt.protocol)
|
||||
if tt.wantErr == "" {
|
||||
assert.NoError(t, err)
|
||||
} else {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_Validation(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
host string
|
||||
port int
|
||||
protocol string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty host returns error",
|
||||
host: "",
|
||||
port: 631,
|
||||
protocol: "ipp",
|
||||
wantErr: "host is required",
|
||||
},
|
||||
{
|
||||
name: "invalid port returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 0,
|
||||
protocol: "ipp",
|
||||
wantErr: "port must be between 1 and 65535",
|
||||
},
|
||||
{
|
||||
name: "invalid protocol returns error",
|
||||
host: "192.168.0.5",
|
||||
port: 631,
|
||||
protocol: "ftp",
|
||||
wantErr: "protocol must be one of: ipp, ipps, lpd, socket",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
_, err := m.TestRemotePrinter(tt.host, tt.port, tt.protocol)
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_IPP(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
protocol string
|
||||
probeRet *RemotePrinterInfo
|
||||
probeErr error
|
||||
wantTLS bool
|
||||
wantReach bool
|
||||
wantModel string
|
||||
}{
|
||||
{
|
||||
name: "successful ipp probe",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "successful ipps probe",
|
||||
protocol: "ipps",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
URI: "ipps://192.168.0.5:631/ipp/print",
|
||||
},
|
||||
wantTLS: true,
|
||||
wantReach: true,
|
||||
wantModel: "HP OfficeJet 8010",
|
||||
},
|
||||
{
|
||||
name: "unreachable host",
|
||||
protocol: "ipp",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: false,
|
||||
Error: "cannot reach 192.168.0.5:631: connection refused",
|
||||
},
|
||||
wantReach: false,
|
||||
},
|
||||
{
|
||||
name: "empty protocol defaults to ipp",
|
||||
protocol: "",
|
||||
probeRet: &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "Test Printer",
|
||||
},
|
||||
wantTLS: false,
|
||||
wantReach: true,
|
||||
wantModel: "Test Printer",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var capturedTLS bool
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
capturedTLS = useTLS
|
||||
return tt.probeRet, tt.probeErr
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.5", 631, tt.protocol)
|
||||
if tt.probeErr != nil {
|
||||
assert.Error(t, err)
|
||||
return
|
||||
}
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, tt.wantReach, result.Reachable)
|
||||
assert.Equal(t, tt.wantModel, result.MakeModel)
|
||||
assert.Equal(t, tt.wantTLS, capturedTLS)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_AuthRequired(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// Simulate what happens when the printer returns HTTP 401
|
||||
return probeRemotePrinterWithAuthError(host, port, useTLS)
|
||||
}
|
||||
|
||||
result, err := m.TestRemotePrinter("192.168.0.107", 631, "ipp")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Reachable)
|
||||
assert.Equal(t, "authentication required", result.Info)
|
||||
assert.Contains(t, result.URI, "ipp://192.168.0.107:631")
|
||||
}
|
||||
|
||||
// probeRemotePrinterWithAuthError simulates a probe where the printer
|
||||
// returns HTTP 401 on both endpoints.
|
||||
func probeRemotePrinterWithAuthError(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
// This simulates what probeRemotePrinter does when both endpoints
|
||||
// return auth errors. We test the auth detection logic directly.
|
||||
err := ipp.HTTPError{Code: 401}
|
||||
if isAuthError(err) {
|
||||
proto := "ipp"
|
||||
if useTLS {
|
||||
proto = "ipps"
|
||||
}
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
URI: fmt.Sprintf("%s://%s:%d/ipp/print", proto, host, port),
|
||||
Info: "authentication required",
|
||||
}, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func TestManager_TestRemotePrinter_NonIPPProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
probeCalled := false
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
probeCalled = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// These will fail at TCP dial (no real server), but the important
|
||||
// thing is that probeRemoteFn is NOT called for lpd/socket.
|
||||
m.TestRemotePrinter("192.168.0.5", 9100, "socket")
|
||||
assert.False(t, probeCalled, "probe function should not be called for socket protocol")
|
||||
|
||||
m.TestRemotePrinter("192.168.0.5", 515, "lpd")
|
||||
assert.False(t, probeCalled, "probe function should not be called for lpd protocol")
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_Success(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{
|
||||
Reachable: true,
|
||||
MakeModel: "HP OfficeJet 8010",
|
||||
Name: "OfficeJet",
|
||||
State: "idle",
|
||||
URI: "ipp://192.168.0.5:631/ipp/print",
|
||||
}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
assert.Equal(t, "HP OfficeJet 8010", resp.Result.MakeModel)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_MissingHost(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[any]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.Nil(t, resp.Result)
|
||||
assert.NotNil(t, resp.Error)
|
||||
}
|
||||
|
||||
func TestHandleTestConnection_CustomPortAndProtocol(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
assert.Equal(t, 9631, port)
|
||||
assert.True(t, useTLS)
|
||||
return &RemotePrinterInfo{Reachable: true, URI: "ipps://192.168.0.5:9631/ipp/print"}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{
|
||||
"host": "192.168.0.5",
|
||||
"port": float64(9631),
|
||||
"protocol": "ipps",
|
||||
},
|
||||
}
|
||||
|
||||
handleTestConnection(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
|
||||
func TestHandleRequest_TestConnection(t *testing.T) {
|
||||
m := NewTestManager(nil, nil)
|
||||
m.probeRemoteFn = func(host string, port int, useTLS bool) (*RemotePrinterInfo, error) {
|
||||
return &RemotePrinterInfo{Reachable: true}, nil
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
conn := &mockConn{Buffer: buf}
|
||||
|
||||
req := models.Request{
|
||||
ID: 1,
|
||||
Method: "cups.testConnection",
|
||||
Params: map[string]any{"host": "192.168.0.5"},
|
||||
}
|
||||
|
||||
HandleRequest(conn, req, m)
|
||||
|
||||
var resp models.Response[RemotePrinterInfo]
|
||||
err := json.NewDecoder(buf).Decode(&resp)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, resp.Result)
|
||||
assert.True(t, resp.Result.Reachable)
|
||||
}
|
||||
@@ -55,6 +55,16 @@ type PPD struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
type RemotePrinterInfo struct {
|
||||
Reachable bool `json:"reachable"`
|
||||
MakeModel string `json:"makeModel"`
|
||||
Name string `json:"name"`
|
||||
Info string `json:"info"`
|
||||
State string `json:"state"`
|
||||
URI string `json:"uri"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
type PrinterClass struct {
|
||||
Name string `json:"name"`
|
||||
URI string `json:"uri"`
|
||||
@@ -77,6 +87,7 @@ type Manager struct {
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotifiedState *CUPSState
|
||||
baseURL string
|
||||
probeRemoteFn func(host string, port int, useTLS bool) (*RemotePrinterInfo, error)
|
||||
}
|
||||
|
||||
type SubscriptionManagerInterface interface {
|
||||
|
||||
61
core/internal/server/location/handlers.go
Normal file
61
core/internal/server/location/handlers.go
Normal file
@@ -0,0 +1,61 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
)
|
||||
|
||||
type LocationEvent struct {
|
||||
Type string `json:"type"`
|
||||
Data State `json:"data"`
|
||||
}
|
||||
|
||||
func HandleRequest(conn net.Conn, req models.Request, manager *Manager) {
|
||||
switch req.Method {
|
||||
case "location.getState":
|
||||
handleGetState(conn, req, manager)
|
||||
case "location.subscribe":
|
||||
handleSubscribe(conn, req, manager)
|
||||
|
||||
default:
|
||||
models.RespondError(conn, req.ID, fmt.Sprintf("unknown method: %s", req.Method))
|
||||
}
|
||||
}
|
||||
|
||||
func handleGetState(conn net.Conn, req models.Request, manager *Manager) {
|
||||
models.Respond(conn, req.ID, manager.GetState())
|
||||
}
|
||||
|
||||
func handleSubscribe(conn net.Conn, req models.Request, manager *Manager) {
|
||||
clientID := fmt.Sprintf("client-%p", conn)
|
||||
stateChan := manager.Subscribe(clientID)
|
||||
defer manager.Unsubscribe(clientID)
|
||||
|
||||
initialState := manager.GetState()
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: initialState,
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
ID: req.ID,
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for state := range stateChan {
|
||||
event := LocationEvent{
|
||||
Type: "state_changed",
|
||||
Data: state,
|
||||
}
|
||||
if err := json.NewEncoder(conn).Encode(models.Response[LocationEvent]{
|
||||
Result: &event,
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
175
core/internal/server/location/manager.go
Normal file
175
core/internal/server/location/manager.go
Normal file
@@ -0,0 +1,175 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
)
|
||||
|
||||
func NewManager(client geolocation.Client) (*Manager, error) {
|
||||
currLocation, err := client.GetLocation()
|
||||
if err != nil {
|
||||
log.Warnf("Failed to get initial location: %v", err)
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
client: client,
|
||||
dirty: make(chan struct{}),
|
||||
stopChan: make(chan struct{}),
|
||||
|
||||
state: &State{
|
||||
Latitude: currLocation.Latitude,
|
||||
Longitude: currLocation.Longitude,
|
||||
},
|
||||
}
|
||||
|
||||
if err := m.startSignalPump(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
m.notifierWg.Add(1)
|
||||
go m.notifier()
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m *Manager) Close() {
|
||||
close(m.stopChan)
|
||||
m.notifierWg.Wait()
|
||||
|
||||
m.sigWG.Wait()
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
close(ch)
|
||||
m.subscribers.Delete(key)
|
||||
return true
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Manager) Subscribe(id string) chan State {
|
||||
ch := make(chan State, 64)
|
||||
m.subscribers.Store(id, ch)
|
||||
return ch
|
||||
}
|
||||
|
||||
func (m *Manager) Unsubscribe(id string) {
|
||||
if ch, ok := m.subscribers.LoadAndDelete(id); ok {
|
||||
close(ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) startSignalPump() error {
|
||||
m.sigWG.Add(1)
|
||||
go func() {
|
||||
defer m.sigWG.Done()
|
||||
|
||||
subscription := m.client.Subscribe("locationManager")
|
||||
defer m.client.Unsubscribe("locationManager")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
return
|
||||
case location, ok := <-subscription:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
m.handleLocationChange(location)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) handleLocationChange(location geolocation.Location) {
|
||||
m.stateMutex.Lock()
|
||||
defer m.stateMutex.Unlock()
|
||||
|
||||
m.state.Latitude = location.Latitude
|
||||
m.state.Longitude = location.Longitude
|
||||
|
||||
m.notifySubscribers()
|
||||
}
|
||||
|
||||
func (m *Manager) notifySubscribers() {
|
||||
select {
|
||||
case m.dirty <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) GetState() State {
|
||||
m.stateMutex.RLock()
|
||||
defer m.stateMutex.RUnlock()
|
||||
if m.state == nil {
|
||||
return State{
|
||||
Latitude: 0.0,
|
||||
Longitude: 0.0,
|
||||
}
|
||||
}
|
||||
stateCopy := *m.state
|
||||
return stateCopy
|
||||
}
|
||||
|
||||
func (m *Manager) notifier() {
|
||||
defer m.notifierWg.Done()
|
||||
const minGap = 200 * time.Millisecond
|
||||
timer := time.NewTimer(minGap)
|
||||
timer.Stop()
|
||||
var pending bool
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-m.stopChan:
|
||||
timer.Stop()
|
||||
return
|
||||
case <-m.dirty:
|
||||
if pending {
|
||||
continue
|
||||
}
|
||||
pending = true
|
||||
timer.Reset(minGap)
|
||||
case <-timer.C:
|
||||
if !pending {
|
||||
continue
|
||||
}
|
||||
|
||||
currentState := m.GetState()
|
||||
|
||||
if m.lastNotified != nil && !stateChanged(m.lastNotified, ¤tState) {
|
||||
pending = false
|
||||
continue
|
||||
}
|
||||
|
||||
m.subscribers.Range(func(key string, ch chan State) bool {
|
||||
select {
|
||||
case ch <- currentState:
|
||||
default:
|
||||
log.Warn("Location: subscriber channel full, dropping update")
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
stateCopy := currentState
|
||||
m.lastNotified = &stateCopy
|
||||
pending = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stateChanged(old, new *State) bool {
|
||||
if old == nil || new == nil {
|
||||
return true
|
||||
}
|
||||
if old.Latitude != new.Latitude {
|
||||
return true
|
||||
}
|
||||
if old.Longitude != new.Longitude {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
28
core/internal/server/location/types.go
Normal file
28
core/internal/server/location/types.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package location
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
Latitude float64 `json:"latitude"`
|
||||
Longitude float64 `json:"longitude"`
|
||||
}
|
||||
|
||||
type Manager struct {
|
||||
state *State
|
||||
stateMutex sync.RWMutex
|
||||
|
||||
client geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
sigWG sync.WaitGroup
|
||||
|
||||
subscribers syncmap.Map[string, chan State]
|
||||
dirty chan struct{}
|
||||
notifierWg sync.WaitGroup
|
||||
lastNotified *State
|
||||
}
|
||||
@@ -5,5 +5,6 @@ const (
|
||||
dbusPath = "/org/freedesktop/login1"
|
||||
dbusManagerInterface = "org.freedesktop.login1.Manager"
|
||||
dbusSessionInterface = "org.freedesktop.login1.Session"
|
||||
dbusUserInterface = "org.freedesktop.login1.User"
|
||||
dbusPropsInterface = "org.freedesktop.DBus.Properties"
|
||||
)
|
||||
|
||||
@@ -17,15 +17,8 @@ func NewManager() (*Manager, error) {
|
||||
return nil, fmt.Errorf("failed to connect to system bus: %w", err)
|
||||
}
|
||||
|
||||
sessionID := os.Getenv("XDG_SESSION_ID")
|
||||
if sessionID == "" {
|
||||
sessionID = "self"
|
||||
}
|
||||
|
||||
m := &Manager{
|
||||
state: &SessionState{
|
||||
SessionID: sessionID,
|
||||
},
|
||||
state: &SessionState{},
|
||||
stateMutex: sync.RWMutex{},
|
||||
|
||||
stopChan: make(chan struct{}),
|
||||
@@ -60,12 +53,13 @@ func (m *Manager) initialize() error {
|
||||
|
||||
m.initializeFallbackDelay()
|
||||
|
||||
sessionPath, err := m.getSession(m.state.SessionID)
|
||||
sessionID, sessionPath, err := m.discoverSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get session path: %w", err)
|
||||
}
|
||||
|
||||
m.stateMutex.Lock()
|
||||
m.state.SessionID = sessionID
|
||||
m.state.SessionPath = string(sessionPath)
|
||||
m.sessionPath = sessionPath
|
||||
m.stateMutex.Unlock()
|
||||
@@ -79,6 +73,41 @@ func (m *Manager) initialize() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) discoverSession() (string, dbus.ObjectPath, error) {
|
||||
// 1. Explicit XDG_SESSION_ID
|
||||
if id := os.Getenv("XDG_SESSION_ID"); id != "" {
|
||||
if path, err := m.getSession(id); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "loginctl: using XDG_SESSION_ID=%s\n", id)
|
||||
return id, path, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 2. PID-based lookup (works when caller is inside a session cgroup)
|
||||
if id, path, err := m.getSessionByPID(uint32(os.Getpid())); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via PID\n", id)
|
||||
return id, path, nil
|
||||
}
|
||||
|
||||
// 3. User's primary display session (handles UWSM and similar)
|
||||
if id, path, err := m.getUserDisplaySession(); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via User.Display\n", id)
|
||||
return id, path, nil
|
||||
}
|
||||
|
||||
// 4. Score all sessions for current UID
|
||||
if id, path, err := m.findBestSession(); err == nil {
|
||||
fmt.Fprintf(os.Stderr, "loginctl: found session %s via ListSessions scoring\n", id)
|
||||
return id, path, nil
|
||||
}
|
||||
|
||||
// 5. Last resort: "self"
|
||||
path, err := m.getSession("self")
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("%w", err)
|
||||
}
|
||||
return "self", path, nil
|
||||
}
|
||||
|
||||
func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
||||
var out dbus.ObjectPath
|
||||
err := m.managerObj.Call(dbusManagerInterface+".GetSession", 0, id).Store(&out)
|
||||
@@ -88,6 +117,166 @@ func (m *Manager) getSession(id string) (dbus.ObjectPath, error) {
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *Manager) getSessionByPID(pid uint32) (string, dbus.ObjectPath, error) {
|
||||
var path dbus.ObjectPath
|
||||
if err := m.managerObj.Call(dbusManagerInterface+".GetSessionByPID", 0, pid).Store(&path); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
sessionObj := m.conn.Object(dbusDest, path)
|
||||
var id dbus.Variant
|
||||
if err := sessionObj.Call(dbusPropsInterface+".Get", 0, dbusSessionInterface, "Id").Store(&id); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return id.Value().(string), path, nil
|
||||
}
|
||||
|
||||
func (m *Manager) getUserDisplaySession() (string, dbus.ObjectPath, error) {
|
||||
uid := uint32(os.Getuid())
|
||||
|
||||
var userPath dbus.ObjectPath
|
||||
if err := m.managerObj.Call(dbusManagerInterface+".GetUser", 0, uid).Store(&userPath); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
userObj := m.conn.Object(dbusDest, userPath)
|
||||
var display dbus.Variant
|
||||
if err := userObj.Call(dbusPropsInterface+".Get", 0, dbusUserInterface, "Display").Store(&display); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
pair, ok := display.Value().([]any)
|
||||
if !ok || len(pair) < 2 {
|
||||
return "", "", fmt.Errorf("unexpected Display format")
|
||||
}
|
||||
|
||||
sessionID, _ := pair[0].(string)
|
||||
sessionPath, _ := pair[1].(dbus.ObjectPath)
|
||||
if sessionID == "" || sessionPath == "" {
|
||||
return "", "", fmt.Errorf("empty Display session")
|
||||
}
|
||||
|
||||
return sessionID, sessionPath, nil
|
||||
}
|
||||
|
||||
type sessionCandidate struct {
|
||||
id string
|
||||
path dbus.ObjectPath
|
||||
}
|
||||
|
||||
func (m *Manager) findBestSession() (string, dbus.ObjectPath, error) {
|
||||
// ListSessions returns a(susso): [][]any where each entry is [id, uid, name, seat, path]
|
||||
var raw [][]any
|
||||
if err := m.managerObj.Call(dbusManagerInterface+".ListSessions", 0).Store(&raw); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
uid := uint32(os.Getuid())
|
||||
var candidates []sessionCandidate
|
||||
for _, entry := range raw {
|
||||
if len(entry) < 5 {
|
||||
continue
|
||||
}
|
||||
entryUID, _ := entry[1].(uint32)
|
||||
if entryUID != uid {
|
||||
continue
|
||||
}
|
||||
id, _ := entry[0].(string)
|
||||
path, _ := entry[4].(dbus.ObjectPath)
|
||||
if id != "" && path != "" {
|
||||
candidates = append(candidates, sessionCandidate{id: id, path: path})
|
||||
}
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
return "", "", fmt.Errorf("no sessions for uid %d", uid)
|
||||
}
|
||||
|
||||
bestScore := -1
|
||||
var best sessionCandidate
|
||||
for _, c := range candidates {
|
||||
score := m.scoreSession(c.path)
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
best = c
|
||||
}
|
||||
}
|
||||
if bestScore < 0 {
|
||||
return "", "", fmt.Errorf("no viable session found")
|
||||
}
|
||||
return best.id, best.path, nil
|
||||
}
|
||||
|
||||
func (m *Manager) scoreSession(path dbus.ObjectPath) int {
|
||||
obj := m.conn.Object(dbusDest, path)
|
||||
var props map[string]dbus.Variant
|
||||
if err := obj.Call(dbusPropsInterface+".GetAll", 0, dbusSessionInterface).Store(&props); err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
getStr := func(key string) string {
|
||||
if v, ok := props[key]; ok {
|
||||
if s, ok := v.Value().(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
getBool := func(key string) bool {
|
||||
if v, ok := props[key]; ok {
|
||||
if b, ok := v.Value().(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
getUint32 := func(key string) uint32 {
|
||||
if v, ok := props[key]; ok {
|
||||
if u, ok := v.Value().(uint32); ok {
|
||||
return u
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
class := getStr("Class")
|
||||
if class != "user" {
|
||||
return -1
|
||||
}
|
||||
if getBool("Remote") {
|
||||
return -1
|
||||
}
|
||||
|
||||
score := 0
|
||||
|
||||
if getBool("Active") {
|
||||
score += 100
|
||||
}
|
||||
|
||||
switch getStr("Type") {
|
||||
case "wayland", "x11":
|
||||
score += 80
|
||||
case "tty":
|
||||
score += 10
|
||||
}
|
||||
|
||||
if v, ok := props["Seat"]; ok {
|
||||
if seatArr, ok := v.Value().([]any); ok && len(seatArr) >= 1 {
|
||||
if seat, ok := seatArr[0].(string); ok && seat != "" {
|
||||
score += 40
|
||||
if seat == "seat0" {
|
||||
score += 10
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if getUint32("VTNr") > 0 {
|
||||
score += 20
|
||||
}
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
func (m *Manager) refreshSessionBinding() error {
|
||||
if m.managerObj == nil || m.conn == nil {
|
||||
return fmt.Errorf("manager not fully initialized")
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -192,6 +193,15 @@ func RouteRequest(conn net.Conn, req models.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if strings.HasPrefix(req.Method, "location.") {
|
||||
if locationManager == nil {
|
||||
models.RespondError(conn, req.ID, "location manager not initialized")
|
||||
return
|
||||
}
|
||||
location.HandleRequest(conn, req, locationManager)
|
||||
return
|
||||
}
|
||||
|
||||
switch req.Method {
|
||||
case "ping":
|
||||
models.Respond(conn, req.ID, "pong")
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/apppicker"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/bluez"
|
||||
@@ -25,6 +26,7 @@ import (
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/evdev"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/extworkspace"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/freedesktop"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/location"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/models"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/network"
|
||||
@@ -70,6 +72,7 @@ var clipboardManager *clipboard.Manager
|
||||
var dbusManager *serverDbus.Manager
|
||||
var wlContext *wlcontext.SharedContext
|
||||
var themeModeManager *thememode.Manager
|
||||
var locationManager *location.Manager
|
||||
|
||||
const dbusClientID = "dms-dbus-client"
|
||||
|
||||
@@ -188,7 +191,7 @@ func InitializeFreedeskManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeWaylandManager() error {
|
||||
func InitializeWaylandManager(geoClient geolocation.Client) error {
|
||||
log.Info("Attempting to initialize Wayland gamma control...")
|
||||
|
||||
if wlContext == nil {
|
||||
@@ -201,7 +204,7 @@ func InitializeWaylandManager() error {
|
||||
}
|
||||
|
||||
config := wayland.DefaultConfig()
|
||||
manager, err := wayland.NewManager(wlContext.Display(), config)
|
||||
manager, err := wayland.NewManager(wlContext.Display(), geoClient, config)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to initialize wayland manager: %v", err)
|
||||
return err
|
||||
@@ -382,14 +385,27 @@ func InitializeDbusManager() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeThemeModeManager() error {
|
||||
manager := thememode.NewManager()
|
||||
func InitializeThemeModeManager(geoClient geolocation.Client) error {
|
||||
manager := thememode.NewManager(geoClient)
|
||||
themeModeManager = manager
|
||||
|
||||
log.Info("Theme mode automation manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func InitializeLocationManager(geoClient geolocation.Client) error {
|
||||
manager, err := location.NewManager(geoClient)
|
||||
if err != nil {
|
||||
log.Warnf("Failed to initialize location manager: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
locationManager = manager
|
||||
|
||||
log.Info("Location manager initialized")
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleConnection(conn net.Conn) {
|
||||
defer conn.Close()
|
||||
|
||||
@@ -537,6 +553,10 @@ func getServerInfo() ServerInfo {
|
||||
caps = append(caps, "theme.auto")
|
||||
}
|
||||
|
||||
if locationManager != nil {
|
||||
caps = append(caps, "location")
|
||||
}
|
||||
|
||||
if dbusManager != nil {
|
||||
caps = append(caps, "dbus")
|
||||
}
|
||||
@@ -1307,6 +1327,9 @@ func cleanupManagers() {
|
||||
if wlContext != nil {
|
||||
wlContext.Close()
|
||||
}
|
||||
if locationManager != nil {
|
||||
locationManager.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func Start(printDocs bool) error {
|
||||
@@ -1488,6 +1511,9 @@ func Start(printDocs bool) error {
|
||||
log.Info(" clipboard.getConfig - Get clipboard configuration")
|
||||
log.Info(" clipboard.setConfig - Set configuration (params: maxHistory?, maxEntrySize?, autoClearDays?, clearAtStartup?)")
|
||||
log.Info(" clipboard.subscribe - Subscribe to clipboard state changes (streaming)")
|
||||
log.Info("Location:")
|
||||
log.Info(" location.getState - Get current location state")
|
||||
log.Info(" location.subscribe - Subscribe to location changes (streaming)")
|
||||
log.Info("")
|
||||
}
|
||||
log.Info("Initializing managers...")
|
||||
@@ -1519,6 +1545,9 @@ func Start(printDocs bool) error {
|
||||
loginctlReady := make(chan struct{})
|
||||
freedesktopReady := make(chan struct{})
|
||||
|
||||
geoClient := geolocation.NewClient()
|
||||
defer geoClient.Close()
|
||||
|
||||
go func() {
|
||||
defer close(loginctlReady)
|
||||
if err := InitializeLoginctlManager(); err != nil {
|
||||
@@ -1563,7 +1592,7 @@ func Start(printDocs bool) error {
|
||||
}
|
||||
}()
|
||||
|
||||
if err := InitializeWaylandManager(); err != nil {
|
||||
if err := InitializeWaylandManager(geoClient); err != nil {
|
||||
log.Warnf("Wayland manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
@@ -1595,7 +1624,7 @@ func Start(printDocs bool) error {
|
||||
log.Debugf("WlrOutput manager unavailable: %v", err)
|
||||
}
|
||||
|
||||
if err := InitializeThemeModeManager(); err != nil {
|
||||
if err := InitializeThemeModeManager(geoClient); err != nil {
|
||||
log.Warnf("Theme mode manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
@@ -1608,6 +1637,12 @@ func Start(printDocs bool) error {
|
||||
}()
|
||||
}
|
||||
|
||||
if err := InitializeLocationManager(geoClient); err != nil {
|
||||
log.Warnf("Location manager unavailable: %v", err)
|
||||
} else {
|
||||
notifyCapabilityChange()
|
||||
}
|
||||
|
||||
fatalErrChan := make(chan error, 1)
|
||||
if wlrOutputManager != nil {
|
||||
go func() {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/loginctl"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/server/wayland"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
@@ -32,12 +33,14 @@ type Manager struct {
|
||||
cachedIPLat *float64
|
||||
cachedIPLon *float64
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
stopChan chan struct{}
|
||||
updateTrigger chan struct{}
|
||||
wg sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
func NewManager(geoClient geolocation.Client) *Manager {
|
||||
m := &Manager{
|
||||
config: Config{
|
||||
Enabled: false,
|
||||
@@ -51,6 +54,7 @@ func NewManager() *Manager {
|
||||
},
|
||||
stopChan: make(chan struct{}),
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
m.updateState(time.Now())
|
||||
@@ -327,17 +331,17 @@ func (m *Manager) getLocation(config Config) (*float64, *float64) {
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
lat, lon, err := wayland.FetchIPLocation()
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
|
||||
return lat, lon
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
}
|
||||
|
||||
func statesEqual(a, b *State) bool {
|
||||
|
||||
@@ -13,13 +13,14 @@ import (
|
||||
"golang.org/x/sys/unix"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/log"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/proto/wlr_gamma_control"
|
||||
)
|
||||
|
||||
const animKelvinStep = 25
|
||||
|
||||
func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error) {
|
||||
func NewManager(display wlclient.WaylandDisplay, geoClient geolocation.Client, config Config) (*Manager, error) {
|
||||
if err := config.Validate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,6 +41,7 @@ func NewManager(display wlclient.WaylandDisplay, config Config) (*Manager, error
|
||||
updateTrigger: make(chan struct{}, 1),
|
||||
dirty: make(chan struct{}, 1),
|
||||
dbusSignal: make(chan *dbus.Signal, 16),
|
||||
geoClient: geoClient,
|
||||
}
|
||||
|
||||
if err := m.setupRegistry(); err != nil {
|
||||
@@ -437,15 +439,16 @@ func (m *Manager) getLocation() (*float64, *float64) {
|
||||
}
|
||||
m.locationMutex.RUnlock()
|
||||
|
||||
lat, lon, err := FetchIPLocation()
|
||||
location, err := m.geoClient.GetLocation()
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
m.locationMutex.Lock()
|
||||
m.cachedIPLat = lat
|
||||
m.cachedIPLon = lon
|
||||
m.cachedIPLat = &location.Latitude
|
||||
m.cachedIPLon = &location.Longitude
|
||||
m.locationMutex.Unlock()
|
||||
return lat, lon
|
||||
return m.cachedIPLat, m.cachedIPLon
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
mocks_geolocation "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/geolocation"
|
||||
mocks_wlclient "github.com/AvengeMedia/DankMaterialShell/core/internal/mocks/wlclient"
|
||||
)
|
||||
|
||||
@@ -390,18 +391,20 @@ func TestNotifySubscribers_NonBlocking(t *testing.T) {
|
||||
|
||||
func TestNewManager_GetRegistryError(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
mockDisplay.EXPECT().Context().Return(nil)
|
||||
mockDisplay.EXPECT().GetRegistry().Return(nil, errors.New("failed to get registry"))
|
||||
|
||||
config := DefaultConfig()
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "get registry")
|
||||
}
|
||||
|
||||
func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
mockDisplay := mocks_wlclient.NewMockWaylandDisplay(t)
|
||||
mockGeoclient := mocks_geolocation.NewMockClient(t)
|
||||
|
||||
config := Config{
|
||||
LowTemp: 500,
|
||||
@@ -409,6 +412,6 @@ func TestNewManager_InvalidConfig(t *testing.T) {
|
||||
Gamma: 1.0,
|
||||
}
|
||||
|
||||
_, err := NewManager(mockDisplay, config)
|
||||
_, err := NewManager(mockDisplay, mockGeoclient, config)
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/internal/geolocation"
|
||||
wlclient "github.com/AvengeMedia/DankMaterialShell/core/pkg/go-wayland/wayland/client"
|
||||
"github.com/AvengeMedia/DankMaterialShell/core/pkg/syncmap"
|
||||
"github.com/godbus/dbus/v5"
|
||||
@@ -97,6 +98,8 @@ type Manager struct {
|
||||
dbusConn *dbus.Conn
|
||||
dbusSignal chan *dbus.Signal
|
||||
|
||||
geoClient geolocation.Client
|
||||
|
||||
lastAppliedTemp int
|
||||
lastAppliedGamma float64
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<service name="download_url">
|
||||
<param name="protocol">https</param>
|
||||
<param name="host">github.com</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.2/dms-qml.tar.gz</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.4.3/dms-qml.tar.gz</param>
|
||||
<param name="filename">dms-qml.tar.gz</param>
|
||||
</service>
|
||||
</services>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
dms-greeter (1.4.2db8) unstable; urgency=medium
|
||||
dms-greeter (1.4.3db1) unstable; urgency=medium
|
||||
|
||||
* Initial Debian OBS package
|
||||
* Port from Ubuntu/Fedora packaging
|
||||
* Update to v1.4.3 stable release
|
||||
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Sat, 21 Feb 2026 00:00:00 +0000
|
||||
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 25 Feb 2026 02:40:00 +0000
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
dmsPkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -10,7 +9,7 @@ let
|
||||
in
|
||||
{
|
||||
packages = [
|
||||
dmsPkgs.dms-shell
|
||||
cfg.package
|
||||
]
|
||||
++ lib.optional cfg.enableSystemMonitoring cfg.dgop.package
|
||||
++ lib.optionals cfg.enableVPN [
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
let
|
||||
inherit (lib) types;
|
||||
cfg = config.programs.dank-material-shell.greeter;
|
||||
cfgDms = config.programs.dank-material-shell;
|
||||
|
||||
inherit (config.services.greetd.settings.default_session) user;
|
||||
|
||||
@@ -29,13 +30,13 @@ let
|
||||
lib.escapeShellArgs (
|
||||
[
|
||||
"sh"
|
||||
"${../../quickshell/Modules/Greetd/assets/dms-greeter}"
|
||||
"${cfg.package}/share/quickshell/dms/Modules/Greetd/assets/dms-greeter"
|
||||
"--cache-dir"
|
||||
cacheDir
|
||||
"--command"
|
||||
cfg.compositor.name
|
||||
"-p"
|
||||
"${dmsPkgs.dms-shell}/share/quickshell/dms"
|
||||
"${cfg.package}/share/quickshell/dms"
|
||||
]
|
||||
++ lib.optionals (cfg.compositor.customConfig != "") [
|
||||
"-C"
|
||||
@@ -65,6 +66,21 @@ in
|
||||
|
||||
options.programs.dank-material-shell.greeter = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell greeter";
|
||||
package = lib.mkOption {
|
||||
type = types.package;
|
||||
default = if cfgDms.enable or false then cfgDms.package else dmsPkgs.dms-shell;
|
||||
defaultText = lib.literalExpression ''
|
||||
if config.programs.dank-material-shell.enable
|
||||
then config.programs.dank-material-shell.package
|
||||
else built from source;
|
||||
'';
|
||||
description = ''
|
||||
The DankMaterialShell package to use for the greeter.
|
||||
|
||||
Defaults to the package from `programs.dank-material-shell` if it is enabled,
|
||||
otherwise defaults to building from source.
|
||||
'';
|
||||
};
|
||||
compositor.name = lib.mkOption {
|
||||
type = types.enum [
|
||||
"niri"
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -13,7 +12,6 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
hasPluginSettings = lib.any (plugin: plugin.settings != { }) (
|
||||
@@ -96,7 +94,7 @@ in
|
||||
};
|
||||
|
||||
Service = {
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
config,
|
||||
pkgs,
|
||||
lib,
|
||||
dmsPkgs,
|
||||
...
|
||||
}@args:
|
||||
let
|
||||
@@ -12,7 +11,6 @@ let
|
||||
config
|
||||
pkgs
|
||||
lib
|
||||
dmsPkgs
|
||||
;
|
||||
};
|
||||
in
|
||||
@@ -36,7 +34,7 @@ in
|
||||
restartIfChanged = cfg.systemd.restartIfChanged;
|
||||
|
||||
serviceConfig = {
|
||||
ExecStart = lib.getExe dmsPkgs.dms-shell + " run --session";
|
||||
ExecStart = lib.getExe cfg.package + " run --session";
|
||||
Restart = "on-failure";
|
||||
};
|
||||
};
|
||||
@@ -50,6 +48,7 @@ in
|
||||
|
||||
services.power-profiles-daemon.enable = lib.mkDefault true;
|
||||
services.accounts-daemon.enable = lib.mkDefault true;
|
||||
services.geoclue2.enable = lib.mkDefault true;
|
||||
security.polkit.enable = lib.mkDefault true;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,6 +26,9 @@ in
|
||||
|
||||
options.programs.dank-material-shell = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell";
|
||||
package = lib.mkPackageOption dmsPkgs "dms-shell" {
|
||||
extraDescription = "The DankMaterialShell package to use (defaults to be built from source)";
|
||||
};
|
||||
|
||||
systemd = {
|
||||
enable = lib.mkEnableOption "DankMaterialShell systemd startup";
|
||||
|
||||
@@ -419,6 +419,9 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]];
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
|
||||
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
|
||||
@@ -813,6 +816,9 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
sed -i "s/VERSION_PLACEHOLDER/${DMS_GREETER_BASE_VERSION}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/RELEASE_PLACEHOLDER/${DMS_GREETER_RELEASE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/CHANGELOG_DATE_PLACEHOLDER/${CHANGELOG_DATE}/g" "$WORK_DIR/$PACKAGE.spec"
|
||||
# Explicitly set Version:/Release: in case the spec uses %{version} macro
|
||||
sed -i "s/^Version:.*/Version: ${DMS_GREETER_BASE_VERSION}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
sed -i "s/^Release:.*/Release: ${DMS_GREETER_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
174
quickshell/Common/AnimVariants.qml
Normal file
174
quickshell/Common/AnimVariants.qml
Normal file
@@ -0,0 +1,174 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
|
||||
// AnimVariants — Central tuning for animation and Motion Effects variants
|
||||
// (Material/Fluent/Dynamic) (Standard/Directional/Depth)
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property list<real> variantEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return Anims.standardDecel;
|
||||
case 2:
|
||||
return Anims.expressiveFastSpatial;
|
||||
default:
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property list<real> variantExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return Anims.standard;
|
||||
case 2:
|
||||
return Anims.emphasized;
|
||||
default:
|
||||
return Anims.emphasized;
|
||||
}
|
||||
}
|
||||
|
||||
// Modal-specific entry curve
|
||||
readonly property list<real> variantModalEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.standardDecel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.expressiveFastSpatial;
|
||||
}
|
||||
return variantEnterCurve;
|
||||
}
|
||||
|
||||
readonly property list<real> variantModalExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.emphasizedAccel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.emphasizedAccel;
|
||||
}
|
||||
return variantExitCurve;
|
||||
}
|
||||
|
||||
// Popout-specific entry curve
|
||||
readonly property list<real> variantPopoutEnterCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.expressiveDefaultSpatial;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.standardDecel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.expressiveFastSpatial;
|
||||
return Anims.standardDecel;
|
||||
}
|
||||
return variantEnterCurve;
|
||||
}
|
||||
|
||||
readonly property list<real> variantPopoutExitCurve: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return Anims.emphasized;
|
||||
if (isDirectionalEffect) {
|
||||
if (SettingsData.animationVariant === 1)
|
||||
return Anims.emphasizedAccel;
|
||||
if (SettingsData.animationVariant === 2)
|
||||
return Anims.emphasizedAccel;
|
||||
}
|
||||
return variantExitCurve;
|
||||
}
|
||||
|
||||
readonly property real variantEnterDurationFactor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return 0.9;
|
||||
case 2:
|
||||
return 1.08;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real variantExitDurationFactor: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
switch (SettingsData.animationVariant) {
|
||||
case 1:
|
||||
return 0.85;
|
||||
case 2:
|
||||
return 0.92;
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
// Fluent: opacity at ~55% of duration; Material/Dynamic: 1:1 with position
|
||||
readonly property real variantOpacityDurationScale: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 1.0;
|
||||
return SettingsData.animationVariant === 1 ? 0.55 : 1.0;
|
||||
}
|
||||
|
||||
function variantDuration(baseDuration, entering) {
|
||||
const factor = entering ? variantEnterDurationFactor : variantExitDurationFactor;
|
||||
return Math.max(0, Math.round(baseDuration * factor));
|
||||
}
|
||||
|
||||
function variantExitCleanupPadding() {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 50;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 8;
|
||||
case 2:
|
||||
return 24;
|
||||
default:
|
||||
return 50;
|
||||
}
|
||||
}
|
||||
|
||||
function variantCloseInterval(baseDuration) {
|
||||
return variantDuration(baseDuration, false) + variantExitCleanupPadding();
|
||||
}
|
||||
|
||||
readonly property bool isDirectionalEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 1
|
||||
readonly property bool isDepthEffect: typeof SettingsData !== "undefined" && SettingsData.motionEffect === 2
|
||||
|
||||
readonly property real effectScaleCollapsed: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 0.96;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 1.0;
|
||||
case 2:
|
||||
return 0.88;
|
||||
default:
|
||||
return 0.96;
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real effectAnimOffset: {
|
||||
if (typeof SettingsData === "undefined")
|
||||
return 16;
|
||||
switch (SettingsData.motionEffect) {
|
||||
case 1:
|
||||
return 144;
|
||||
case 2:
|
||||
return 56;
|
||||
default:
|
||||
return 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,4 +22,9 @@ Singleton {
|
||||
readonly property var standard: [0.20, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardDecel: [0.00, 0.00, 0.00, 1.00, 1.00, 1.00]
|
||||
readonly property var standardAccel: [0.30, 0.00, 1.00, 1.00, 1.00, 1.00]
|
||||
|
||||
// Used by AnimVariants for variant/effect logic
|
||||
readonly property var expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property var expressiveFastSpatial: [0.34, 1.5, 0.2, 1.0, 1.0, 1.0]
|
||||
readonly property var expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
54
quickshell/Common/ElevationShadow.qml
Normal file
54
quickshell/Common/ElevationShadow.qml
Normal file
@@ -0,0 +1,54 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var level: Theme.elevationLevel2
|
||||
property string direction: Theme.elevationLightDirection
|
||||
property real fallbackOffset: 4
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0
|
||||
property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset)
|
||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
||||
property color shadowColor: Theme.elevationShadowColor(level)
|
||||
property real shadowOpacity: 1
|
||||
property real blurMax: Theme.elevationBlurMax
|
||||
|
||||
property alias sourceRect: sourceRect
|
||||
|
||||
layer.enabled: shadowEnabled
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, root.shadowBlurPx / Math.max(1, root.blurMax)))
|
||||
shadowScale: 1 + (2 * root.shadowSpreadPx) / Math.max(1, Math.min(root.width, root.height))
|
||||
shadowHorizontalOffset: root.shadowOffsetX
|
||||
shadowVerticalOffset: root.shadowOffsetY
|
||||
blurMax: root.blurMax
|
||||
shadowColor: root.shadowColor
|
||||
shadowOpacity: root.shadowOpacity
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: sourceRect
|
||||
anchors.fill: parent
|
||||
radius: root.targetRadius
|
||||
color: root.targetColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
import Quickshell
|
||||
@@ -8,7 +9,9 @@ import Quickshell.Io
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property string _rawLocale: Qt.locale().name
|
||||
property string _resolvedLocale: "en"
|
||||
|
||||
readonly property string _rawLocale: SessionData.locale === "" ? Qt.locale().name : SessionData.locale
|
||||
readonly property string _lang: _rawLocale.split(/[_-]/)[0]
|
||||
readonly property var _candidates: {
|
||||
const fullUnderscore = _rawLocale;
|
||||
@@ -21,7 +24,10 @@ Singleton {
|
||||
|
||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||
|
||||
property string currentLocale: "en"
|
||||
readonly property alias folder: dir.folder
|
||||
property var presentLocales: ({
|
||||
"en": Qt.locale("en")
|
||||
})
|
||||
property var translations: ({})
|
||||
property bool translationsLoaded: false
|
||||
|
||||
@@ -34,8 +40,10 @@ Singleton {
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
|
||||
onStatusChanged: if (status === FolderListModel.Ready)
|
||||
root._pickTranslation()
|
||||
onStatusChanged: if (status === FolderListModel.Ready) {
|
||||
root._loadPresentLocales();
|
||||
root._pickTranslation();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
@@ -46,41 +54,54 @@ Singleton {
|
||||
try {
|
||||
root.translations = JSON.parse(text());
|
||||
root.translationsLoaded = true;
|
||||
console.info(`I18n: Loaded translations for '${root.currentLocale}' ` + `(${Object.keys(root.translations).length} contexts)`);
|
||||
console.info(`I18n: Loaded translations for '${root._resolvedLocale}' (${Object.keys(root.translations).length} contexts)`);
|
||||
} catch (e) {
|
||||
console.warn(`I18n: Error parsing '${root.currentLocale}':`, e, "- falling back to English");
|
||||
console.warn(`I18n: Error parsing '${root._resolvedLocale}':`, e, "- falling back to English");
|
||||
root._fallbackToEnglish();
|
||||
}
|
||||
}
|
||||
|
||||
onLoadFailed: error => {
|
||||
console.warn(`I18n: Failed to load '${root.currentLocale}' (${error}), ` + "falling back to English");
|
||||
console.warn(`I18n: Failed to load '${root._resolvedLocale}' (${error}), ` + "falling back to English");
|
||||
root._fallbackToEnglish();
|
||||
}
|
||||
}
|
||||
|
||||
function _pickTranslation() {
|
||||
const present = new Set();
|
||||
function locale() {
|
||||
if (SessionData.timeLocale)
|
||||
return Qt.locale(SessionData.timeLocale);
|
||||
return Qt.locale();
|
||||
}
|
||||
|
||||
function _loadPresentLocales() {
|
||||
if (Object.keys(presentLocales).length > 1) {
|
||||
return; // already loaded
|
||||
}
|
||||
for (let i = 0; i < dir.count; i++) {
|
||||
const name = dir.get(i, "fileName"); // e.g. "zh_CN.json"
|
||||
if (name && name.endsWith(".json")) {
|
||||
present.add(name.slice(0, -5));
|
||||
const shortName = name.slice(0, -5);
|
||||
presentLocales[shortName] = Qt.locale(shortName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function _pickTranslation() {
|
||||
for (let i = 0; i < _candidates.length; i++) {
|
||||
const cand = _candidates[i];
|
||||
if (present.has(cand)) {
|
||||
_useLocale(cand, dir.folder + "/" + cand + ".json");
|
||||
return;
|
||||
}
|
||||
if (presentLocales[cand] === undefined)
|
||||
continue;
|
||||
_resolvedLocale = cand;
|
||||
useLocale(cand, cand.startsWith("en") ? "" : translationsFolder + "/" + cand + ".json");
|
||||
return;
|
||||
}
|
||||
|
||||
_resolvedLocale = "en";
|
||||
_fallbackToEnglish();
|
||||
}
|
||||
|
||||
function _useLocale(localeTag, fileUrl) {
|
||||
currentLocale = localeTag;
|
||||
function useLocale(localeTag, fileUrl) {
|
||||
_resolvedLocale = localeTag || "en";
|
||||
_selectedPath = fileUrl;
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
@@ -88,7 +109,6 @@ Singleton {
|
||||
}
|
||||
|
||||
function _fallbackToEnglish() {
|
||||
currentLocale = "en";
|
||||
_selectedPath = "";
|
||||
translationsLoaded = false;
|
||||
translations = ({});
|
||||
|
||||
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import Quickshell
|
||||
import QtCore
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
@@ -70,20 +71,50 @@ Singleton {
|
||||
return appId;
|
||||
}
|
||||
|
||||
function resolveIconPath(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
return toFileUrl(expandTilde(moddedId));
|
||||
if (moddedId.startsWith("file://"))
|
||||
return moddedId;
|
||||
return Quickshell.iconPath(moddedId, true);
|
||||
}
|
||||
return Quickshell.iconPath(iconName, true) || DesktopService.resolveIconPath(iconName);
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName) return "";
|
||||
const moddedId = moddedAppId(iconName);
|
||||
if (moddedId !== iconName) {
|
||||
if (moddedId.startsWith("~") || moddedId.startsWith("/"))
|
||||
return toFileUrl(expandTilde(moddedId));
|
||||
if (moddedId.startsWith("file://"))
|
||||
return moddedId;
|
||||
return "image://icon/" + moddedId;
|
||||
}
|
||||
return "image://icon/" + iconName;
|
||||
}
|
||||
|
||||
function getAppIcon(appId: string, desktopEntry: var): string {
|
||||
if (appId === "org.quickshell") {
|
||||
return Qt.resolvedUrl("../assets/danklogo.svg");
|
||||
}
|
||||
|
||||
const moddedId = moddedAppId(appId);
|
||||
if (moddedId !== appId) {
|
||||
return Quickshell.iconPath(moddedId, true);
|
||||
}
|
||||
if (moddedId !== appId)
|
||||
return resolveIconPath(appId);
|
||||
|
||||
if (desktopEntry && desktopEntry.icon) {
|
||||
return Quickshell.iconPath(desktopEntry.icon, true);
|
||||
}
|
||||
return Quickshell.iconPath(appId, true);
|
||||
|
||||
const icon = Quickshell.iconPath(appId, true);
|
||||
if (icon && icon !== "")
|
||||
return icon;
|
||||
|
||||
return DesktopService.resolveIconPath(appId);
|
||||
}
|
||||
|
||||
function getAppName(appId: string, desktopEntry: var): string {
|
||||
|
||||
@@ -21,7 +21,9 @@ Singleton {
|
||||
property bool _isReadOnly: false
|
||||
property bool _hasUnsavedChanges: false
|
||||
property var _loadedSessionSnapshot: null
|
||||
readonly property var _hooks: ({})
|
||||
readonly property var _hooks: ({
|
||||
"updateLocale": updateLocale
|
||||
})
|
||||
readonly property string _stateUrl: StandardPaths.writableLocation(StandardPaths.GenericStateLocation)
|
||||
readonly property string _stateDir: Paths.strip(_stateUrl)
|
||||
|
||||
@@ -126,6 +128,9 @@ Singleton {
|
||||
property var hiddenOutputDeviceNames: []
|
||||
property var hiddenInputDeviceNames: []
|
||||
|
||||
property string locale: ""
|
||||
property string timeLocale: ""
|
||||
|
||||
property string launcherLastMode: "all"
|
||||
property string appDrawerLastMode: "apps"
|
||||
property string niriOverviewLastMode: "apps"
|
||||
@@ -1104,6 +1109,14 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function updateLocale() {
|
||||
if (!locale) {
|
||||
I18n._pickTranslation();
|
||||
return;
|
||||
}
|
||||
I18n.useLocale(locale, locale.startsWith("en") ? "" : I18n.folder + "/" + locale + ".json");
|
||||
}
|
||||
|
||||
function setLauncherLastMode(mode) {
|
||||
launcherLastMode = mode;
|
||||
saveSettings();
|
||||
|
||||
@@ -14,7 +14,7 @@ import "settings/SettingsStore.js" as Store
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
readonly property int settingsConfigVersion: 5
|
||||
readonly property int settingsConfigVersion: 6
|
||||
|
||||
readonly property bool isGreeterMode: Quickshell.env("DMS_RUN_GREETER") === "1" || Quickshell.env("DMS_RUN_GREETER") === "true"
|
||||
|
||||
@@ -37,6 +37,18 @@ Singleton {
|
||||
Custom
|
||||
}
|
||||
|
||||
enum AnimationVariant {
|
||||
Material,
|
||||
Fluent,
|
||||
Dynamic
|
||||
}
|
||||
|
||||
enum AnimationEffect {
|
||||
Standard, // 0 — M3: scale-in, rises from below
|
||||
Directional, // 1 — pure large slide, no scale
|
||||
Depth // 2 — medium slide with deep depth scale pop
|
||||
}
|
||||
|
||||
enum SuspendBehavior {
|
||||
Suspend,
|
||||
Hibernate,
|
||||
@@ -149,6 +161,7 @@ Singleton {
|
||||
property int mangoLayoutRadiusOverride: -1
|
||||
property int mangoLayoutBorderSize: -1
|
||||
|
||||
property int firstDayOfWeek: -1
|
||||
property bool use24HourClock: true
|
||||
property bool showSeconds: false
|
||||
property bool padHours12Hour: false
|
||||
@@ -165,6 +178,30 @@ Singleton {
|
||||
property int modalCustomAnimationDuration: 150
|
||||
property bool enableRippleEffects: true
|
||||
onEnableRippleEffectsChanged: saveSettings()
|
||||
property int animationVariant: SettingsData.AnimationVariant.Material
|
||||
onAnimationVariantChanged: saveSettings()
|
||||
property int motionEffect: SettingsData.AnimationEffect.Standard
|
||||
onMotionEffectChanged: saveSettings()
|
||||
property int directionalAnimationMode: 0
|
||||
onDirectionalAnimationModeChanged: saveSettings()
|
||||
property bool m3ElevationEnabled: true
|
||||
onM3ElevationEnabledChanged: saveSettings()
|
||||
property int m3ElevationIntensity: 12
|
||||
onM3ElevationIntensityChanged: saveSettings()
|
||||
property int m3ElevationOpacity: 30
|
||||
onM3ElevationOpacityChanged: saveSettings()
|
||||
property string m3ElevationColorMode: "default"
|
||||
onM3ElevationColorModeChanged: saveSettings()
|
||||
property string m3ElevationLightDirection: "top"
|
||||
onM3ElevationLightDirectionChanged: saveSettings()
|
||||
property string m3ElevationCustomColor: "#000000"
|
||||
onM3ElevationCustomColorChanged: saveSettings()
|
||||
property bool modalElevationEnabled: true
|
||||
onModalElevationEnabledChanged: saveSettings()
|
||||
property bool popoutElevationEnabled: true
|
||||
onPopoutElevationEnabledChanged: saveSettings()
|
||||
property bool barElevationEnabled: true
|
||||
onBarElevationEnabledChanged: saveSettings()
|
||||
property string wallpaperFillMode: "Fill"
|
||||
property bool blurredWallpaperLayer: false
|
||||
property bool blurWallpaperOnOverview: false
|
||||
@@ -494,9 +531,15 @@ Singleton {
|
||||
property bool enableFprint: false
|
||||
property int maxFprintTries: 15
|
||||
property bool fprintdAvailable: false
|
||||
property bool enableU2f: false
|
||||
property string u2fMode: "or"
|
||||
property bool u2fAvailable: false
|
||||
property string lockScreenActiveMonitor: "all"
|
||||
property string lockScreenInactiveColor: "#000000"
|
||||
property int lockScreenNotificationMode: 0
|
||||
property bool lockScreenVideoEnabled: false
|
||||
property string lockScreenVideoPath: ""
|
||||
property bool lockScreenVideoCycling: false
|
||||
property bool hideBrightnessSlider: false
|
||||
|
||||
property int notificationTimeoutLow: 5000
|
||||
@@ -575,7 +618,7 @@ Singleton {
|
||||
"maximizeWidgetIcons": false,
|
||||
"maximizeWidgetText": false,
|
||||
"removeWidgetPadding": false,
|
||||
"widgetPadding": 12,
|
||||
"widgetPadding": 8,
|
||||
"gothCornersEnabled": false,
|
||||
"gothCornerRadiusOverride": false,
|
||||
"gothCornerRadiusValue": 12,
|
||||
@@ -602,7 +645,7 @@ Singleton {
|
||||
"scrollYBehavior": "workspace",
|
||||
"shadowIntensity": 0,
|
||||
"shadowOpacity": 60,
|
||||
"shadowColorMode": "text",
|
||||
"shadowColorMode": "default",
|
||||
"shadowCustomColor": "#000000",
|
||||
"clickThrough": false
|
||||
}
|
||||
@@ -982,6 +1025,7 @@ Singleton {
|
||||
loadSettings();
|
||||
initializeListModels();
|
||||
Processes.detectFprintd();
|
||||
Processes.detectU2f();
|
||||
Processes.checkPluginSettings();
|
||||
}
|
||||
}
|
||||
@@ -1129,7 +1173,7 @@ Singleton {
|
||||
"updateCompositorLayout": updateCompositorLayout,
|
||||
"applyStoredIconTheme": applyStoredIconTheme,
|
||||
"updateBarConfigs": updateBarConfigs,
|
||||
"updateCompositorCursor": updateCompositorCursor
|
||||
"updateCompositorCursor": updateCompositorCursor,
|
||||
})
|
||||
|
||||
function set(key, value) {
|
||||
|
||||
@@ -673,6 +673,232 @@ Singleton {
|
||||
property color shadowMedium: Qt.rgba(0, 0, 0, 0.08)
|
||||
property color shadowStrong: Qt.rgba(0, 0, 0, 0.3)
|
||||
|
||||
readonly property bool elevationEnabled: typeof SettingsData !== "undefined" && (SettingsData.m3ElevationEnabled ?? true)
|
||||
readonly property real elevationBlurMax: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? Math.min(128, Math.max(32, SettingsData.m3ElevationIntensity * 2)) : 64
|
||||
|
||||
readonly property real _elevMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationIntensity !== undefined ? SettingsData.m3ElevationIntensity / 12 : 1
|
||||
readonly property real _opMult: typeof SettingsData !== "undefined" && SettingsData.m3ElevationOpacity !== undefined ? SettingsData.m3ElevationOpacity / 60 : 1
|
||||
function normalizeElevationDirection(direction) {
|
||||
switch (direction) {
|
||||
case "top":
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
case "left":
|
||||
case "right":
|
||||
case "autoBar":
|
||||
return direction;
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
|
||||
readonly property string elevationLightDirection: {
|
||||
if (typeof SettingsData === "undefined" || !SettingsData.m3ElevationLightDirection)
|
||||
return "top";
|
||||
switch (SettingsData.m3ElevationLightDirection) {
|
||||
case "autoBar":
|
||||
case "top":
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottom":
|
||||
return SettingsData.m3ElevationLightDirection;
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property real _elevDiagRatio: 0.55
|
||||
readonly property string _globalElevationDirForTokens: {
|
||||
const normalized = normalizeElevationDirection(elevationLightDirection);
|
||||
return normalized === "autoBar" ? "top" : normalized;
|
||||
}
|
||||
readonly property real _elevDirX: {
|
||||
switch (_globalElevationDirForTokens) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
case "left":
|
||||
return 1;
|
||||
case "topRight":
|
||||
case "bottomRight":
|
||||
case "right":
|
||||
return -1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
readonly property real _elevDirY: {
|
||||
switch (_globalElevationDirForTokens) {
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return -1;
|
||||
case "left":
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
readonly property real _elevDirXScale: (_globalElevationDirForTokens === "left" || _globalElevationDirForTokens === "right") ? 1 : _elevDiagRatio
|
||||
|
||||
readonly property var elevationLevel1: ({
|
||||
blurPx: 4 * _elevMult,
|
||||
offsetX: 1 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 1 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.2 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel2: ({
|
||||
blurPx: 8 * _elevMult,
|
||||
offsetX: 4 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 4 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.25 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel3: ({
|
||||
blurPx: 12 * _elevMult,
|
||||
offsetX: 6 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 6 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel4: ({
|
||||
blurPx: 16 * _elevMult,
|
||||
offsetX: 8 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 8 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
readonly property var elevationLevel5: ({
|
||||
blurPx: 20 * _elevMult,
|
||||
offsetX: 10 * _elevMult * _elevDirXScale * _elevDirX,
|
||||
offsetY: 10 * _elevMult * _elevDirY,
|
||||
spreadPx: 0,
|
||||
alpha: 0.3 * _opMult
|
||||
})
|
||||
|
||||
function elevationOffsetMagnitude(level, fallback, direction) {
|
||||
if (!level) {
|
||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
||||
}
|
||||
const yMag = Math.abs(level.offsetY !== undefined ? level.offsetY : 0);
|
||||
if (yMag > 0)
|
||||
return yMag;
|
||||
const xMag = Math.abs(level.offsetX !== undefined ? level.offsetX : 0);
|
||||
if (xMag > 0) {
|
||||
if (direction === "left" || direction === "right")
|
||||
return xMag;
|
||||
return xMag / _elevDiagRatio;
|
||||
}
|
||||
return fallback !== undefined ? Math.abs(fallback) : 0;
|
||||
}
|
||||
|
||||
function elevationOffsetXFor(level, direction, fallback) {
|
||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
||||
switch (dir) {
|
||||
case "topLeft":
|
||||
case "bottomLeft":
|
||||
return mag * _elevDiagRatio;
|
||||
case "topRight":
|
||||
case "bottomRight":
|
||||
return -mag * _elevDiagRatio;
|
||||
case "left":
|
||||
return mag;
|
||||
case "right":
|
||||
return -mag;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function elevationOffsetYFor(level, direction, fallback) {
|
||||
const dir = normalizeElevationDirection(direction || elevationLightDirection);
|
||||
const mag = elevationOffsetMagnitude(level, fallback, dir);
|
||||
switch (dir) {
|
||||
case "bottom":
|
||||
case "bottomLeft":
|
||||
case "bottomRight":
|
||||
return -mag;
|
||||
case "left":
|
||||
case "right":
|
||||
return 0;
|
||||
default:
|
||||
return mag;
|
||||
}
|
||||
}
|
||||
|
||||
function elevationOffsetX(level, fallback) {
|
||||
return elevationOffsetXFor(level, elevationLightDirection, fallback);
|
||||
}
|
||||
|
||||
function elevationOffsetY(level, fallback) {
|
||||
return elevationOffsetYFor(level, elevationLightDirection, fallback);
|
||||
}
|
||||
|
||||
function elevationRenderPadding(level, direction, fallbackOffset, extraPadding, minPadding) {
|
||||
const dir = direction !== undefined ? direction : elevationLightDirection;
|
||||
const blur = (level && level.blurPx !== undefined) ? Math.max(0, level.blurPx) : 0;
|
||||
const spread = (level && level.spreadPx !== undefined) ? Math.max(0, level.spreadPx) : 0;
|
||||
const fallback = fallbackOffset !== undefined ? fallbackOffset : 0;
|
||||
const extra = extraPadding !== undefined ? extraPadding : 8;
|
||||
const minPad = minPadding !== undefined ? minPadding : 16;
|
||||
const offsetX = Math.abs(elevationOffsetXFor(level, dir, fallback));
|
||||
const offsetY = Math.abs(elevationOffsetYFor(level, dir, fallback));
|
||||
return Math.max(minPad, blur + spread + Math.max(offsetX, offsetY) + extra);
|
||||
}
|
||||
|
||||
function elevationShadowColor(level) {
|
||||
const alpha = (level && level.alpha !== undefined) ? level.alpha : 0.3;
|
||||
let r = 0;
|
||||
let g = 0;
|
||||
let b = 0;
|
||||
|
||||
if (typeof SettingsData !== "undefined") {
|
||||
const mode = SettingsData.m3ElevationColorMode || "default";
|
||||
if (mode === "default") {
|
||||
r = 0;
|
||||
g = 0;
|
||||
b = 0;
|
||||
} else if (mode === "text") {
|
||||
r = surfaceText.r;
|
||||
g = surfaceText.g;
|
||||
b = surfaceText.b;
|
||||
} else if (mode === "primary") {
|
||||
r = primary.r;
|
||||
g = primary.g;
|
||||
b = primary.b;
|
||||
} else if (mode === "surfaceVariant") {
|
||||
r = surfaceVariant.r;
|
||||
g = surfaceVariant.g;
|
||||
b = surfaceVariant.b;
|
||||
} else if (mode === "custom" && SettingsData.m3ElevationCustomColor) {
|
||||
const c = Qt.color(SettingsData.m3ElevationCustomColor);
|
||||
r = c.r;
|
||||
g = c.g;
|
||||
b = c.b;
|
||||
}
|
||||
}
|
||||
return Qt.rgba(r, g, b, alpha);
|
||||
}
|
||||
function elevationTintOpacity(level) {
|
||||
if (!level)
|
||||
return 0;
|
||||
if (level === elevationLevel1)
|
||||
return 0.05;
|
||||
if (level === elevationLevel2)
|
||||
return 0.08;
|
||||
if (level === elevationLevel3)
|
||||
return 0.11;
|
||||
if (level === elevationLevel4)
|
||||
return 0.12;
|
||||
if (level === elevationLevel5)
|
||||
return 0.14;
|
||||
return 0.08;
|
||||
}
|
||||
|
||||
readonly property var animationDurations: [
|
||||
{
|
||||
"shorter": 0,
|
||||
@@ -734,6 +960,24 @@ Singleton {
|
||||
"expressiveEffects": [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
// Delegates to AnimVariants.qml for curves, timing, scale, and offsets.
|
||||
readonly property list<real> variantEnterCurve: AnimVariants.variantEnterCurve
|
||||
readonly property list<real> variantExitCurve: AnimVariants.variantExitCurve
|
||||
readonly property list<real> variantModalEnterCurve: AnimVariants.variantModalEnterCurve
|
||||
readonly property list<real> variantModalExitCurve: AnimVariants.variantModalExitCurve
|
||||
readonly property list<real> variantPopoutEnterCurve: AnimVariants.variantPopoutEnterCurve
|
||||
readonly property list<real> variantPopoutExitCurve: AnimVariants.variantPopoutExitCurve
|
||||
readonly property real variantEnterDurationFactor: AnimVariants.variantEnterDurationFactor
|
||||
readonly property real variantExitDurationFactor: AnimVariants.variantExitDurationFactor
|
||||
readonly property real variantOpacityDurationScale: AnimVariants.variantOpacityDurationScale
|
||||
readonly property bool isDirectionalEffect: AnimVariants.isDirectionalEffect
|
||||
readonly property bool isDepthEffect: AnimVariants.isDepthEffect
|
||||
readonly property real effectScaleCollapsed: AnimVariants.effectScaleCollapsed
|
||||
readonly property real effectAnimOffset: AnimVariants.effectAnimOffset
|
||||
function variantDuration(baseDuration, entering) { return AnimVariants.variantDuration(baseDuration, entering); }
|
||||
function variantExitCleanupPadding() { return AnimVariants.variantExitCleanupPadding(); }
|
||||
function variantCloseInterval(baseDuration) { return AnimVariants.variantCloseInterval(baseDuration); }
|
||||
|
||||
readonly property var animationPresetDurations: {
|
||||
"none": 0,
|
||||
"short": 250,
|
||||
|
||||
@@ -18,6 +18,10 @@ Singleton {
|
||||
fprintdDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function detectU2f() {
|
||||
u2fDetectionProcess.running = true;
|
||||
}
|
||||
|
||||
function checkPluginSettings() {
|
||||
pluginSettingsCheckProcess.running = true;
|
||||
}
|
||||
@@ -57,6 +61,16 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
property var u2fDetectionProcess: Process {
|
||||
command: ["sh", "-c", "(test -f /usr/lib/security/pam_u2f.so || test -f /usr/lib64/security/pam_u2f.so) && (test -f /etc/pam.d/dankshell-u2f || test -f \"$HOME/.config/Yubico/u2f_keys\")"]
|
||||
running: false
|
||||
onExited: function (exitCode) {
|
||||
if (!settingsRoot)
|
||||
return;
|
||||
settingsRoot.u2fAvailable = (exitCode === 0);
|
||||
}
|
||||
}
|
||||
|
||||
property var pluginSettingsCheckProcess: Process {
|
||||
command: ["test", "-f", settingsRoot?.pluginSettingsPath || ""]
|
||||
running: false
|
||||
|
||||
@@ -79,6 +79,9 @@ var SPEC = {
|
||||
hiddenOutputDeviceNames: { def: [] },
|
||||
hiddenInputDeviceNames: { def: [] },
|
||||
|
||||
locale: { def: "", onChange: "updateLocale" },
|
||||
timeLocale: { def: "" },
|
||||
|
||||
launcherLastMode: { def: "all" },
|
||||
appDrawerLastMode: { def: "apps" },
|
||||
niriOverviewLastMode: { def: "apps" }
|
||||
|
||||
@@ -21,7 +21,7 @@ var SPEC = {
|
||||
widgetColorMode: { def: "default" },
|
||||
controlCenterTileColorMode: { def: "primary" },
|
||||
buttonColorMode: { def: "primary" },
|
||||
cornerRadius: { def: 12, onChange: "updateCompositorLayout" },
|
||||
cornerRadius: { def: 16, onChange: "updateCompositorLayout" },
|
||||
niriLayoutGapsOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
niriLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
niriLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
@@ -32,6 +32,7 @@ var SPEC = {
|
||||
mangoLayoutRadiusOverride: { def: -1, onChange: "updateCompositorLayout" },
|
||||
mangoLayoutBorderSize: { def: -1, onChange: "updateCompositorLayout" },
|
||||
|
||||
firstDayOfWeek: { def: -1 },
|
||||
use24HourClock: { def: true },
|
||||
showSeconds: { def: false },
|
||||
padHours12Hour: { def: false },
|
||||
@@ -46,6 +47,18 @@ var SPEC = {
|
||||
modalAnimationSpeed: { def: 1 },
|
||||
modalCustomAnimationDuration: { def: 150 },
|
||||
enableRippleEffects: { def: true },
|
||||
animationVariant: { def: 0 },
|
||||
motionEffect: { def: 0 },
|
||||
directionalAnimationMode: { def: 0 },
|
||||
m3ElevationEnabled: { def: true },
|
||||
m3ElevationIntensity: { def: 12 },
|
||||
m3ElevationOpacity: { def: 30 },
|
||||
m3ElevationColorMode: { def: "default" },
|
||||
m3ElevationLightDirection: { def: "top" },
|
||||
m3ElevationCustomColor: { def: "#000000" },
|
||||
modalElevationEnabled: { def: true },
|
||||
popoutElevationEnabled: { def: true },
|
||||
barElevationEnabled: { def: true },
|
||||
wallpaperFillMode: { def: "Fill" },
|
||||
blurredWallpaperLayer: { def: false },
|
||||
blurWallpaperOnOverview: { def: false },
|
||||
@@ -317,9 +330,15 @@ var SPEC = {
|
||||
enableFprint: { def: false },
|
||||
maxFprintTries: { def: 15 },
|
||||
fprintdAvailable: { def: false, persist: false },
|
||||
enableU2f: { def: false },
|
||||
u2fMode: { def: "or" },
|
||||
u2fAvailable: { def: false, persist: false },
|
||||
lockScreenActiveMonitor: { def: "all" },
|
||||
lockScreenInactiveColor: { def: "#000000" },
|
||||
lockScreenNotificationMode: { def: 0 },
|
||||
lockScreenVideoEnabled: { def: false },
|
||||
lockScreenVideoPath: { def: "" },
|
||||
lockScreenVideoCycling: { def: false },
|
||||
hideBrightnessSlider: { def: false },
|
||||
|
||||
notificationTimeoutLow: { def: 5000 },
|
||||
@@ -398,7 +417,7 @@ var SPEC = {
|
||||
maximizeWidgetIcons: false,
|
||||
maximizeWidgetText: false,
|
||||
removeWidgetPadding: false,
|
||||
widgetPadding: 12,
|
||||
widgetPadding: 8,
|
||||
gothCornersEnabled: false,
|
||||
gothCornerRadiusOverride: false,
|
||||
gothCornerRadiusValue: 12,
|
||||
@@ -425,7 +444,7 @@ var SPEC = {
|
||||
scrollYBehavior: "workspace",
|
||||
shadowIntensity: 0,
|
||||
shadowOpacity: 60,
|
||||
shadowColorMode: "text",
|
||||
shadowColorMode: "default",
|
||||
shadowCustomColor: "#000000",
|
||||
clickThrough: false
|
||||
}], onChange: "updateBarConfigs"
|
||||
|
||||
@@ -9,6 +9,9 @@ function parse(root, jsonObj) {
|
||||
|
||||
for (var k in SPEC) {
|
||||
if (k === "pluginSettings") continue;
|
||||
// Runtime-only keys are never in the JSON; resetting them here
|
||||
// would wipe values set by detection processes on every reload.
|
||||
if (SPEC[k].persist === false) continue;
|
||||
if (!(k in jsonObj)) {
|
||||
root[k] = SPEC[k].def;
|
||||
}
|
||||
@@ -226,6 +229,25 @@ function migrateToVersion(obj, targetVersion) {
|
||||
settings.configVersion = 5;
|
||||
}
|
||||
|
||||
if (currentVersion < 6) {
|
||||
console.info("Migrating settings from version", currentVersion, "to version 6");
|
||||
|
||||
if (settings.barElevationEnabled === undefined) {
|
||||
var legacyBars = Array.isArray(settings.barConfigs) ? settings.barConfigs : [];
|
||||
var hadLegacyBarShadowEnabled = false;
|
||||
for (var j = 0; j < legacyBars.length; j++) {
|
||||
var legacyIntensity = Number(legacyBars[j] && legacyBars[j].shadowIntensity);
|
||||
if (!isNaN(legacyIntensity) && legacyIntensity > 0) {
|
||||
hadLegacyBarShadowEnabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
settings.barElevationEnabled = hadLegacyBarShadowEnabled;
|
||||
}
|
||||
|
||||
settings.configVersion = 6;
|
||||
}
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
|
||||
@@ -142,25 +142,45 @@ Item {
|
||||
fadeDpmsWindowLoader.item.cancelFade();
|
||||
}
|
||||
}
|
||||
|
||||
function onRequestMonitorOn() {
|
||||
if (!fadeDpmsWindowLoader.item)
|
||||
return;
|
||||
fadeDpmsWindowLoader.item.cancelFade();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property string _barLayoutStateJson: {
|
||||
const configs = SettingsData.barConfigs;
|
||||
const mapped = configs.map(c => ({
|
||||
id: c.id,
|
||||
position: c.position,
|
||||
autoHide: c.autoHide,
|
||||
visible: c.visible
|
||||
})).sort((a, b) => {
|
||||
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
||||
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
||||
if (aVertical !== bVertical) {
|
||||
return aVertical - bVertical;
|
||||
}
|
||||
return String(a.id).localeCompare(String(b.id));
|
||||
});
|
||||
return JSON.stringify(mapped);
|
||||
}
|
||||
|
||||
on_BarLayoutStateJsonChanged: {
|
||||
if (typeof dockRecreateDebounce !== "undefined") {
|
||||
dockRecreateDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Repeater {
|
||||
id: dankBarRepeater
|
||||
model: ScriptModel {
|
||||
id: barRepeaterModel
|
||||
values: {
|
||||
const configs = SettingsData.barConfigs;
|
||||
return configs.map(c => ({
|
||||
id: c.id,
|
||||
position: c.position
|
||||
})).sort((a, b) => {
|
||||
const aVertical = a.position === SettingsData.Position.Left || a.position === SettingsData.Position.Right;
|
||||
const bVertical = b.position === SettingsData.Position.Left || b.position === SettingsData.Position.Right;
|
||||
return aVertical - bVertical;
|
||||
});
|
||||
}
|
||||
values: JSON.parse(root._barLayoutStateJson)
|
||||
}
|
||||
|
||||
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
|
||||
@@ -207,13 +227,6 @@ Item {
|
||||
PolkitService.polkitAvailable;
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
dockRecreateDebounce.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: dockLoader
|
||||
active: root.dockEnabled
|
||||
@@ -265,6 +278,7 @@ Item {
|
||||
sourceComponent: Component {
|
||||
DankDashPopout {
|
||||
id: dankDashPopout
|
||||
onPopoutClosed: PopoutService.unloadDankDash()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -284,8 +298,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationCenterLoader = notificationCenterLoader;
|
||||
}
|
||||
|
||||
NotificationCenterPopout {
|
||||
id: notificationCenter
|
||||
onPopoutClosed: PopoutService.unloadNotificationCenter()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.notificationCenterPopout = notificationCenter;
|
||||
@@ -309,10 +328,15 @@ Item {
|
||||
property var modalRef: colorPickerModal
|
||||
property LazyLoader powerModalLoaderRef: powerMenuModalLoader
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.controlCenterLoader = controlCenterLoader;
|
||||
}
|
||||
|
||||
ControlCenterPopout {
|
||||
id: controlCenterPopout
|
||||
colorPickerModal: controlCenterLoader.modalRef
|
||||
powerMenuModalLoader: controlCenterLoader.powerModalLoaderRef
|
||||
onPopoutClosed: PopoutService.unloadControlCenter()
|
||||
|
||||
onLockRequested: {
|
||||
lock.activate();
|
||||
@@ -437,8 +461,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.batteryPopoutLoader = batteryPopoutLoader;
|
||||
}
|
||||
|
||||
BatteryPopout {
|
||||
id: batteryPopout
|
||||
onPopoutClosed: PopoutService.unloadBattery()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.batteryPopout = batteryPopout;
|
||||
@@ -451,8 +480,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.layoutPopoutLoader = layoutPopoutLoader;
|
||||
}
|
||||
|
||||
DWLLayoutPopout {
|
||||
id: layoutPopout
|
||||
onPopoutClosed: PopoutService.unloadLayoutPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.layoutPopout = layoutPopout;
|
||||
@@ -465,8 +499,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.vpnPopoutLoader = vpnPopoutLoader;
|
||||
}
|
||||
|
||||
VpnPopout {
|
||||
id: vpnPopout
|
||||
onPopoutClosed: PopoutService.unloadVpn()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.vpnPopout = vpnPopout;
|
||||
@@ -479,8 +518,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListPopoutLoader = processListPopoutLoader;
|
||||
}
|
||||
|
||||
ProcessListPopout {
|
||||
id: processListPopout
|
||||
onPopoutClosed: PopoutService.unloadProcessListPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.processListPopout = processListPopout;
|
||||
@@ -523,8 +567,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.appDrawerLoader = appDrawerLoader;
|
||||
}
|
||||
|
||||
AppDrawerPopout {
|
||||
id: appDrawerPopout
|
||||
onPopoutClosed: PopoutService.unloadAppDrawer()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.appDrawerPopout = appDrawerPopout;
|
||||
@@ -556,8 +605,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.clipboardHistoryPopoutLoader = clipboardHistoryPopoutLoader;
|
||||
}
|
||||
|
||||
ClipboardHistoryPopout {
|
||||
id: clipboardHistoryPopout
|
||||
onPopoutClosed: PopoutService.unloadClipboardHistoryPopout()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.clipboardHistoryPopout = clipboardHistoryPopout;
|
||||
@@ -732,8 +786,13 @@ Item {
|
||||
|
||||
active: false
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdateLoader = systemUpdateLoader;
|
||||
}
|
||||
|
||||
SystemUpdatePopout {
|
||||
id: systemUpdatePopout
|
||||
onPopoutClosed: PopoutService.unloadSystemUpdate()
|
||||
|
||||
Component.onCompleted: {
|
||||
PopoutService.systemUpdatePopout = systemUpdatePopout;
|
||||
|
||||
@@ -21,11 +21,37 @@ Item {
|
||||
required property var workspaceRenameModalLoader
|
||||
required property var windowRuleModalLoader
|
||||
|
||||
function getFirstBar() {
|
||||
function getPreferredBar(refPropertyName) {
|
||||
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
|
||||
return null;
|
||||
const firstLoader = root.dankBarRepeater.itemAt(0);
|
||||
return firstLoader ? firstLoader.item : null;
|
||||
|
||||
const focusedScreenName = BarWidgetService.getFocusedScreenName();
|
||||
|
||||
const loaders = Array.from({
|
||||
length: root.dankBarRepeater.count
|
||||
}, (_, i) => root.dankBarRepeater.itemAt(i));
|
||||
|
||||
let currentBar = null;
|
||||
|
||||
for (const loader of loaders) {
|
||||
const instances = loader?.item?.barVariants?.instances || [];
|
||||
for (const bar of instances) {
|
||||
if (!bar)
|
||||
continue;
|
||||
|
||||
const onFocusedScreen = focusedScreenName && bar.modelData?.name === focusedScreenName;
|
||||
const hasRef = !refPropertyName || !!bar[refPropertyName];
|
||||
|
||||
if (hasRef) {
|
||||
currentBar = bar;
|
||||
|
||||
if (onFocusedScreen)
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return currentBar;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
@@ -97,9 +123,9 @@ Item {
|
||||
|
||||
IpcHandler {
|
||||
function open(): string {
|
||||
const bar = root.getFirstBar();
|
||||
const bar = root.getPreferredBar("controlCenterButtonRef");
|
||||
if (bar) {
|
||||
bar.triggerControlCenterOnFocusedScreen();
|
||||
bar.triggerControlCenter();
|
||||
return "CONTROL_CENTER_OPEN_SUCCESS";
|
||||
}
|
||||
return "CONTROL_CENTER_OPEN_FAILED";
|
||||
@@ -114,9 +140,14 @@ Item {
|
||||
}
|
||||
|
||||
function toggle(): string {
|
||||
const bar = root.getFirstBar();
|
||||
if (root.controlCenterLoader.item?.shouldBeVisible) {
|
||||
root.controlCenterLoader.item.close();
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
const bar = root.getPreferredBar("controlCenterButtonRef");
|
||||
if (bar) {
|
||||
bar.triggerControlCenterOnFocusedScreen();
|
||||
bar.triggerControlCenter();
|
||||
return "CONTROL_CENTER_TOGGLE_SUCCESS";
|
||||
}
|
||||
return "CONTROL_CENTER_TOGGLE_FAILED";
|
||||
@@ -131,27 +162,37 @@ Item {
|
||||
|
||||
IpcHandler {
|
||||
function open(tab: string): string {
|
||||
root.dankDashPopoutLoader.active = true;
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||
break;
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||
break;
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||
break;
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||
break;
|
||||
}
|
||||
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
|
||||
root.dankDashPopoutLoader.item.dashVisible = true;
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
const bar = root.getPreferredBar("clockButtonRef");
|
||||
if (!bar)
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
const dash = root.dankDashPopoutLoader.item;
|
||||
const onSameScreen = dash && dash.shouldBeVisible && dash.triggerScreen?.name === bar.screen?.name;
|
||||
|
||||
if (!onSameScreen) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
}
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
if (!root.dankDashPopoutLoader.item)
|
||||
return "DASH_OPEN_FAILED";
|
||||
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 1;
|
||||
break;
|
||||
case "wallpaper":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 2;
|
||||
break;
|
||||
case "weather":
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
|
||||
break;
|
||||
default:
|
||||
root.dankDashPopoutLoader.item.currentTabIndex = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
root.dankDashPopoutLoader.item.dashVisible = true;
|
||||
return "DASH_OPEN_SUCCESS";
|
||||
}
|
||||
|
||||
function close(): string {
|
||||
@@ -163,8 +204,14 @@ Item {
|
||||
}
|
||||
|
||||
function toggle(tab: string): string {
|
||||
const bar = root.getFirstBar();
|
||||
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
if (root.dankDashPopoutLoader.item?.dashVisible) {
|
||||
root.dankDashPopoutLoader.item.dashVisible = false;
|
||||
return "DASH_TOGGLE_SUCCESS";
|
||||
}
|
||||
|
||||
const bar = root.getPreferredBar("clockButtonRef");
|
||||
if (bar) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
if (root.dankDashPopoutLoader.item) {
|
||||
switch (tab.toLowerCase()) {
|
||||
case "media":
|
||||
@@ -521,8 +568,9 @@ Item {
|
||||
|
||||
IpcHandler {
|
||||
function wallpaper(): string {
|
||||
const bar = root.getFirstBar();
|
||||
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
|
||||
const bar = root.getPreferredBar("clockButtonRef");
|
||||
if (bar) {
|
||||
bar.triggerWallpaperBrowser();
|
||||
return "SUCCESS: Toggled wallpaper browser";
|
||||
}
|
||||
return "ERROR: Failed to toggle wallpaper browser";
|
||||
|
||||
@@ -86,7 +86,7 @@ Item {
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.bottomMargin: modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0
|
||||
anchors.bottomMargin: (modal.showKeyboardHints ? (ClipboardConstants.keyboardHintsHeight + Theme.spacingM * 2) : 0) + Theme.spacingXS
|
||||
clip: true
|
||||
|
||||
DankListView {
|
||||
@@ -112,14 +112,7 @@ Item {
|
||||
if (index < 0 || index >= count) {
|
||||
return;
|
||||
}
|
||||
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
||||
const itemY = index * itemHeight;
|
||||
const itemBottom = itemY + itemHeight;
|
||||
if (itemY < contentY) {
|
||||
contentY = itemY;
|
||||
} else if (itemBottom > contentY + height) {
|
||||
contentY = itemBottom - height;
|
||||
}
|
||||
positionViewAtIndex(index, ListView.Contain);
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
@@ -178,14 +171,7 @@ Item {
|
||||
if (index < 0 || index >= count) {
|
||||
return;
|
||||
}
|
||||
const itemHeight = ClipboardConstants.itemHeight + spacing;
|
||||
const itemY = index * itemHeight;
|
||||
const itemBottom = itemY + itemHeight;
|
||||
if (itemY < contentY) {
|
||||
contentY = itemY;
|
||||
} else if (itemBottom > contentY + height) {
|
||||
contentY = itemBottom - height;
|
||||
}
|
||||
positionViewAtIndex(index, ListView.Contain);
|
||||
}
|
||||
|
||||
onCurrentIndexChanged: {
|
||||
|
||||
@@ -31,13 +31,13 @@ Item {
|
||||
sourceSize.height: 128
|
||||
|
||||
function tryLoadImage() {
|
||||
if (loadQueued || entryType !== "image" || cachedImageData) {
|
||||
if (thumbnailImage.loadQueued || entryType !== "image" || thumbnailImage.cachedImageData) {
|
||||
return;
|
||||
}
|
||||
loadQueued = true;
|
||||
thumbnailImage.loadQueued = true;
|
||||
if (modal.activeImageLoads < modal.maxConcurrentLoads) {
|
||||
modal.activeImageLoads++;
|
||||
loadImage();
|
||||
thumbnailImage.loadImage();
|
||||
} else {
|
||||
retryTimer.restart();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ Item {
|
||||
DMSService.sendRequest("clipboard.getEntry", {
|
||||
"id": entry.id
|
||||
}, function (response) {
|
||||
loadQueued = false;
|
||||
thumbnailImage.loadQueued = false;
|
||||
if (modal.activeImageLoads > 0) {
|
||||
modal.activeImageLoads--;
|
||||
}
|
||||
@@ -57,7 +57,7 @@ Item {
|
||||
}
|
||||
const data = response.result?.data;
|
||||
if (data) {
|
||||
cachedImageData = data;
|
||||
thumbnailImage.cachedImageData = data;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -26,15 +26,15 @@ Item {
|
||||
property bool closeOnBackgroundClick: true
|
||||
property string animationType: "scale"
|
||||
property int animationDuration: Theme.modalAnimationDuration
|
||||
property real animationScaleCollapsed: 0.96
|
||||
property real animationOffset: Theme.spacingL
|
||||
property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
|
||||
property real animationScaleCollapsed: Theme.effectScaleCollapsed
|
||||
property real animationOffset: Theme.effectAnimOffset
|
||||
property list<real> animationEnterCurve: Theme.variantModalEnterCurve
|
||||
property list<real> animationExitCurve: Theme.variantModalExitCurve
|
||||
property color backgroundColor: Theme.surfaceContainer
|
||||
property color borderColor: Theme.outlineMedium
|
||||
property real borderWidth: 1
|
||||
property real borderWidth: 0
|
||||
property real cornerRadius: Theme.cornerRadius
|
||||
property bool enableShadow: false
|
||||
property bool enableShadow: true
|
||||
property alias modalFocusScope: focusScope
|
||||
property bool shouldBeVisible: false
|
||||
property bool shouldHaveFocus: shouldBeVisible
|
||||
@@ -44,11 +44,13 @@ Item {
|
||||
property bool keepPopoutsOpen: false
|
||||
property var customKeyboardFocus: null
|
||||
property bool useOverlayLayer: false
|
||||
property real frozenMotionOffsetX: 0
|
||||
property real frozenMotionOffsetY: 0
|
||||
readonly property alias contentWindow: contentWindow
|
||||
readonly property alias clickCatcher: clickCatcher
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property bool useBackground: showBackground && SettingsData.modalDarkenBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland || useBackground
|
||||
readonly property bool useSingleWindow: CompositorService.isHyprland
|
||||
|
||||
signal opened
|
||||
signal dialogClosed
|
||||
@@ -58,19 +60,34 @@ Item {
|
||||
|
||||
function open() {
|
||||
closeTimer.stop();
|
||||
animationsEnabled = false;
|
||||
frozenMotionOffsetX = modalContainer ? modalContainer.offsetX : 0;
|
||||
frozenMotionOffsetY = modalContainer ? modalContainer.offsetY : animationOffset;
|
||||
|
||||
const focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
contentWindow.screen = focusedScreen;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.screen = focusedScreen;
|
||||
}
|
||||
|
||||
if (Theme.isDirectionalEffect || root.useBackground) {
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
}
|
||||
ModalManager.openModal(root);
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow)
|
||||
clickCatcher.visible = true;
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
|
||||
Qt.callLater(() => {
|
||||
animationsEnabled = true;
|
||||
shouldBeVisible = true;
|
||||
if (!useSingleWindow && !clickCatcher.visible)
|
||||
clickCatcher.visible = true;
|
||||
if (!contentWindow.visible)
|
||||
contentWindow.visible = true;
|
||||
shouldHaveFocus = false;
|
||||
Qt.callLater(() => shouldHaveFocus = Qt.binding(() => shouldBeVisible));
|
||||
});
|
||||
}
|
||||
|
||||
function close() {
|
||||
@@ -131,7 +148,7 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeTimer
|
||||
interval: animationDuration + 50
|
||||
interval: Theme.variantCloseInterval(animationDuration)
|
||||
onTriggered: {
|
||||
if (shouldBeVisible)
|
||||
return;
|
||||
@@ -142,7 +159,21 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real shadowBuffer: 5
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowMotionPadding: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0 && Theme.isDirectionalEffect)
|
||||
return 0; // Wayland native overlap mask
|
||||
if (animationType === "slide")
|
||||
return 30;
|
||||
if (Theme.isDirectionalEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.9);
|
||||
if (Theme.isDepthEffect)
|
||||
return Math.max(Math.max(0, animationOffset), Math.max(alignedWidth, alignedHeight) * 0.35);
|
||||
return Math.max(0, animationOffset);
|
||||
}
|
||||
readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
|
||||
@@ -201,9 +232,26 @@ Item {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
enabled: !root.useSingleWindow && root.closeOnBackgroundClick && root.shouldBeVisible
|
||||
onClicked: root.backgroundClicked()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: (!root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PanelWindow {
|
||||
@@ -246,9 +294,12 @@ Item {
|
||||
bottom: root.useSingleWindow
|
||||
}
|
||||
|
||||
readonly property real actualMarginLeft: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
readonly property real actualMarginTop: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
|
||||
top: root.useSingleWindow ? 0 : Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
|
||||
left: actualMarginLeft
|
||||
top: actualMarginTop
|
||||
right: 0
|
||||
bottom: 0
|
||||
}
|
||||
@@ -278,13 +329,14 @@ Item {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
color: "black"
|
||||
opacity: root.useBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: root.useBackground
|
||||
opacity: (root.useSingleWindow && root.useBackground) ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
|
||||
visible: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Math.round(Theme.variantDuration(root.animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -292,8 +344,8 @@ Item {
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.useSingleWindow ? root.alignedX : shadowBuffer
|
||||
y: root.useSingleWindow ? root.alignedY : shadowBuffer
|
||||
x: (root.useSingleWindow ? root.alignedX : (root.alignedX - contentWindow.actualMarginLeft)) + Theme.snap(animX, root.dpr)
|
||||
y: (root.useSingleWindow ? root.alignedY : (root.alignedY - contentWindow.actualMarginTop)) + Theme.snap(animY, root.dpr)
|
||||
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
@@ -309,45 +361,117 @@ Item {
|
||||
}
|
||||
|
||||
readonly property bool slide: root.animationType === "slide"
|
||||
readonly property real offsetX: slide ? 15 : 0
|
||||
readonly property real offsetY: slide ? -30 : root.animationOffset
|
||||
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: root.animationScaleCollapsed
|
||||
|
||||
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
|
||||
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function onShouldBeVisibleChanged() {
|
||||
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr);
|
||||
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr);
|
||||
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real directionalTravel: Math.max(root.animationOffset, Math.max(root.alignedWidth, root.alignedHeight) * 0.8)
|
||||
readonly property real depthTravel: Math.max(root.animationOffset * 0.8, 36)
|
||||
readonly property real customAnchorX: root.alignedX + root.alignedWidth * 0.5
|
||||
readonly property real customAnchorY: root.alignedY + root.alignedHeight * 0.5
|
||||
readonly property real customDistLeft: customAnchorX
|
||||
readonly property real customDistRight: root.screenWidth - customAnchorX
|
||||
readonly property real customDistTop: customAnchorY
|
||||
readonly property real customDistBottom: root.screenHeight - customAnchorY
|
||||
readonly property real offsetX: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return 15;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -directionalTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return 0;
|
||||
case "custom":
|
||||
if (customDistLeft <= customDistRight && customDistLeft <= customDistTop && customDistLeft <= customDistBottom)
|
||||
return -depthTravel;
|
||||
if (customDistRight <= customDistTop && customDistRight <= customDistBottom)
|
||||
return depthTravel;
|
||||
return 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
readonly property real offsetY: {
|
||||
if (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect)
|
||||
return 0;
|
||||
if (slide && !directionalEffect && !depthEffect)
|
||||
return -30;
|
||||
if (directionalEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -Math.max(directionalTravel * 0.65, 96);
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -directionalTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return directionalTravel;
|
||||
return 0;
|
||||
default:
|
||||
// Default to sliding down from top when centered
|
||||
return -Math.max(directionalTravel, root.screenHeight * 0.24);
|
||||
}
|
||||
}
|
||||
if (depthEffect) {
|
||||
switch (root.positioning) {
|
||||
case "top-right":
|
||||
return -depthTravel * 0.75;
|
||||
case "custom":
|
||||
if (customDistTop <= customDistBottom && customDistTop <= customDistLeft && customDistTop <= customDistRight)
|
||||
return -depthTravel;
|
||||
if (customDistBottom <= customDistLeft && customDistBottom <= customDistRight)
|
||||
return depthTravel;
|
||||
return depthTravel * 0.45;
|
||||
default:
|
||||
return -depthTravel;
|
||||
}
|
||||
}
|
||||
return root.animationOffset;
|
||||
}
|
||||
|
||||
property real animX: root.shouldBeVisible ? 0 : root.frozenMotionOffsetX
|
||||
property real animY: root.shouldBeVisible ? 0 : root.frozenMotionOffsetY
|
||||
|
||||
readonly property real computedScaleCollapsed: (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 && Theme.isDirectionalEffect) ? 0.0 : root.animationScaleCollapsed
|
||||
property real scaleValue: root.shouldBeVisible ? 1.0 : computedScaleCollapsed
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: root.animationDuration
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(root.animationDuration, root.shouldBeVisible)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
@@ -363,26 +487,29 @@ Item {
|
||||
id: animatedContent
|
||||
anchors.fill: parent
|
||||
clip: false
|
||||
opacity: root.shouldBeVisible ? 1 : 0
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (root.shouldBeVisible ? 1 : 0)
|
||||
scale: modalContainer.scaleValue
|
||||
x: Theme.snap(modalContainer.animX, root.dpr) + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5
|
||||
y: Theme.snap(modalContainer.animY, root.dpr) + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: animationDuration
|
||||
duration: Math.round(Theme.variantDuration(animationDuration, root.shouldBeVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
ElevationShadow {
|
||||
id: modalShadowLayer
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetRadius: root.cornerRadius
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
shadowEnabled: root.enableShadow && Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
|
||||
@@ -51,6 +51,15 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
onSearchModeChanged: {
|
||||
if (searchMode === "apps") {
|
||||
_loadAppCategories();
|
||||
} else {
|
||||
appCategory = "";
|
||||
appCategories = [];
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onSortAppsAlphabeticallyChanged() {
|
||||
@@ -65,8 +74,12 @@ Item {
|
||||
if (!active)
|
||||
return;
|
||||
_clearModeCache();
|
||||
if (!searchQuery && searchMode === "all")
|
||||
if (searchMode === "apps") {
|
||||
_loadAppCategories();
|
||||
performSearch();
|
||||
} else if (!searchQuery && searchMode === "all") {
|
||||
performSearch();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,6 +184,8 @@ Item {
|
||||
property string activePluginName: ""
|
||||
property var activePluginCategories: []
|
||||
property string activePluginCategory: ""
|
||||
property string appCategory: ""
|
||||
property var appCategories: []
|
||||
|
||||
function getSectionViewMode(sectionId) {
|
||||
if (sectionId === "browse_plugins")
|
||||
@@ -364,6 +379,8 @@ Item {
|
||||
activePluginName = "";
|
||||
activePluginCategories = [];
|
||||
activePluginCategory = "";
|
||||
appCategory = "";
|
||||
appCategories = [];
|
||||
pluginFilter = "";
|
||||
collapsedSections = {};
|
||||
_clearModeCache();
|
||||
@@ -408,6 +425,19 @@ Item {
|
||||
performSearch();
|
||||
}
|
||||
|
||||
function setAppCategory(category) {
|
||||
if (appCategory === category)
|
||||
return;
|
||||
appCategory = category;
|
||||
_queryDrivenSearch = true;
|
||||
_clearModeCache();
|
||||
performSearch();
|
||||
}
|
||||
|
||||
function _loadAppCategories() {
|
||||
appCategories = AppSearchService.getAllCategories();
|
||||
}
|
||||
|
||||
function setFileSearchType(type) {
|
||||
if (fileSearchType === type)
|
||||
return;
|
||||
@@ -592,8 +622,9 @@ Item {
|
||||
}
|
||||
|
||||
if (searchMode === "apps") {
|
||||
var isCategoryFiltered = appCategory && appCategory !== I18n.tr("All");
|
||||
var cachedSections = AppSearchService.getCachedDefaultSections();
|
||||
if (cachedSections && !searchQuery) {
|
||||
if (cachedSections && !searchQuery && !isCategoryFiltered) {
|
||||
var modeCache = _getCachedModeData("apps");
|
||||
if (modeCache) {
|
||||
_applyHighlights(modeCache.sections, "");
|
||||
@@ -623,9 +654,23 @@ Item {
|
||||
return;
|
||||
}
|
||||
|
||||
var apps = searchApps(searchQuery);
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
allItems.push(apps[i]);
|
||||
if (isCategoryFiltered) {
|
||||
var rawApps = AppSearchService.getAppsInCategory(appCategory);
|
||||
for (var i = 0; i < rawApps.length; i++) {
|
||||
allItems.push(getOrTransformApp(rawApps[i]));
|
||||
}
|
||||
// Also include core apps (DMS Settings etc.) that match this category
|
||||
var allCoreApps = AppSearchService.getCoreApps("");
|
||||
for (var i = 0; i < allCoreApps.length; i++) {
|
||||
var coreAppCats = AppSearchService.getCategoriesForApp(allCoreApps[i]);
|
||||
if (coreAppCats.indexOf(appCategory) !== -1)
|
||||
allItems.push(transformCoreApp(allCoreApps[i]));
|
||||
}
|
||||
} else {
|
||||
var apps = searchApps(searchQuery);
|
||||
for (var i = 0; i < apps.length; i++) {
|
||||
allItems.push(apps[i]);
|
||||
}
|
||||
}
|
||||
|
||||
var scoredItems = Scorer.scoreItems(allItems, searchQuery, getFrecencyForItem);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
@@ -13,6 +14,7 @@ Item {
|
||||
property bool spotlightOpen: false
|
||||
property bool keyboardActive: false
|
||||
property bool contentVisible: false
|
||||
readonly property bool launcherMotionVisible: Theme.isDirectionalEffect ? spotlightOpen : _motionActive
|
||||
property var spotlightContent: launcherContentLoader.item
|
||||
property bool openedFromOverview: false
|
||||
property bool isClosing: false
|
||||
@@ -22,8 +24,14 @@ Item {
|
||||
property string _pendingMode: ""
|
||||
readonly property bool unloadContentOnClose: SettingsData.dankLauncherV2UnloadOnClose
|
||||
|
||||
// Animation state — matches DankPopout/DankModal pattern
|
||||
property bool animationsEnabled: true
|
||||
property bool _motionActive: false
|
||||
property real _frozenMotionX: 0
|
||||
property real _frozenMotionY: 0
|
||||
|
||||
readonly property bool useHyprlandFocusGrab: CompositorService.useHyprlandFocusGrab
|
||||
readonly property var effectiveScreen: launcherWindow.screen
|
||||
readonly property var effectiveScreen: contentWindow.screen
|
||||
readonly property real screenWidth: effectiveScreen?.width ?? 1920
|
||||
readonly property real screenHeight: effectiveScreen?.height ?? 1080
|
||||
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
|
||||
@@ -75,7 +83,35 @@ Item {
|
||||
return Theme.primary;
|
||||
}
|
||||
}
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 1
|
||||
readonly property int borderWidth: SettingsData.dankLauncherV2BorderEnabled ? SettingsData.dankLauncherV2BorderThickness : 0
|
||||
|
||||
// Shadow padding for the content window (render padding only, no motion padding)
|
||||
readonly property var shadowLevel: Theme.elevationLevel3
|
||||
readonly property real shadowFallbackOffset: 6
|
||||
readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.modalElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, Theme.elevationLightDirection, shadowFallbackOffset, 8, 16) : 0
|
||||
readonly property real shadowPad: Theme.snap(shadowRenderPadding, dpr)
|
||||
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
|
||||
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
|
||||
readonly property real alignedX: Theme.snap(modalX, dpr)
|
||||
readonly property real alignedY: Theme.snap(modalY, dpr)
|
||||
|
||||
// For directional/depth: window extends from screen top (content slides within)
|
||||
// For standard: small window tightly around the modal + shadow padding
|
||||
readonly property bool _needsExtendedWindow: Theme.isDirectionalEffect || Theme.isDepthEffect
|
||||
// Content window geometry
|
||||
readonly property real _cwMarginLeft: Theme.snap(alignedX - shadowPad, dpr)
|
||||
readonly property real _cwMarginTop: _needsExtendedWindow ? 0 : Theme.snap(alignedY - shadowPad, dpr)
|
||||
readonly property real _cwWidth: alignedWidth + shadowPad * 2
|
||||
readonly property real _cwHeight: {
|
||||
if (Theme.isDirectionalEffect)
|
||||
return screenHeight + shadowPad;
|
||||
if (Theme.isDepthEffect)
|
||||
return alignedY + alignedHeight + shadowPad;
|
||||
return alignedHeight + shadowPad * 2;
|
||||
}
|
||||
// Where the content container sits inside the content window
|
||||
readonly property real _ccX: shadowPad
|
||||
readonly property real _ccY: _needsExtendedWindow ? alignedY : shadowPad
|
||||
|
||||
signal dialogClosed
|
||||
|
||||
@@ -96,7 +132,8 @@ Item {
|
||||
if (!spotlightContent)
|
||||
return;
|
||||
contentVisible = true;
|
||||
spotlightContent.searchField.forceActiveFocus();
|
||||
// NOTE: forceActiveFocus() is deliberately NOT called here.
|
||||
// It is deferred to after animation starts to avoid compositor IPC stalls.
|
||||
|
||||
if (spotlightContent.searchField) {
|
||||
spotlightContent.searchField.text = query;
|
||||
@@ -129,40 +166,59 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
function show() {
|
||||
function _openCommon(query, mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
// Disable animations so the snap is instant
|
||||
animationsEnabled = false;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
// Freeze the collapsed offsets (they depend on height which could change)
|
||||
_frozenMotionX = contentContainer ? contentContainer.collapsedMotionX : 0;
|
||||
_frozenMotionY = contentContainer ? contentContainer.collapsedMotionY : (Theme.isDirectionalEffect ? Math.max(root.screenHeight - root._ccY + root.shadowPad, Theme.effectAnimOffset * 1.1) : -Theme.effectAnimOffset);
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen) {
|
||||
backgroundWindow.screen = focusedScreen;
|
||||
contentWindow.screen = focusedScreen;
|
||||
}
|
||||
|
||||
// _motionActive = false ensures motionX/Y snap to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
// Make windows visible but do NOT request keyboard focus yet
|
||||
ModalManager.openModal(root);
|
||||
spotlightOpen = true;
|
||||
backgroundWindow.visible = true;
|
||||
contentWindow.visible = true;
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", "");
|
||||
// Load content and initialize (but no forceActiveFocus — that's deferred)
|
||||
_ensureContentLoadedAndInitialize(query || "", mode || "");
|
||||
|
||||
// Frame 1: enable animations and trigger enter motion
|
||||
Qt.callLater(() => {
|
||||
root.animationsEnabled = true;
|
||||
root._motionActive = true;
|
||||
|
||||
// Frame 2: request keyboard focus + activate search field
|
||||
// Double-deferred to avoid compositor IPC competing with animation frames
|
||||
Qt.callLater(() => {
|
||||
root.keyboardActive = true;
|
||||
if (root.spotlightContent && root.spotlightContent.searchField)
|
||||
root.spotlightContent.searchField.forceActiveFocus();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function show() {
|
||||
_openCommon("", "");
|
||||
}
|
||||
|
||||
function showWithQuery(query) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize(query, "");
|
||||
_openCommon(query, "");
|
||||
}
|
||||
|
||||
function hide() {
|
||||
@@ -170,13 +226,17 @@ Item {
|
||||
return;
|
||||
openedFromOverview = false;
|
||||
isClosing = true;
|
||||
contentVisible = false;
|
||||
// For directional effects, defer contentVisible=false so content stays rendered during exit slide
|
||||
if (!Theme.isDirectionalEffect)
|
||||
contentVisible = false;
|
||||
|
||||
// Trigger exit animation — Behaviors will animate motionX/Y to frozen collapsed position
|
||||
_motionActive = false;
|
||||
|
||||
keyboardActive = false;
|
||||
spotlightOpen = false;
|
||||
focusGrab.active = false;
|
||||
ModalManager.closeModal(root);
|
||||
|
||||
closeCleanupTimer.start();
|
||||
}
|
||||
|
||||
@@ -185,21 +245,7 @@ Item {
|
||||
}
|
||||
|
||||
function showWithMode(mode) {
|
||||
closeCleanupTimer.stop();
|
||||
isClosing = false;
|
||||
openedFromOverview = false;
|
||||
|
||||
var focusedScreen = CompositorService.getFocusedScreen();
|
||||
if (focusedScreen)
|
||||
launcherWindow.screen = focusedScreen;
|
||||
|
||||
spotlightOpen = true;
|
||||
keyboardActive = true;
|
||||
ModalManager.openModal(root);
|
||||
if (useHyprlandFocusGrab)
|
||||
focusGrab.active = true;
|
||||
|
||||
_ensureContentLoadedAndInitialize("", mode);
|
||||
_openCommon("", mode);
|
||||
}
|
||||
|
||||
function toggleWithMode(mode) {
|
||||
@@ -220,10 +266,13 @@ Item {
|
||||
|
||||
Timer {
|
||||
id: closeCleanupTimer
|
||||
interval: Theme.modalAnimationDuration + 50
|
||||
interval: Theme.variantCloseInterval(Theme.modalAnimationDuration)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
isClosing = false;
|
||||
contentVisible = false;
|
||||
contentWindow.visible = false;
|
||||
backgroundWindow.visible = false;
|
||||
if (root.unloadContentOnClose)
|
||||
launcherContentLoader.active = false;
|
||||
dialogClosed();
|
||||
@@ -241,7 +290,7 @@ Item {
|
||||
|
||||
HyprlandFocusGrab {
|
||||
id: focusGrab
|
||||
windows: [launcherWindow]
|
||||
windows: [contentWindow]
|
||||
active: false
|
||||
|
||||
onCleared: {
|
||||
@@ -266,7 +315,7 @@ Item {
|
||||
if (Quickshell.screens.length === 0)
|
||||
return;
|
||||
|
||||
const screen = launcherWindow.screen;
|
||||
const screen = contentWindow.screen;
|
||||
const screenName = screen?.name;
|
||||
|
||||
let needsReset = !screen || !screenName;
|
||||
@@ -288,35 +337,31 @@ Item {
|
||||
return;
|
||||
|
||||
root._windowEnabled = false;
|
||||
launcherWindow.screen = newScreen;
|
||||
backgroundWindow.screen = newScreen;
|
||||
contentWindow.screen = newScreen;
|
||||
Qt.callLater(() => {
|
||||
root._windowEnabled = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── Background window: fullscreen, handles darkening + click-to-dismiss ──
|
||||
PanelWindow {
|
||||
id: launcherWindow
|
||||
visible: root._windowEnabled && (spotlightOpen || isClosing)
|
||||
id: backgroundWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankModal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankModal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
WlrLayershell.namespace: "dms:spotlight:bg"
|
||||
WlrLayershell.layer: WlrLayershell.Top
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||
|
||||
WlrLayershell.margins {
|
||||
top: contentContainer.dockTop ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 0 ? Theme.px(42, root.dpr) : 0)
|
||||
bottom: contentContainer.dockBottom ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 1 ? Theme.px(42, root.dpr) : 0)
|
||||
left: contentContainer.dockLeft ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 2 ? Theme.px(42, root.dpr) : 0)
|
||||
right: contentContainer.dockRight ? contentContainer.dockThickness : (typeof SettingsData !== "undefined" && SettingsData.barPosition === 3 ? Theme.px(42, root.dpr) : 0)
|
||||
}
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
@@ -326,11 +371,11 @@ Item {
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: spotlightOpen ? fullScreenMask : null
|
||||
item: (spotlightOpen || isClosing) ? bgFullScreenMask : null
|
||||
}
|
||||
|
||||
Item {
|
||||
id: fullScreenMask
|
||||
id: bgFullScreenMask
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
@@ -338,13 +383,14 @@ Item {
|
||||
id: backgroundDarken
|
||||
anchors.fill: parent
|
||||
color: "black"
|
||||
opacity: contentVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: contentVisible || opacity > 0
|
||||
opacity: launcherMotionVisible && SettingsData.modalDarkenBackground ? 0.5 : 0
|
||||
visible: launcherMotionVisible || opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -352,84 +398,240 @@ Item {
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: spotlightOpen
|
||||
onClicked: mouse => {
|
||||
var contentX = modalContainer.x;
|
||||
var contentY = modalContainer.y;
|
||||
var contentW = modalContainer.width;
|
||||
var contentH = modalContainer.height;
|
||||
onClicked: root.hide()
|
||||
}
|
||||
}
|
||||
|
||||
if (mouse.x < contentX || mouse.x > contentX + contentW || mouse.y < contentY || mouse.y > contentY + contentH) {
|
||||
root.hide();
|
||||
}
|
||||
// ── Content window: SMALL, positioned with margins — only renders the modal area ──
|
||||
PanelWindow {
|
||||
id: contentWindow
|
||||
visible: false
|
||||
color: "transparent"
|
||||
|
||||
WlrLayershell.namespace: "dms:spotlight"
|
||||
WlrLayershell.layer: {
|
||||
switch (Quickshell.env("DMS_MODAL_LAYER")) {
|
||||
case "bottom":
|
||||
console.error("DankLauncherV2Modal: 'bottom' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "background":
|
||||
console.error("DankLauncherV2Modal: 'background' layer is not valid for modals. Defaulting to 'top' layer.");
|
||||
return WlrLayershell.Top;
|
||||
case "overlay":
|
||||
return WlrLayershell.Overlay;
|
||||
default:
|
||||
return WlrLayershell.Top;
|
||||
}
|
||||
}
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: keyboardActive ? (root.useHyprlandFocusGrab ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.Exclusive) : WlrKeyboardFocus.None
|
||||
|
||||
anchors {
|
||||
left: true
|
||||
top: true
|
||||
}
|
||||
|
||||
WlrLayershell.margins {
|
||||
left: root._cwMarginLeft
|
||||
top: root._cwMarginTop
|
||||
}
|
||||
|
||||
implicitWidth: root._cwWidth
|
||||
implicitHeight: root._cwHeight
|
||||
|
||||
mask: Region {
|
||||
item: contentInputMask
|
||||
}
|
||||
|
||||
Item {
|
||||
id: modalContainer
|
||||
x: root.modalX
|
||||
y: root.modalY
|
||||
width: root.modalWidth
|
||||
height: root.modalHeight
|
||||
visible: contentVisible || opacity > 0
|
||||
|
||||
opacity: contentVisible ? 1 : 0
|
||||
scale: contentVisible ? 1 : 0.96
|
||||
transformOrigin: Item.Center
|
||||
|
||||
Behavior on opacity {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
DankAnim {
|
||||
duration: Theme.modalAnimationDuration
|
||||
easing.bezierCurve: contentVisible ? Theme.expressiveCurves.expressiveDefaultSpatial : Theme.expressiveCurves.emphasized
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: root.backgroundColor
|
||||
border.color: root.borderColor
|
||||
border.width: root.borderWidth
|
||||
radius: root.cornerRadius
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
id: contentInputMask
|
||||
visible: false
|
||||
x: contentContainer.x + contentWrapper.x
|
||||
y: contentContainer.y + contentWrapper.y
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: contentContainer
|
||||
|
||||
// For directional/depth: contentContainer is at alignedY from window top (window starts at screen top)
|
||||
// For standard: contentContainer is at shadowPad from window top (window starts near modal)
|
||||
x: root._ccX
|
||||
y: root._ccY
|
||||
width: root.alignedWidth
|
||||
height: root.alignedHeight
|
||||
|
||||
readonly property int dockEdge: typeof SettingsData !== "undefined" ? SettingsData.dockPosition : 1
|
||||
readonly property bool dockTop: dockEdge === 0
|
||||
readonly property bool dockBottom: dockEdge === 1
|
||||
readonly property bool dockLeft: dockEdge === 2
|
||||
readonly property bool dockRight: dockEdge === 3
|
||||
|
||||
readonly property real dockThickness: typeof SettingsData !== "undefined" && SettingsData.showDock ? Theme.px(SettingsData.dockIconSize + (SettingsData.dockMargin * 2) + SettingsData.dockSpacing + 8, root.dpr) : Theme.px(60, root.dpr)
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
readonly property real collapsedMotionX: {
|
||||
if (directionalEffect) {
|
||||
if (dockLeft)
|
||||
return -(root._ccX + root.alignedWidth + Theme.effectAnimOffset);
|
||||
if (dockRight)
|
||||
return root.screenWidth - root._ccX + Theme.effectAnimOffset;
|
||||
}
|
||||
if (depthEffect)
|
||||
return Theme.effectAnimOffset * 0.25;
|
||||
return 0;
|
||||
}
|
||||
readonly property real collapsedMotionY: {
|
||||
if (directionalEffect) {
|
||||
if (dockTop)
|
||||
return -(root._ccY + root.alignedHeight + Theme.effectAnimOffset);
|
||||
if (dockBottom)
|
||||
return root.screenHeight - root._ccY + root.shadowPad + Theme.effectAnimOffset;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect)
|
||||
return -Math.max(Theme.effectAnimOffset * 0.85, 34);
|
||||
return -Math.max((root.shadowPad || 0) + Theme.effectAnimOffset, 40);
|
||||
}
|
||||
|
||||
// animX/animY are Behavior-animated — DankPopout pattern
|
||||
property real animX: 0
|
||||
property real animY: 0
|
||||
property real scaleValue: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed)
|
||||
|
||||
Component.onCompleted: {
|
||||
animX = Theme.snap(root._motionActive ? 0 : collapsedMotionX, root.dpr);
|
||||
animY = Theme.snap(root._motionActive ? 0 : collapsedMotionY, root.dpr);
|
||||
scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: root
|
||||
function on_MotionActiveChanged() {
|
||||
contentContainer.animX = Theme.snap(root._motionActive ? 0 : root._frozenMotionX, root.dpr);
|
||||
contentContainer.animY = Theme.snap(root._motionActive ? 0 : root._frozenMotionY, root.dpr);
|
||||
contentContainer.scaleValue = root._motionActive ? 1.0 : (Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2 ? Theme.effectScaleCollapsed : (Theme.isDirectionalEffect ? 1 : Theme.effectScaleCollapsed));
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animX {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on animY {
|
||||
enabled: root.animationsEnabled
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scaleValue {
|
||||
enabled: root.animationsEnabled && (!Theme.isDirectionalEffect || (typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode === 2))
|
||||
DankAnim {
|
||||
duration: Theme.variantDuration(Theme.modalAnimationDuration, root._motionActive)
|
||||
easing.bezierCurve: root._motionActive ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: directionalClipMask
|
||||
readonly property bool shouldClip: Theme.isDirectionalEffect && typeof SettingsData !== "undefined" && SettingsData.directionalAnimationMode > 0
|
||||
readonly property real clipOversize: 2000
|
||||
|
||||
clip: shouldClip
|
||||
|
||||
x: shouldClip ? (contentContainer.dockRight ? -clipOversize : (contentContainer.dockLeft ? contentContainer.dockThickness - root._ccX : -clipOversize)) : 0
|
||||
y: shouldClip ? (contentContainer.dockBottom ? -clipOversize : (contentContainer.dockTop ? contentContainer.dockThickness - root._ccY : -clipOversize)) : 0
|
||||
|
||||
width: shouldClip ? parent.width + clipOversize + (contentContainer.dockRight ? (root.screenWidth - contentContainer.dockThickness - root._ccX - parent.width) : (contentContainer.dockLeft ? clipOversize : clipOversize)) : parent.width
|
||||
height: shouldClip ? parent.height + clipOversize + (contentContainer.dockBottom ? (root.screenHeight - contentContainer.dockThickness - root._ccY - parent.height) : (contentContainer.dockTop ? clipOversize : clipOversize)) : parent.height
|
||||
|
||||
Item {
|
||||
id: aligner
|
||||
x: directionalClipMask.x !== 0 ? -directionalClipMask.x : 0
|
||||
y: directionalClipMask.y !== 0 ? -directionalClipMask.y : 0
|
||||
width: contentContainer.width
|
||||
height: contentContainer.height
|
||||
|
||||
// Shadow mirrors contentWrapper position/scale/opacity
|
||||
ElevationShadow {
|
||||
id: launcherShadowLayer
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: contentWrapper.opacity
|
||||
scale: contentWrapper.scale
|
||||
x: contentWrapper.x
|
||||
y: contentWrapper.y
|
||||
level: root.shadowLevel
|
||||
fallbackOffset: root.shadowFallbackOffset
|
||||
targetColor: root.backgroundColor
|
||||
borderColor: root.borderColor
|
||||
borderWidth: root.borderWidth
|
||||
targetRadius: root.cornerRadius
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.modalElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
|
||||
}
|
||||
|
||||
// contentWrapper moves inside static contentContainer — DankPopout pattern
|
||||
Item {
|
||||
id: contentWrapper
|
||||
width: parent.width
|
||||
height: parent.height
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (launcherMotionVisible ? 1 : 0)
|
||||
visible: opacity > 0
|
||||
scale: contentContainer.scaleValue
|
||||
x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: root.animationsEnabled && !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.modalAnimationDuration, launcherMotionVisible) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: launcherMotionVisible ? Theme.variantModalEnterCurve : Theme.variantModalExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => mouse.accepted = true
|
||||
}
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: keyboardActive
|
||||
|
||||
Loader {
|
||||
id: launcherContentLoader
|
||||
anchors.fill: parent
|
||||
active: !root.unloadContentOnClose || root.spotlightOpen || root.isClosing || root.contentVisible || root._pendingInitialize
|
||||
asynchronous: false
|
||||
sourceComponent: LauncherContent {
|
||||
focus: true
|
||||
parentModal: root
|
||||
}
|
||||
|
||||
onLoaded: {
|
||||
if (root._pendingInitialize) {
|
||||
root._initializeAndShow(root._pendingQuery, root._pendingMode);
|
||||
root._pendingInitialize = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onEscapePressed: event => {
|
||||
root.hide();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
} // contentWrapper
|
||||
} // aligner
|
||||
} // directionalClipMask
|
||||
} // contentContainer
|
||||
} // PanelWindow
|
||||
}
|
||||
|
||||
@@ -86,7 +86,7 @@ FocusScope {
|
||||
|
||||
Controller {
|
||||
id: controller
|
||||
active: root.parentModal?.spotlightOpen ?? true
|
||||
active: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
viewModeContext: root.viewModeContext
|
||||
|
||||
onItemExecuted: {
|
||||
@@ -462,7 +462,7 @@ FocusScope {
|
||||
showClearButton: true
|
||||
textColor: Theme.surfaceText
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
enabled: root.parentModal ? root.parentModal.spotlightOpen : true
|
||||
enabled: root.parentModal ? (root.parentModal.spotlightOpen || root.parentModal.isClosing) : true
|
||||
placeholderText: ""
|
||||
ignoreUpDownKeys: true
|
||||
ignoreTabKeys: true
|
||||
@@ -496,8 +496,9 @@ FocusScope {
|
||||
Row {
|
||||
id: categoryRow
|
||||
width: parent.width
|
||||
height: controller.activePluginCategories.length > 0 ? 36 : 0
|
||||
visible: controller.activePluginCategories.length > 0
|
||||
readonly property bool showPluginCategories: controller.activePluginCategories.length > 0
|
||||
height: showPluginCategories ? 36 : 0
|
||||
visible: showPluginCategories
|
||||
spacing: Theme.spacingS
|
||||
|
||||
clip: true
|
||||
@@ -511,6 +512,7 @@ FocusScope {
|
||||
|
||||
DankDropdown {
|
||||
id: categoryDropdown
|
||||
visible: categoryRow.showPluginCategories
|
||||
width: Math.min(200, parent.width)
|
||||
compactMode: true
|
||||
dropdownWidth: 200
|
||||
@@ -694,7 +696,13 @@ FocusScope {
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - searchField.height - categoryRow.height - fileFilterRow.height - actionPanel.height - Theme.spacingXS * ((categoryRow.visible ? 1 : 0) + (fileFilterRow.visible ? 1 : 0) + 2)
|
||||
opacity: root.parentModal?.isClosing ? 0 : 1
|
||||
opacity: {
|
||||
if (!root.parentModal)
|
||||
return 1;
|
||||
if (Theme.isDirectionalEffect && root.parentModal.isClosing)
|
||||
return 1;
|
||||
return root.parentModal.isClosing ? 0 : 1;
|
||||
}
|
||||
|
||||
ResultsList {
|
||||
id: resultsList
|
||||
@@ -789,7 +797,7 @@ FocusScope {
|
||||
Image {
|
||||
width: 40
|
||||
height: 40
|
||||
source: editingApp?.icon ? "image://icon/" + editingApp.icon : "image://icon/application-x-executable"
|
||||
source: Paths.resolveIconUrl(editingApp?.icon || "application-x-executable")
|
||||
sourceSize.width: 40
|
||||
sourceSize.height: 40
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
Rectangle {
|
||||
@@ -35,21 +37,190 @@ Rectangle {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
// Whether the apps category picker should replace the plain title
|
||||
readonly property bool hasAppCategories: root.section?.id === "apps" && (root.controller?.appCategories?.length ?? 0) > 0
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Hide section icon when the category chip already shows one
|
||||
visible: !leftContent.hasAppCategories
|
||||
name: root.section?.icon ?? "folder"
|
||||
size: 16
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
// Plain title — hidden when the category chip is shown
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: !leftContent.hasAppCategories
|
||||
text: root.section?.title ?? ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
// Compact inline category chip — only visible on the apps section
|
||||
Item {
|
||||
id: categoryChip
|
||||
visible: leftContent.hasAppCategories
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
// Size to content with a fixed-min width so it doesn't jump around
|
||||
width: chipRow.implicitWidth + Theme.spacingM * 2
|
||||
height: 24
|
||||
|
||||
readonly property string currentCategory: root.controller?.appCategory || (root.controller?.appCategories?.length > 0 ? root.controller.appCategories[0] : "")
|
||||
readonly property var iconMap: {
|
||||
const cats = root.controller?.appCategories ?? [];
|
||||
const m = {};
|
||||
cats.forEach(c => { m[c] = AppSearchService.getCategoryIcon(c); });
|
||||
return m;
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: chipArea.containsMouse || categoryPopup.visible ? Theme.surfaceContainerHigh : "transparent"
|
||||
border.color: categoryPopup.visible ? Theme.primary : Theme.outlineMedium
|
||||
border.width: categoryPopup.visible ? 2 : 1
|
||||
}
|
||||
|
||||
Row {
|
||||
id: chipRow
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: categoryChip.iconMap[categoryChip.currentCategory] ?? "apps"
|
||||
size: 14
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: categoryChip.currentCategory
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: categoryPopup.visible ? "expand_less" : "expand_more"
|
||||
size: 14
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: chipArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (categoryPopup.visible) {
|
||||
categoryPopup.close();
|
||||
} else {
|
||||
const pos = categoryChip.mapToItem(Overlay.overlay, 0, 0);
|
||||
categoryPopup.x = pos.x;
|
||||
categoryPopup.y = pos.y + categoryChip.height + 4;
|
||||
categoryPopup.open();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Popup {
|
||||
id: categoryPopup
|
||||
parent: Overlay.overlay
|
||||
width: Math.max(categoryChip.width, 180)
|
||||
padding: 0
|
||||
modal: true
|
||||
dim: false
|
||||
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
|
||||
background: Rectangle { color: "transparent" }
|
||||
|
||||
contentItem: Rectangle {
|
||||
radius: Theme.cornerRadius
|
||||
color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
|
||||
ElevationShadow {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: parent.radius
|
||||
targetColor: parent.color
|
||||
borderColor: parent.border.color
|
||||
borderWidth: parent.border.width
|
||||
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: categoryList
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
model: root.controller?.appCategories ?? []
|
||||
spacing: 2
|
||||
clip: true
|
||||
interactive: contentHeight > height
|
||||
implicitHeight: contentHeight
|
||||
|
||||
delegate: Rectangle {
|
||||
id: catDelegate
|
||||
required property string modelData
|
||||
required property int index
|
||||
width: categoryList.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
readonly property bool isCurrent: categoryChip.currentCategory === modelData
|
||||
color: isCurrent ? Theme.primaryHover : catArea.containsMouse ? Theme.primaryHoverLight : "transparent"
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: categoryChip.iconMap[catDelegate.modelData] ?? "apps"
|
||||
size: 16
|
||||
color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: catDelegate.modelData
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: catDelegate.isCurrent ? Theme.primary : Theme.surfaceText
|
||||
font.weight: catDelegate.isCurrent ? Font.Medium : Font.Normal
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: catArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
if (root.controller)
|
||||
root.controller.setAppCategory(catDelegate.modelData);
|
||||
categoryPopup.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Size to list content, cap at 10 visible items
|
||||
height: Math.min((root.controller?.appCategories?.length ?? 0) * 34, 10 * 34) + Theme.spacingS * 2 + 4
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: root.section?.items?.length ?? 0
|
||||
|
||||
@@ -225,7 +225,13 @@ Item {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: root.errorCount > 0 ? I18n.tr("%1 issue(s) found", "greeter doctor page error count").arg(root.errorCount) : I18n.tr("All checks passed", "greeter doctor page success")
|
||||
text: {
|
||||
if (root.errorCount === 0)
|
||||
return I18n.tr("All checks passed", "greeter doctor page success");
|
||||
return root.errorCount === 1
|
||||
? I18n.tr("%1 issue found", "greeter doctor page error count").arg(root.errorCount)
|
||||
: I18n.tr("%1 issues found", "greeter doctor page error count").arg(root.errorCount);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: root.errorCount > 0 ? Theme.error : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -470,7 +470,22 @@ FocusScope {
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: localeLoader
|
||||
anchors.fill: parent
|
||||
active: root.currentIndex === 30
|
||||
visible: active
|
||||
focus: active
|
||||
|
||||
sourceComponent: LocaleTab {}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active && item)
|
||||
Qt.callLater(() => item.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,6 +246,12 @@ Rectangle {
|
||||
"icon": "headphones",
|
||||
"tabIndex": 29
|
||||
},
|
||||
{
|
||||
"id": "locale",
|
||||
"text": I18n.tr("Locale"),
|
||||
"icon": "language",
|
||||
"tabIndex": 30
|
||||
},
|
||||
{
|
||||
"id": "clipboard",
|
||||
"text": I18n.tr("Clipboard"),
|
||||
|
||||
@@ -106,7 +106,7 @@ DankPopout {
|
||||
QtObject {
|
||||
id: modalAdapter
|
||||
property bool spotlightOpen: appDrawerPopout.shouldBeVisible
|
||||
property bool isClosing: false
|
||||
property bool isClosing: appDrawerPopout.isClosing
|
||||
|
||||
function hide() {
|
||||
appDrawerPopout.close();
|
||||
|
||||
@@ -85,12 +85,8 @@ Variants {
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (!source) {
|
||||
isInitialized = true;
|
||||
return;
|
||||
}
|
||||
const formattedSource = source.startsWith("file://") ? source : encodeFileUrl(source);
|
||||
setWallpaperImmediate(formattedSource);
|
||||
if (typeof blurWallpaperWindow.updatesEnabled !== "undefined")
|
||||
blurWallpaperWindow.updatesEnabled = Qt.binding(() => root.effectActive || root._renderSettling || currentWallpaper.status === Image.Loading || nextWallpaper.status === Image.Loading);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
||||
@@ -98,8 +94,25 @@ Variants {
|
||||
property real transitionProgress: 0
|
||||
readonly property bool transitioning: transitionAnimation.running
|
||||
property bool effectActive: false
|
||||
property bool _renderSettling: true
|
||||
property bool useNextForEffect: false
|
||||
|
||||
Connections {
|
||||
target: currentWallpaper
|
||||
function onStatusChanged() {
|
||||
if (currentWallpaper.status === Image.Ready) {
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: renderSettleTimer
|
||||
interval: 1000
|
||||
onTriggered: root._renderSettling = false
|
||||
}
|
||||
|
||||
onSourceChanged: {
|
||||
if (!source || source.startsWith("#")) {
|
||||
setWallpaperImmediate("");
|
||||
@@ -124,6 +137,8 @@ Variants {
|
||||
transitionAnimation.stop();
|
||||
root.transitionProgress = 0.0;
|
||||
root.effectActive = false;
|
||||
root._renderSettling = true;
|
||||
renderSettleTimer.restart();
|
||||
currentWallpaper.source = newSource;
|
||||
nextWallpaper.source = "";
|
||||
}
|
||||
|
||||
@@ -271,8 +271,8 @@ Item {
|
||||
|
||||
text: {
|
||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
||||
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
|
||||
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
|
||||
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
|
||||
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.accentColor
|
||||
@@ -324,8 +324,8 @@ Item {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: {
|
||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0)
|
||||
return systemClock.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat) ?? "";
|
||||
return systemClock.date?.toLocaleDateString(Qt.locale(), "ddd, MMM d") ?? "";
|
||||
return systemClock.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat) ?? "";
|
||||
return systemClock.date?.toLocaleDateString(I18n.locale(), "ddd, MMM d") ?? "";
|
||||
}
|
||||
font.pixelSize: digitalRoot.smallSize
|
||||
color: Theme.withAlpha(root.accentColor, 0.7)
|
||||
@@ -528,7 +528,7 @@ Item {
|
||||
StyledText {
|
||||
visible: stackedRoot.hasDate
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: systemClock.date?.toLocaleDateString(Qt.locale(), "MMM dd") ?? ""
|
||||
text: systemClock.date?.toLocaleDateString(I18n.locale(), "MMM dd") ?? ""
|
||||
font.pixelSize: stackedRoot.smallSize * 0.7
|
||||
color: Theme.withAlpha(root.accentColor, 0.7)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ DankPopout {
|
||||
id: root
|
||||
|
||||
layerNamespace: "dms:control-center"
|
||||
fullHeightSurface: true
|
||||
fullHeightSurface: false
|
||||
|
||||
property string expandedSection: ""
|
||||
property var triggerScreen: null
|
||||
@@ -126,9 +126,11 @@ DankPopout {
|
||||
z: 5000
|
||||
|
||||
Behavior on opacity {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: root.shouldBeVisible ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import QtQuick.Shapes
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
@@ -53,15 +52,43 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
readonly property real shadowIntensity: barConfig?.shadowIntensity ?? 0
|
||||
readonly property bool shadowEnabled: shadowIntensity > 0
|
||||
readonly property int blurMax: 64
|
||||
readonly property real shadowBlurPx: shadowIntensity * 0.2
|
||||
readonly property real shadowBlur: Math.max(0, Math.min(1, shadowBlurPx / blurMax))
|
||||
readonly property real shadowOpacity: (barConfig?.shadowOpacity ?? 60) / 100
|
||||
readonly property string shadowColorMode: barConfig?.shadowColorMode ?? "text"
|
||||
readonly property color shadowBaseColor: {
|
||||
switch (shadowColorMode) {
|
||||
// M3 elevation shadow — Level 2 baseline (navigation bar), with per-bar override support
|
||||
readonly property bool hasPerBarOverride: (barConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property var elevLevel: Theme.elevationLevel2
|
||||
readonly property bool shadowEnabled: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || hasPerBarOverride
|
||||
readonly property string autoBarShadowDirection: isTop ? "top" : (isBottom ? "bottom" : (isLeft ? "left" : (isRight ? "right" : "top")))
|
||||
readonly property string globalShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
|
||||
readonly property string perBarShadowDirectionMode: barConfig?.shadowDirectionMode ?? "inherit"
|
||||
readonly property string perBarManualShadowDirection: {
|
||||
switch (barConfig?.shadowDirection) {
|
||||
case "top":
|
||||
case "topLeft":
|
||||
case "topRight":
|
||||
case "bottom":
|
||||
return barConfig.shadowDirection;
|
||||
default:
|
||||
return "top";
|
||||
}
|
||||
}
|
||||
readonly property string effectiveShadowDirection: {
|
||||
if (!hasPerBarOverride)
|
||||
return globalShadowDirection;
|
||||
switch (perBarShadowDirectionMode) {
|
||||
case "autoBar":
|
||||
return autoBarShadowDirection;
|
||||
case "manual":
|
||||
return perBarManualShadowDirection === "autoBar" ? autoBarShadowDirection : perBarManualShadowDirection;
|
||||
default:
|
||||
return globalShadowDirection;
|
||||
}
|
||||
}
|
||||
|
||||
// Per-bar override values (when barConfig.shadowIntensity > 0)
|
||||
readonly property real overrideBlurPx: (barConfig?.shadowIntensity ?? 0) * 0.2
|
||||
readonly property real overrideOpacity: (barConfig?.shadowOpacity ?? 60) / 100
|
||||
readonly property string overrideColorMode: barConfig?.shadowColorMode ?? "default"
|
||||
readonly property color overrideBaseColor: {
|
||||
switch (overrideColorMode) {
|
||||
case "surface":
|
||||
return Theme.surface;
|
||||
case "primary":
|
||||
@@ -71,10 +98,16 @@ Item {
|
||||
case "custom":
|
||||
return barConfig?.shadowCustomColor ?? "#000000";
|
||||
default:
|
||||
return Theme.surfaceText;
|
||||
return "#000000";
|
||||
}
|
||||
}
|
||||
readonly property color shadowColor: Theme.withAlpha(shadowBaseColor, shadowOpacity * barWindow._backgroundAlpha)
|
||||
|
||||
// Resolved values — per-bar override wins if set, otherwise use global M3 elevation
|
||||
readonly property real shadowBlurPx: hasPerBarOverride ? overrideBlurPx : (elevLevel.blurPx ?? 8)
|
||||
readonly property color shadowColor: hasPerBarOverride ? Theme.withAlpha(overrideBaseColor, overrideOpacity) : Theme.elevationShadowColor(elevLevel)
|
||||
readonly property real shadowOffsetMagnitude: hasPerBarOverride ? (overrideBlurPx * 0.5) : Theme.elevationOffsetMagnitude(elevLevel, 4, effectiveShadowDirection)
|
||||
readonly property real shadowOffsetX: Theme.elevationOffsetXFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
||||
readonly property real shadowOffsetY: Theme.elevationOffsetYFor(hasPerBarOverride ? null : elevLevel, effectiveShadowDirection, shadowOffsetMagnitude)
|
||||
|
||||
readonly property string mainPath: generatePathForPosition(width, height)
|
||||
readonly property string borderFullPath: generateBorderFullPath(width, height)
|
||||
@@ -118,42 +151,28 @@ Item {
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: shadowLoader
|
||||
anchors.fill: parent
|
||||
active: root.shadowEnabled && mainPathCorrectShape
|
||||
asynchronous: false
|
||||
sourceComponent: Item {
|
||||
anchors.fill: parent
|
||||
ElevationShadow {
|
||||
id: barShadow
|
||||
visible: root.shadowEnabled && root.width > 0 && root.height > 0
|
||||
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
layer.samples: 8
|
||||
layer.textureSize: Qt.size(Math.round(width * barWindow._dpr * 2), Math.round(height * barWindow._dpr * 2))
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowBlur: root.shadowBlur
|
||||
shadowColor: root.shadowColor
|
||||
shadowVerticalOffset: root.isTop ? root.shadowBlurPx * 0.5 : (root.isBottom ? -root.shadowBlurPx * 0.5 : 0)
|
||||
shadowHorizontalOffset: root.isLeft ? root.shadowBlurPx * 0.5 : (root.isRight ? -root.shadowBlurPx * 0.5 : 0)
|
||||
autoPaddingEnabled: true
|
||||
}
|
||||
// Size to the bar's rectangular body, excluding gothic wing extensions
|
||||
x: root.isRight ? root.wing : 0
|
||||
y: root.isBottom ? root.wing : 0
|
||||
width: axis.isVertical ? (parent.width - root.wing) : parent.width
|
||||
height: axis.isVertical ? parent.height : (parent.height - root.wing)
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
shadowEnabled: root.shadowEnabled
|
||||
level: root.hasPerBarOverride ? null : root.elevLevel
|
||||
direction: root.effectiveShadowDirection
|
||||
fallbackOffset: 4
|
||||
targetRadius: root.rt
|
||||
targetColor: barWindow._bgColor
|
||||
|
||||
ShapePath {
|
||||
fillColor: barWindow._bgColor
|
||||
strokeColor: "transparent"
|
||||
strokeWidth: 0
|
||||
|
||||
PathSvg {
|
||||
path: root.mainPath
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
shadowBlurPx: root.shadowBlurPx
|
||||
shadowOffsetX: root.shadowOffsetX
|
||||
shadowOffsetY: root.shadowOffsetY
|
||||
shadowColor: root.shadowColor
|
||||
blurMax: Theme.elevationBlurMax
|
||||
}
|
||||
|
||||
Loader {
|
||||
|
||||
@@ -21,73 +21,82 @@ Item {
|
||||
property alias centerWidgetsModel: centerWidgetsModel
|
||||
property alias rightWidgetsModel: rightWidgetsModel
|
||||
|
||||
property string _leftWidgetsJson: {
|
||||
root.barConfig;
|
||||
const leftWidgets = root.barConfig?.leftWidgets || [];
|
||||
const mapped = leftWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
return JSON.stringify(mapped);
|
||||
}
|
||||
|
||||
property string _centerWidgetsJson: {
|
||||
root.barConfig;
|
||||
const centerWidgets = root.barConfig?.centerWidgets || [];
|
||||
const mapped = centerWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
return JSON.stringify(mapped);
|
||||
}
|
||||
|
||||
property string _rightWidgetsJson: {
|
||||
root.barConfig;
|
||||
const rightWidgets = root.barConfig?.rightWidgets || [];
|
||||
const mapped = rightWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
return JSON.stringify(mapped);
|
||||
}
|
||||
|
||||
ScriptModel {
|
||||
id: leftWidgetsModel
|
||||
values: {
|
||||
root.barConfig;
|
||||
const leftWidgets = root.barConfig?.leftWidgets || [];
|
||||
return leftWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
values: JSON.parse(root._leftWidgetsJson)
|
||||
}
|
||||
|
||||
ScriptModel {
|
||||
id: centerWidgetsModel
|
||||
values: {
|
||||
root.barConfig;
|
||||
const centerWidgets = root.barConfig?.centerWidgets || [];
|
||||
return centerWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
values: JSON.parse(root._centerWidgetsJson)
|
||||
}
|
||||
|
||||
ScriptModel {
|
||||
id: rightWidgetsModel
|
||||
values: {
|
||||
root.barConfig;
|
||||
const rightWidgets = root.barConfig?.rightWidgets || [];
|
||||
return rightWidgets.map((w, index) => {
|
||||
if (typeof w === "string") {
|
||||
return {
|
||||
widgetId: w,
|
||||
id: w + "_" + index,
|
||||
enabled: true
|
||||
};
|
||||
} else {
|
||||
const obj = Object.assign({}, w);
|
||||
obj.widgetId = w.id || w.widgetId;
|
||||
obj.id = (w.id || w.widgetId) + "_" + index;
|
||||
obj.enabled = w.enabled !== false;
|
||||
return obj;
|
||||
}
|
||||
});
|
||||
}
|
||||
values: JSON.parse(root._rightWidgetsJson)
|
||||
}
|
||||
|
||||
function triggerControlCenterOnFocusedScreen() {
|
||||
|
||||
@@ -561,10 +561,7 @@ Item {
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent)
|
||||
parentScreen: barWindow.screen
|
||||
popoutTarget: {
|
||||
clipboardHistoryPopoutLoader.active = true;
|
||||
return clipboardHistoryPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: clipboardHistoryPopoutLoader.item ?? null
|
||||
|
||||
function openClipboardPopout(initialTab) {
|
||||
clipboardHistoryPopoutLoader.active = true;
|
||||
@@ -759,10 +756,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
section: topBarContent.getWidgetSection(parent) || "center"
|
||||
popoutTarget: {
|
||||
dankDashPopoutLoader.active = true;
|
||||
return dankDashPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: dankDashPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -826,10 +820,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
section: topBarContent.getWidgetSection(parent) || "center"
|
||||
popoutTarget: {
|
||||
dankDashPopoutLoader.active = true;
|
||||
return dankDashPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: dankDashPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
dankDashPopoutLoader.active = true;
|
||||
@@ -881,10 +872,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
section: topBarContent.getWidgetSection(parent) || "center"
|
||||
popoutTarget: {
|
||||
dankDashPopoutLoader.active = true;
|
||||
return dankDashPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: dankDashPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
dankDashPopoutLoader.active = true;
|
||||
@@ -968,10 +956,7 @@ Item {
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
processListPopoutLoader.active = true;
|
||||
return processListPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: processListPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
widgetData: parent.widgetData
|
||||
onCpuClicked: {
|
||||
@@ -1004,10 +989,7 @@ Item {
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
processListPopoutLoader.active = true;
|
||||
return processListPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: processListPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
widgetData: parent.widgetData
|
||||
onRamClicked: {
|
||||
@@ -1054,10 +1036,7 @@ Item {
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
processListPopoutLoader.active = true;
|
||||
return processListPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: processListPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
widgetData: parent.widgetData
|
||||
onCpuTempClicked: {
|
||||
@@ -1090,10 +1069,7 @@ Item {
|
||||
widgetThickness: barWindow.widgetThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
processListPopoutLoader.active = true;
|
||||
return processListPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: processListPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
widgetData: parent.widgetData
|
||||
onGpuTempClicked: {
|
||||
@@ -1134,16 +1110,14 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
notificationCenterLoader.active = true;
|
||||
return notificationCenterLoader.item;
|
||||
}
|
||||
popoutTarget: notificationCenterLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
notificationCenterLoader.active = true;
|
||||
if (!notificationCenterLoader.item) {
|
||||
return;
|
||||
}
|
||||
notificationCenterLoader.item.triggerScreen = barWindow.screen;
|
||||
const effectiveBarConfig = topBarContent.barConfig;
|
||||
const barPosition = barWindow.axis?.edge === "left" ? 2 : (barWindow.axis?.edge === "right" ? 3 : (barWindow.axis?.edge === "top" ? 0 : 1));
|
||||
if (notificationCenterLoader.item.setBarContext) {
|
||||
@@ -1172,10 +1146,7 @@ Item {
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
barSpacing: barConfig?.spacing ?? 4
|
||||
barConfig: topBarContent.barConfig
|
||||
popoutTarget: {
|
||||
batteryPopoutLoader.active = true;
|
||||
return batteryPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: batteryPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onToggleBatteryPopup: {
|
||||
batteryPopoutLoader.active = true;
|
||||
@@ -1208,10 +1179,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "center"
|
||||
popoutTarget: {
|
||||
layoutPopoutLoader.active = true;
|
||||
return layoutPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: layoutPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onToggleLayoutPopup: {
|
||||
layoutPopoutLoader.active = true;
|
||||
@@ -1244,10 +1212,7 @@ Item {
|
||||
barSpacing: barConfig?.spacing ?? 4
|
||||
barConfig: topBarContent.barConfig
|
||||
isAutoHideBar: topBarContent.barConfig?.autoHide ?? false
|
||||
popoutTarget: {
|
||||
vpnPopoutLoader.active = true;
|
||||
return vpnPopoutLoader.item;
|
||||
}
|
||||
popoutTarget: vpnPopoutLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onToggleVpnPopup: {
|
||||
vpnPopoutLoader.active = true;
|
||||
@@ -1281,10 +1246,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
controlCenterLoader.active = true;
|
||||
return controlCenterLoader.item;
|
||||
}
|
||||
popoutTarget: controlCenterLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
screenName: barWindow.screen?.name || ""
|
||||
screenModel: barWindow.screen?.model || ""
|
||||
@@ -1434,10 +1396,7 @@ Item {
|
||||
barThickness: barWindow.effectiveBarThickness
|
||||
axis: barWindow.axis
|
||||
section: topBarContent.getWidgetSection(parent) || "right"
|
||||
popoutTarget: {
|
||||
systemUpdateLoader.active = true;
|
||||
return systemUpdateLoader.item;
|
||||
}
|
||||
popoutTarget: systemUpdateLoader.item ?? null
|
||||
parentScreen: barWindow.screen
|
||||
onClicked: {
|
||||
systemUpdateLoader.active = true;
|
||||
|
||||
@@ -131,38 +131,69 @@ PanelWindow {
|
||||
readonly property real _wingR: Math.max(0, wingtipsRadius)
|
||||
readonly property color _surfaceContainer: Theme.surfaceContainer
|
||||
readonly property string _barId: barConfig?.id ?? "default"
|
||||
readonly property var _liveBarConfig: SettingsData.barConfigs.find(c => c.id === _barId) || barConfig
|
||||
readonly property real _backgroundAlpha: _liveBarConfig?.transparency ?? 1.0
|
||||
property real _backgroundAlpha: barConfig?.transparency ?? 1.0
|
||||
readonly property color _bgColor: Theme.withAlpha(_surfaceContainer, _backgroundAlpha)
|
||||
|
||||
function _updateBackgroundAlpha() {
|
||||
const live = SettingsData.barConfigs.find(c => c.id === _barId);
|
||||
_backgroundAlpha = (live ?? barConfig)?.transparency ?? 1.0;
|
||||
}
|
||||
readonly property real _dpr: CompositorService.getScreenScale(barWindow.screen)
|
||||
|
||||
// Shadow buffer: extra window space for shadow to render beyond bar bounds
|
||||
readonly property bool _shadowActive: (Theme.elevationEnabled && (typeof SettingsData !== "undefined" ? (SettingsData.barElevationEnabled ?? true) : false)) || (barConfig?.shadowIntensity ?? 0) > 0
|
||||
readonly property real _shadowBuffer: {
|
||||
if (!_shadowActive)
|
||||
return 0;
|
||||
const hasOverride = (barConfig?.shadowIntensity ?? 0) > 0;
|
||||
if (hasOverride) {
|
||||
const blur = (barConfig.shadowIntensity ?? 0) * 0.2;
|
||||
const offset = blur * 0.5;
|
||||
return Theme.snap(Math.max(16, blur + offset + 8), _dpr);
|
||||
}
|
||||
return Theme.snap(Theme.elevationRenderPadding(Theme.elevationLevel2, "top", 4, 8, 16), _dpr);
|
||||
}
|
||||
|
||||
property string screenName: modelData.name
|
||||
|
||||
readonly property bool hasMaximizedToplevel: {
|
||||
if (!(barConfig?.maximizeDetection ?? true))
|
||||
return false;
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri)
|
||||
return false;
|
||||
property bool hasMaximizedToplevel: false
|
||||
property bool shouldHideForWindows: false
|
||||
|
||||
function _updateHasMaximizedToplevel() {
|
||||
if (!(barConfig?.maximizeDetection ?? true)) {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isHyprland && !CompositorService.isNiri) {
|
||||
hasMaximizedToplevel = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
for (let i = 0; i < filtered.length; i++) {
|
||||
if (filtered[i]?.maximized)
|
||||
return true;
|
||||
if (filtered[i]?.maximized) {
|
||||
hasMaximizedToplevel = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
hasMaximizedToplevel = false;
|
||||
}
|
||||
|
||||
readonly property bool shouldHideForWindows: {
|
||||
if (!(barConfig?.showOnWindowsOpen ?? false))
|
||||
return false;
|
||||
if (!(barConfig?.autoHide ?? false))
|
||||
return false;
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland)
|
||||
return false;
|
||||
function _updateShouldHideForWindows() {
|
||||
if (!(barConfig?.showOnWindowsOpen ?? false)) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
if (!(barConfig?.autoHide ?? false)) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
if (!CompositorService.isNiri && !CompositorService.isHyprland) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (CompositorService.isNiri) {
|
||||
NiriService.windows;
|
||||
|
||||
let currentWorkspaceId = null;
|
||||
for (let i = 0; i < NiriService.allWorkspaces.length; i++) {
|
||||
const ws = NiriService.allWorkspaces[i];
|
||||
@@ -172,8 +203,10 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentWorkspaceId === null)
|
||||
return false;
|
||||
if (currentWorkspaceId === null) {
|
||||
shouldHideForWindows = false;
|
||||
return;
|
||||
}
|
||||
|
||||
let hasTiled = false;
|
||||
let hasFloatingTouchingBar = false;
|
||||
@@ -217,14 +250,12 @@ PanelWindow {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasTiled)
|
||||
return true;
|
||||
|
||||
return hasFloatingTouchingBar;
|
||||
shouldHideForWindows = hasTiled || hasFloatingTouchingBar;
|
||||
return;
|
||||
}
|
||||
|
||||
const filtered = CompositorService.filterCurrentWorkspace(CompositorService.sortedToplevels, screenName);
|
||||
return filtered.length > 0;
|
||||
shouldHideForWindows = filtered.length > 0;
|
||||
}
|
||||
|
||||
property real effectiveSpacing: hasMaximizedToplevel ? 0 : (barConfig?.spacing ?? 4)
|
||||
@@ -337,8 +368,8 @@ PanelWindow {
|
||||
}
|
||||
|
||||
screen: modelData
|
||||
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) : 0
|
||||
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) : 0
|
||||
implicitHeight: !isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
implicitWidth: isVertical ? Theme.px(effectiveBarThickness + effectiveSpacing + ((barConfig?.gothCornersEnabled ?? false) && !hasMaximizedToplevel ? _wingR : 0), _dpr) + _shadowBuffer : 0
|
||||
color: "transparent"
|
||||
|
||||
property var nativeInhibitor: null
|
||||
@@ -356,6 +387,9 @@ PanelWindow {
|
||||
}
|
||||
|
||||
updateGpuTempConfig();
|
||||
_updateBackgroundAlpha();
|
||||
_updateHasMaximizedToplevel();
|
||||
_updateShouldHideForWindows();
|
||||
|
||||
inhibitorInitTimer.start();
|
||||
}
|
||||
@@ -432,11 +466,37 @@ PanelWindow {
|
||||
Connections {
|
||||
function onBarConfigChanged() {
|
||||
barWindow.updateGpuTempConfig();
|
||||
barWindow._updateBackgroundAlpha();
|
||||
barWindow._updateHasMaximizedToplevel();
|
||||
barWindow._updateShouldHideForWindows();
|
||||
}
|
||||
|
||||
target: rootWindow
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onBarConfigsChanged() {
|
||||
barWindow._updateBackgroundAlpha();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: CompositorService
|
||||
function onToplevelsChanged() {
|
||||
barWindow._updateHasMaximizedToplevel();
|
||||
barWindow._updateShouldHideForWindows();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: NiriService
|
||||
function onAllWorkspacesChanged() {
|
||||
barWindow._updateHasMaximizedToplevel();
|
||||
barWindow._updateShouldHideForWindows();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onNvidiaGpuTempEnabledChanged() {
|
||||
barWindow.updateGpuTempConfig();
|
||||
@@ -506,8 +566,9 @@ PanelWindow {
|
||||
readonly property var _leftSection: topBarContent ? (barWindow.isVertical ? topBarContent.vLeftSection : topBarContent.hLeftSection) : null
|
||||
readonly property var _centerSection: topBarContent ? (barWindow.isVertical ? topBarContent.vCenterSection : topBarContent.hCenterSection) : null
|
||||
readonly property var _rightSection: topBarContent ? (barWindow.isVertical ? topBarContent.vRightSection : topBarContent.hRightSection) : null
|
||||
readonly property real _revealProgress: topBarSlide.x + topBarSlide.y
|
||||
|
||||
function sectionRect(section, isCenter) {
|
||||
function sectionRect(section, isCenter, _dep) {
|
||||
if (!section)
|
||||
return {
|
||||
"x": 0,
|
||||
@@ -536,7 +597,7 @@ PanelWindow {
|
||||
item: clickThroughEnabled ? null : inputMask
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false) : {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._leftSection, false, barWindow._revealProgress) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
@@ -549,7 +610,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true) : {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._centerSection, true, barWindow._revealProgress) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
@@ -562,7 +623,7 @@ PanelWindow {
|
||||
}
|
||||
|
||||
Region {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false) : {
|
||||
readonly property var r: barWindow.clickThroughEnabled ? barWindow.sectionRect(barWindow._rightSection, false, barWindow._revealProgress) : {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 0,
|
||||
@@ -573,19 +634,27 @@ PanelWindow {
|
||||
width: r.w
|
||||
height: r.h
|
||||
}
|
||||
|
||||
Region {
|
||||
readonly property bool active: barWindow.clickThroughEnabled && !inputMask.showing
|
||||
x: active ? inputMask.x : 0
|
||||
y: active ? inputMask.y : 0
|
||||
width: active ? inputMask.width : 0
|
||||
height: active ? inputMask.height : 0
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: topBarCore
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.enabled: false
|
||||
|
||||
property bool autoHide: barConfig?.autoHide ?? false
|
||||
property bool revealSticky: false
|
||||
|
||||
Timer {
|
||||
id: revealHold
|
||||
interval: barConfig?.autoHideDelay ?? 250
|
||||
interval: barWindow.clickThroughEnabled ? Math.max((barConfig?.autoHideDelay ?? 250) * 6, 1500) : (barConfig?.autoHideDelay ?? 250)
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!topBarMouseArea.containsMouse && !topBarCore.hasActivePopout) {
|
||||
@@ -643,7 +712,6 @@ PanelWindow {
|
||||
Connections {
|
||||
function onBarConfigChanged() {
|
||||
topBarCore.autoHide = barConfig?.autoHide ?? false;
|
||||
revealHold.interval = barConfig?.autoHideDelay ?? 250;
|
||||
}
|
||||
|
||||
target: rootWindow
|
||||
|
||||
@@ -167,9 +167,22 @@ DankPopout {
|
||||
}
|
||||
|
||||
Column {
|
||||
id: headerInfoColumn
|
||||
spacing: Theme.spacingXS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: parent.width - Theme.iconSizeLarge - 32 - Theme.spacingM * 2
|
||||
readonly property string timeInfoText: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return "Power profile management available";
|
||||
const time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown") {
|
||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
readonly property bool showPowerRate: BatteryService.batteryAvailable && Math.abs(BatteryService.changeRate) > 0.05
|
||||
readonly property bool isOnAC: BatteryService.batteryAvailable && (BatteryService.isCharging || BatteryService.isPluggedIn)
|
||||
readonly property bool isDischarging: BatteryService.batteryAvailable && !BatteryService.isCharging && !BatteryService.isPluggedIn
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingS
|
||||
@@ -207,21 +220,35 @@ DankPopout {
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
if (!BatteryService.batteryAvailable)
|
||||
return "Power profile management available";
|
||||
const time = BatteryService.formatTimeRemaining();
|
||||
if (time !== "Unknown") {
|
||||
return BatteryService.isCharging ? `Time until full: ${time}` : `Time remaining: ${time}`;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: text.length > 0
|
||||
elide: Text.ElideRight
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
visible: headerInfoColumn.timeInfoText.length > 0
|
||||
|
||||
StyledText {
|
||||
id: powerRateText
|
||||
text: `${headerInfoColumn.isOnAC ? "+" : (headerInfoColumn.isDischarging ? "-" : "")}${Math.abs(BatteryService.changeRate).toFixed(1)}W`
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: {
|
||||
if (headerInfoColumn.isOnAC) {
|
||||
return Theme.primary;
|
||||
}
|
||||
if (headerInfoColumn.isDischarging) {
|
||||
return Theme.warning;
|
||||
}
|
||||
return Theme.surfaceTextMedium;
|
||||
}
|
||||
font.weight: Font.Medium
|
||||
visible: headerInfoColumn.showPowerRate
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: headerInfoColumn.timeInfoText
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
elide: Text.ElideRight
|
||||
width: parent.width - (powerRateText.visible ? (powerRateText.implicitWidth + parent.spacing) : 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,12 +29,21 @@ Loader {
|
||||
|
||||
readonly property bool orientationMatches: (axis?.isVertical ?? false) === isInColumn
|
||||
|
||||
readonly property bool widgetEnabled: widgetData?.enabled !== false
|
||||
|
||||
active: orientationMatches && getWidgetVisible(widgetId, DgopService.dgopAvailable) && (widgetId !== "music" || MprisController.activePlayer !== null)
|
||||
sourceComponent: getWidgetComponent(widgetId, components)
|
||||
opacity: getWidgetEnabled(widgetData?.enabled) ? 1 : 0
|
||||
|
||||
signal contentItemReady(var item)
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && !root.widgetEnabled
|
||||
property: "visible"
|
||||
value: false
|
||||
restoreMode: Binding.RestoreBinding
|
||||
}
|
||||
|
||||
Binding {
|
||||
target: root.item
|
||||
when: root.item && "parentScreen" in root.item
|
||||
@@ -269,8 +278,4 @@ Loader {
|
||||
|
||||
return widgetVisibility[widgetId] ?? true;
|
||||
}
|
||||
|
||||
function getWidgetEnabled(enabled) {
|
||||
return enabled !== false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -584,8 +584,7 @@ BasePill {
|
||||
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
const desktopEntry = appId ? DesktopEntries.heuristicLookup(appId) : null;
|
||||
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
|
||||
|
||||
if (modelData.type === "grouped" && windowCount > 1) {
|
||||
@@ -690,9 +689,8 @@ BasePill {
|
||||
if (!appItem.appId)
|
||||
return "";
|
||||
if (modelData.isCoreApp)
|
||||
return ""; // Explicitly skip if core app to avoid flickering or wrong look ups
|
||||
const moddedId = Paths.moddedAppId(appItem.appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
return "";
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
|
||||
return Paths.getAppIcon(appItem.appId, desktopEntry);
|
||||
}
|
||||
smooth: true
|
||||
@@ -749,8 +747,7 @@ BasePill {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
if (!appItem.appId)
|
||||
return "?";
|
||||
const moddedId = Paths.moddedAppId(appItem.appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(appItem.appId);
|
||||
const appName = Paths.getAppName(appItem.appId, desktopEntry);
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
@@ -273,7 +273,7 @@ PanelWindow {
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
|
||||
@@ -9,9 +9,6 @@ BasePill {
|
||||
id: root
|
||||
|
||||
property bool isActive: false
|
||||
property var popoutTarget: null
|
||||
property var parentScreen: null
|
||||
property Item windowRoot: (Window.window ? Window.window.contentItem : null)
|
||||
property bool isAutoHideBar: false
|
||||
|
||||
signal clipboardClicked
|
||||
|
||||
@@ -129,7 +129,7 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const locale = Qt.locale();
|
||||
const locale = I18n.locale();
|
||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
||||
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
|
||||
@@ -144,7 +144,7 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const locale = Qt.locale();
|
||||
const locale = I18n.locale();
|
||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
||||
const value = dayFirst ? String(systemClock?.date?.getDate()).padStart(2, '0') : String(systemClock?.date?.getMonth() + 1).padStart(2, '0');
|
||||
@@ -165,7 +165,7 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const locale = Qt.locale();
|
||||
const locale = I18n.locale();
|
||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
||||
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
|
||||
@@ -180,7 +180,7 @@ BasePill {
|
||||
|
||||
StyledText {
|
||||
text: {
|
||||
const locale = Qt.locale();
|
||||
const locale = I18n.locale();
|
||||
const dateFormatShort = locale.dateFormat(Locale.ShortFormat);
|
||||
const dayFirst = dateFormatShort.indexOf('d') < dateFormatShort.indexOf('M');
|
||||
const value = dayFirst ? String(systemClock?.date?.getMonth() + 1).padStart(2, '0') : String(systemClock?.date?.getDate()).padStart(2, '0');
|
||||
@@ -311,9 +311,9 @@ BasePill {
|
||||
id: dateText
|
||||
text: {
|
||||
if (SettingsData.clockDateFormat && SettingsData.clockDateFormat.length > 0) {
|
||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), SettingsData.clockDateFormat);
|
||||
return systemClock?.date?.toLocaleDateString(I18n.locale(), SettingsData.clockDateFormat);
|
||||
}
|
||||
return systemClock?.date?.toLocaleDateString(Qt.locale(), "ddd d");
|
||||
return systemClock?.date?.toLocaleDateString(I18n.locale(), "ddd d");
|
||||
}
|
||||
font.pixelSize: clockRow.fontSize
|
||||
color: Theme.widgetTextColor
|
||||
|
||||
@@ -93,7 +93,7 @@ BasePill {
|
||||
id: textBox
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuText.paintedWidth) : cpuText.paintedWidth
|
||||
implicitWidth: root.minimumWidth ? Math.max(cpuBaseline.width, cpuCurrent.width) : cpuCurrent.width
|
||||
implicitHeight: cpuText.implicitHeight
|
||||
|
||||
width: implicitWidth
|
||||
@@ -105,6 +105,12 @@ BasePill {
|
||||
text: "88%"
|
||||
}
|
||||
|
||||
StyledTextMetrics {
|
||||
id: cpuCurrent
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
text: cpuText.text
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: cpuText
|
||||
text: {
|
||||
|
||||
@@ -9,6 +9,7 @@ BasePill {
|
||||
|
||||
property var widgetData: null
|
||||
property string mountPath: (widgetData && widgetData.mountPath !== undefined) ? widgetData.mountPath : "/"
|
||||
property int diskUsageMode: (widgetData && widgetData.diskUsageMode !== undefined) ? widgetData.diskUsageMode : 0
|
||||
property bool isHovered: mouseArea.containsMouse
|
||||
property bool isAutoHideBar: false
|
||||
|
||||
@@ -130,7 +131,13 @@ BasePill {
|
||||
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
|
||||
return "--";
|
||||
}
|
||||
return root.diskUsagePercent.toFixed(0);
|
||||
if (!root.selectedMount) return "--";
|
||||
switch (root.diskUsageMode) {
|
||||
case 1: return root.selectedMount.size || "--";
|
||||
case 2: return root.selectedMount.avail || "--";
|
||||
case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
|
||||
default: return root.diskUsagePercent.toFixed(0);
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
@@ -178,7 +185,13 @@ BasePill {
|
||||
if (root.diskUsagePercent === undefined || root.diskUsagePercent === null || root.diskUsagePercent === 0) {
|
||||
return "--%";
|
||||
}
|
||||
return root.diskUsagePercent.toFixed(0) + "%";
|
||||
if (!root.selectedMount) return "--%";
|
||||
switch (root.diskUsageMode) {
|
||||
case 1: return root.selectedMount.size || "--";
|
||||
case 2: return root.selectedMount.avail || "--";
|
||||
case 3: return (root.selectedMount.avail || "--") + " / " + (root.selectedMount.size || "--");
|
||||
default: return root.diskUsagePercent.toFixed(0) + "%";
|
||||
}
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
color: Theme.widgetTextColor
|
||||
@@ -189,7 +202,14 @@ BasePill {
|
||||
StyledTextMetrics {
|
||||
id: diskBaseline
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
text: "100%"
|
||||
text: {
|
||||
switch (root.diskUsageMode) {
|
||||
case 3: return "888.8G / 888.8G";
|
||||
case 1:
|
||||
case 2: return "888.8G";
|
||||
default: return "100%";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
width: Math.max(diskBaseline.width, paintedWidth)
|
||||
|
||||
@@ -178,8 +178,9 @@ BasePill {
|
||||
if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
|
||||
const globalPos = parent.mapToItem(null, 0, 0);
|
||||
const currentScreen = root.parentScreen || Screen;
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width);
|
||||
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen);
|
||||
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, parent.width, root.barSpacing, barPosition, root.barConfig);
|
||||
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
@@ -334,8 +335,9 @@ BasePill {
|
||||
if (root.popoutTarget && root.popoutTarget.setTriggerPosition) {
|
||||
const globalPos = mapToItem(null, 0, 0);
|
||||
const currentScreen = root.parentScreen || Screen;
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width);
|
||||
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen);
|
||||
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, root.barThickness, root.width, root.barSpacing, barPosition, root.barConfig);
|
||||
root.popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, root.section, currentScreen, barPosition, root.barThickness, root.barSpacing, root.barConfig);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
@@ -18,9 +18,43 @@ BasePill {
|
||||
readonly property real contentWidth: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
|
||||
readonly property real contentHeight: hasActivePrivacy ? (activeCount * 18 + (activeCount - 1) * Theme.spacingXS) : 0
|
||||
|
||||
visible: hasActivePrivacy
|
||||
opacity: hasActivePrivacy ? 1 : 0
|
||||
enabled: hasActivePrivacy
|
||||
|
||||
states: [
|
||||
State {
|
||||
name: "hidden_horizontal"
|
||||
when: !hasActivePrivacy && !isVerticalOrientation
|
||||
PropertyChanges {
|
||||
target: root
|
||||
width: 0
|
||||
}
|
||||
},
|
||||
State {
|
||||
name: "hidden_vertical"
|
||||
when: !hasActivePrivacy && isVerticalOrientation
|
||||
PropertyChanges {
|
||||
target: root
|
||||
height: 0
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
transitions: [
|
||||
Transition {
|
||||
NumberAnimation {
|
||||
properties: "width,height"
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
@@ -35,7 +69,7 @@ BasePill {
|
||||
Item {
|
||||
width: 18
|
||||
height: 18
|
||||
visible: PrivacyService.microphoneActive
|
||||
visible: root.showMicIcon || PrivacyService.microphoneActive
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
@@ -47,8 +81,8 @@ BasePill {
|
||||
return "mic";
|
||||
}
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.error
|
||||
filled: true
|
||||
color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText
|
||||
filled: PrivacyService.microphoneActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@@ -56,14 +90,14 @@ BasePill {
|
||||
Item {
|
||||
width: 18
|
||||
height: 18
|
||||
visible: PrivacyService.cameraActive
|
||||
visible: root.showCameraIcon || PrivacyService.cameraActive
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "camera_video"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.widgetTextColor
|
||||
filled: true
|
||||
color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText
|
||||
filled: PrivacyService.cameraActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
@@ -76,20 +110,21 @@ BasePill {
|
||||
anchors.top: parent.top
|
||||
anchors.rightMargin: -2
|
||||
anchors.topMargin: -1
|
||||
visible: PrivacyService.cameraActive
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 18
|
||||
height: 18
|
||||
visible: PrivacyService.screensharingActive
|
||||
visible: root.showScreenSharingIcon || PrivacyService.screensharingActive
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
DankIcon {
|
||||
name: "screen_share"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.warning
|
||||
filled: true
|
||||
color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText
|
||||
filled: PrivacyService.screensharingActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@@ -116,7 +151,7 @@ BasePill {
|
||||
}
|
||||
size: Theme.iconSizeSmall
|
||||
color: PrivacyService.microphoneActive ? Theme.error : Theme.surfaceText
|
||||
filled: true
|
||||
filled: PrivacyService.microphoneActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@@ -131,7 +166,7 @@ BasePill {
|
||||
name: "camera_video"
|
||||
size: Theme.iconSizeSmall
|
||||
color: PrivacyService.cameraActive ? Theme.error : Theme.surfaceText
|
||||
filled: true
|
||||
filled: PrivacyService.cameraActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
|
||||
@@ -158,7 +193,7 @@ BasePill {
|
||||
name: "screen_share"
|
||||
size: Theme.iconSizeSmall
|
||||
color: PrivacyService.screensharingActive ? Theme.warning : Theme.surfaceText
|
||||
filled: true
|
||||
filled: PrivacyService.screensharingActive
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
@@ -209,22 +244,4 @@ BasePill {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on width {
|
||||
enabled: hasActivePrivacy && visible && !isVerticalOrientation
|
||||
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on height {
|
||||
enabled: hasActivePrivacy && visible && isVerticalOrientation
|
||||
|
||||
NumberAnimation {
|
||||
duration: Theme.mediumDuration
|
||||
easing.type: Theme.emphasizedEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ BasePill {
|
||||
property var widgetData: null
|
||||
property bool minimumWidth: (widgetData && widgetData.minimumWidth !== undefined) ? widgetData.minimumWidth : true
|
||||
property bool showSwap: (widgetData && widgetData.showSwap !== undefined) ? widgetData.showSwap : false
|
||||
property bool showInGb: (widgetData && widgetData.showInGb !== undefined) ? widgetData.showInGb : false
|
||||
readonly property real swapUsage: DgopService.totalSwapKB > 0 ? (DgopService.usedSwapKB / DgopService.totalSwapKB) * 100 : 0
|
||||
|
||||
signal ramClicked
|
||||
@@ -59,6 +60,10 @@ BasePill {
|
||||
return "--";
|
||||
}
|
||||
|
||||
if (root.showInGb) {
|
||||
return (DgopService.usedMemoryMB / 1024).toFixed(1);
|
||||
}
|
||||
|
||||
return DgopService.memoryUsage.toFixed(0);
|
||||
}
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
@@ -113,13 +118,14 @@ BasePill {
|
||||
id: ramBaseline
|
||||
font.pixelSize: Theme.barTextSize(root.barThickness, root.barConfig?.fontScale, root.barConfig?.maximizeWidgetText)
|
||||
text: {
|
||||
let baseText = root.showInGb ? "88.8 GB" : "88%";
|
||||
if (!root.showSwap) {
|
||||
return "88%";
|
||||
return baseText;
|
||||
}
|
||||
if (root.swapUsage < 10) {
|
||||
return "88% · 0%";
|
||||
return baseText + " · 0%";
|
||||
}
|
||||
return "88% · 88%";
|
||||
return baseText + " · 88%";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,10 +133,16 @@ BasePill {
|
||||
id: ramText
|
||||
text: {
|
||||
if (DgopService.memoryUsage === undefined || DgopService.memoryUsage === null || DgopService.memoryUsage === 0) {
|
||||
return "--%";
|
||||
return root.showInGb ? "-- GB" : "--%";
|
||||
}
|
||||
|
||||
let ramText = "";
|
||||
if (root.showInGb) {
|
||||
ramText = (DgopService.usedMemoryMB / 1024).toFixed(1) + " GB";
|
||||
} else {
|
||||
ramText = DgopService.memoryUsage.toFixed(0) + "%";
|
||||
}
|
||||
|
||||
let ramText = DgopService.memoryUsage.toFixed(0) + "%";
|
||||
if (root.showSwap && DgopService.totalSwapKB > 0) {
|
||||
return ramText + " · " + root.swapUsage.toFixed(0) + "%";
|
||||
}
|
||||
|
||||
@@ -241,14 +241,17 @@ BasePill {
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
readonly property string effectiveAppId: {
|
||||
root._appIdSubstitutionsTrigger;
|
||||
return Paths.moddedAppId(appId);
|
||||
}
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
|
||||
const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null;
|
||||
const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown";
|
||||
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)";
|
||||
@@ -284,11 +287,10 @@ BasePill {
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
root._appIdSubstitutionsTrigger;
|
||||
if (!appId)
|
||||
if (!effectiveAppId)
|
||||
return "";
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
return Paths.getAppIcon(appId, desktopEntry);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
||||
return Paths.getAppIcon(effectiveAppId, desktopEntry);
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
@@ -311,20 +313,18 @@ BasePill {
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
visible: !iconImg.visible && Paths.isSteamApp(appId)
|
||||
visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible && !Paths.isSteamApp(appId)
|
||||
visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId)
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
if (!appId)
|
||||
if (!effectiveAppId)
|
||||
return "?";
|
||||
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
const appName = Paths.getAppName(appId, desktopEntry);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
||||
const appName = Paths.getAppName(effectiveAppId, desktopEntry);
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
@@ -496,14 +496,17 @@ BasePill {
|
||||
property var toplevelData: isGrouped ? (modelData.windows.length > 0 ? modelData.windows[0].toplevel : null) : modelData
|
||||
property bool isFocused: isGrouped ? (root.focusedAppId === appId) : (toplevelData ? toplevelData.activated : false)
|
||||
property string appId: isGrouped ? modelData.appId : (modelData.appId || "")
|
||||
readonly property string effectiveAppId: {
|
||||
root._appIdSubstitutionsTrigger;
|
||||
return Paths.moddedAppId(appId);
|
||||
}
|
||||
property string windowTitle: toplevelData ? (toplevelData.title || "(Unnamed)") : "(Unnamed)"
|
||||
property var toplevelObject: toplevelData
|
||||
property int windowCount: isGrouped ? modelData.windows.length : 1
|
||||
property string tooltipText: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = moddedId ? DesktopEntries.heuristicLookup(moddedId) : null;
|
||||
const appName = appId ? Paths.getAppName(appId, desktopEntry) : "Unknown";
|
||||
const desktopEntry = effectiveAppId ? DesktopEntries.heuristicLookup(effectiveAppId) : null;
|
||||
const appName = effectiveAppId ? Paths.getAppName(effectiveAppId, desktopEntry) : "Unknown";
|
||||
|
||||
if (isGrouped && windowCount > 1) {
|
||||
return appName + " (" + windowCount + " windows)";
|
||||
@@ -538,11 +541,10 @@ BasePill {
|
||||
source: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
root._appIdSubstitutionsTrigger;
|
||||
if (!appId)
|
||||
if (!effectiveAppId)
|
||||
return "";
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
return Paths.getAppIcon(appId, desktopEntry);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
||||
return Paths.getAppIcon(effectiveAppId, desktopEntry);
|
||||
}
|
||||
smooth: true
|
||||
mipmap: true
|
||||
@@ -565,20 +567,18 @@ BasePill {
|
||||
size: Theme.barIconSize(root.barThickness, undefined, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
name: "sports_esports"
|
||||
color: Theme.widgetTextColor
|
||||
visible: !iconImg.visible && Paths.isSteamApp(appId)
|
||||
visible: !iconImg.visible && Paths.isSteamApp(effectiveAppId)
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
visible: !iconImg.visible && !Paths.isSteamApp(appId)
|
||||
visible: !iconImg.visible && !Paths.isSteamApp(effectiveAppId)
|
||||
text: {
|
||||
root._desktopEntriesUpdateTrigger;
|
||||
if (!appId)
|
||||
if (!effectiveAppId)
|
||||
return "?";
|
||||
|
||||
const moddedId = Paths.moddedAppId(appId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
const appName = Paths.getAppName(appId, desktopEntry);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(effectiveAppId);
|
||||
const appName = Paths.getAppName(effectiveAppId, desktopEntry);
|
||||
return appName.charAt(0).toUpperCase();
|
||||
}
|
||||
font.pixelSize: 10
|
||||
|
||||
@@ -41,6 +41,12 @@ BasePill {
|
||||
return `${id}::${tooltipTitle}`;
|
||||
}
|
||||
|
||||
// ! TODO - replace with either native dbus client (like plugins use) or just a DMS cli or something
|
||||
function callContextMenuFallback(trayItemId, globalX, globalY) {
|
||||
const script = ['ITEMS=$(dbus-send --session --print-reply --dest=org.kde.StatusNotifierWatcher /StatusNotifierWatcher org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierWatcher string:RegisteredStatusNotifierItems 2>/dev/null)', 'while IFS= read -r line; do', ' line="${line#*\\\"}"', ' line="${line%\\\"*}"', ' [ -z "$line" ] && continue', ' BUS="${line%%/*}"', ' OBJ="/${line#*/}"', ' ID=$(dbus-send --session --print-reply --dest="$BUS" "$OBJ" org.freedesktop.DBus.Properties.Get string:org.kde.StatusNotifierItem string:Id 2>/dev/null | grep -oP "(?<=\\\")(.*?)(?=\\\")" | tail -1)', ' if [ "$ID" = "$1" ]; then', ' dbus-send --session --type=method_call --dest="$BUS" "$OBJ" org.kde.StatusNotifierItem.ContextMenu int32:"$2" int32:"$3"', ' exit 0', ' fi', 'done <<< "$ITEMS"',].join("\n");
|
||||
Quickshell.execDetached(["bash", "-c", script, "_", trayItemId, String(globalX), String(globalY)]);
|
||||
}
|
||||
|
||||
property int _trayOrderTrigger: 0
|
||||
|
||||
Connections {
|
||||
@@ -380,8 +386,11 @@ BasePill {
|
||||
return;
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
if (!delegateRoot.trayItem?.hasMenu)
|
||||
if (!delegateRoot.trayItem?.hasMenu) {
|
||||
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
@@ -637,8 +646,11 @@ BasePill {
|
||||
return;
|
||||
if (mouse.button !== Qt.RightButton)
|
||||
return;
|
||||
if (!delegateRoot.trayItem?.hasMenu)
|
||||
if (!delegateRoot.trayItem?.hasMenu) {
|
||||
const gp = trayItemArea.mapToGlobal(mouse.x, mouse.y);
|
||||
root.callContextMenuFallback(delegateRoot.trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
root.menuOpen = false;
|
||||
root.showForTrayItem(delegateRoot.trayItem, visualContent, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
@@ -928,9 +940,10 @@ BasePill {
|
||||
}
|
||||
})(), overflowMenu.dpr)
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property var elev: Theme.elevationLevel2
|
||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
||||
|
||||
@@ -951,37 +964,26 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
ElevationShadow {
|
||||
id: bgShadowLayer
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
level: menuContainer.elev
|
||||
fallbackOffset: 4
|
||||
shadowBlurPx: menuContainer.shadowBlurPx
|
||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
sourceRect.smooth: true
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * overflowMenu.dpr * 2), Math.round(height * overflowMenu.dpr * 2))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
layer.samples: 4
|
||||
|
||||
readonly property int blurMax: 64
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / bgShadowLayer.blurMax))
|
||||
shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(bgShadowLayer.width, bgShadowLayer.height))
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
smooth: true
|
||||
}
|
||||
}
|
||||
|
||||
Grid {
|
||||
@@ -1065,9 +1067,11 @@ BasePill {
|
||||
root.menuOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!trayItem.hasMenu)
|
||||
if (!trayItem.hasMenu) {
|
||||
const gp = itemArea.mapToGlobal(mouse.x, mouse.y);
|
||||
root.callContextMenuFallback(trayItem.id, Math.round(gp.x), Math.round(gp.y));
|
||||
return;
|
||||
}
|
||||
root.showForTrayItem(trayItem, menuContainer, parentScreen, root.isAtBottom, root.isVerticalOrientation, root.axis);
|
||||
}
|
||||
}
|
||||
@@ -1398,9 +1402,10 @@ BasePill {
|
||||
}
|
||||
})(), menuWindow.dpr)
|
||||
|
||||
property real shadowBlurPx: 10
|
||||
property real shadowSpreadPx: 0
|
||||
property real shadowBaseAlpha: 0.60
|
||||
readonly property var elev: Theme.elevationLevel2
|
||||
property real shadowBlurPx: elev && elev.blurPx !== undefined ? elev.blurPx : 8
|
||||
property real shadowSpreadPx: elev && elev.spreadPx !== undefined ? elev.spreadPx : 0
|
||||
property real shadowBaseAlpha: elev && elev.alpha !== undefined ? elev.alpha : 0.25
|
||||
readonly property real popupSurfaceAlpha: Theme.popupTransparency
|
||||
readonly property real effectiveShadowAlpha: Math.max(0, Math.min(1, shadowBaseAlpha * popupSurfaceAlpha))
|
||||
|
||||
@@ -1421,35 +1426,24 @@ BasePill {
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
ElevationShadow {
|
||||
id: menuBgShadowLayer
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
level: menuContainer.elev
|
||||
fallbackOffset: 4
|
||||
shadowBlurPx: menuContainer.shadowBlurPx
|
||||
shadowSpreadPx: menuContainer.shadowSpreadPx
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
targetRadius: Theme.cornerRadius
|
||||
sourceRect.antialiasing: true
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
layer.smooth: true
|
||||
layer.textureSize: Qt.size(Math.round(width * menuWindow.dpr), Math.round(height * menuWindow.dpr))
|
||||
layer.textureMirroring: ShaderEffectSource.MirrorVertically
|
||||
|
||||
readonly property int blurMax: 64
|
||||
|
||||
layer.effect: MultiEffect {
|
||||
autoPaddingEnabled: true
|
||||
shadowEnabled: true
|
||||
blurEnabled: false
|
||||
maskEnabled: false
|
||||
shadowBlur: Math.max(0, Math.min(1, menuContainer.shadowBlurPx / menuBgShadowLayer.blurMax))
|
||||
shadowScale: 1 + (2 * menuContainer.shadowSpreadPx) / Math.max(1, Math.min(menuBgShadowLayer.width, menuBgShadowLayer.height))
|
||||
shadowColor: {
|
||||
const baseColor = Theme.isLightMode ? Qt.rgba(0, 0, 0, 1) : Theme.surfaceContainerHighest;
|
||||
return Theme.withAlpha(baseColor, menuContainer.effectiveShadowAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
|
||||
radius: Theme.cornerRadius
|
||||
antialiasing: true
|
||||
}
|
||||
}
|
||||
|
||||
QsMenuAnchor {
|
||||
|
||||
@@ -177,8 +177,9 @@ BasePill {
|
||||
if (popoutTarget && popoutTarget.setTriggerPosition) {
|
||||
const globalPos = root.visualContent.mapToItem(null, 0, 0);
|
||||
const currentScreen = parentScreen || Screen;
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth);
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen);
|
||||
const barPosition = root.axis?.edge === "left" ? 2 : (root.axis?.edge === "right" ? 3 : (root.axis?.edge === "top" ? 0 : 1));
|
||||
const pos = SettingsData.getPopupTriggerPosition(globalPos, currentScreen, barThickness, root.visualWidth, root.barSpacing, barPosition, root.barConfig);
|
||||
popoutTarget.setTriggerPosition(pos.x, pos.y, pos.width, section, currentScreen, barPosition, barThickness, root.barSpacing, root.barConfig);
|
||||
}
|
||||
root.clicked();
|
||||
}
|
||||
|
||||
@@ -282,14 +282,15 @@ Item {
|
||||
}
|
||||
|
||||
const keyBase = (w.app_id || w.appId || w.class || w.windowClass || "unknown");
|
||||
const key = isActiveWs || !SettingsData.groupWorkspaceApps ? `${keyBase}_${i}` : keyBase;
|
||||
const moddedId = Paths.moddedAppId(keyBase);
|
||||
const key = isActiveWs || !SettingsData.groupWorkspaceApps ? `${moddedId}_${i}` : moddedId;
|
||||
|
||||
if (!byApp[key]) {
|
||||
const isQuickshell = keyBase === "org.quickshell";
|
||||
const isSteamApp = Paths.isSteamApp(keyBase);
|
||||
const moddedId = Paths.moddedAppId(keyBase);
|
||||
const isSteamApp = Paths.isSteamApp(moddedId);
|
||||
const desktopEntry = DesktopEntries.heuristicLookup(moddedId);
|
||||
const icon = Paths.getAppIcon(keyBase, desktopEntry);
|
||||
const icon = Paths.getAppIcon(moddedId, desktopEntry);
|
||||
const appName = Paths.getAppName(moddedId, desktopEntry);
|
||||
byApp[key] = {
|
||||
"type": "icon",
|
||||
"icon": icon,
|
||||
@@ -298,7 +299,7 @@ Item {
|
||||
"active": !!((w.activated || w.is_focused) || (CompositorService.isNiri && w.is_focused)),
|
||||
"count": 1,
|
||||
"windowId": w.address || w.id,
|
||||
"fallbackText": w.appId || w.class || w.title || ""
|
||||
"fallbackText": appName || ""
|
||||
};
|
||||
} else {
|
||||
byApp[key].count++;
|
||||
@@ -1473,9 +1474,44 @@ Item {
|
||||
IconImage {
|
||||
id: rowAppIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
source: modelData.icon || ""
|
||||
opacity: modelData.active ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && rowAppIcon.status !== Image.Ready
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||
font.pixelSize: parent.width * 0.45
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.isQuickshell && modelData.isSteamApp && rowSteamIcon.status !== Image.Ready
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : rowAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: parent.width * 0.7
|
||||
name: "sports_esports"
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
@@ -1592,9 +1628,44 @@ Item {
|
||||
IconImage {
|
||||
id: colAppIcon
|
||||
anchors.fill: parent
|
||||
source: modelData.icon
|
||||
source: modelData.icon || ""
|
||||
opacity: modelData.active ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && status === Image.Ready
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.isQuickshell && !modelData.isSteamApp && colAppIcon.status !== Image.Ready
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: (modelData.fallbackText || "?").charAt(0).toUpperCase()
|
||||
font.pixelSize: parent.width * 0.45
|
||||
color: Theme.primary
|
||||
font.weight: Font.Bold
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
visible: !modelData.isQuickshell && modelData.isSteamApp && colSteamIcon.status !== Image.Ready
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius * (root.appIconSize / 40)
|
||||
border.width: 1
|
||||
border.color: Theme.primarySelected
|
||||
opacity: (modelData.active || isActive) ? 1.0 : colAppMouseArea.containsMouse ? 0.8 : 0.6
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
size: parent.width * 0.7
|
||||
name: "sports_esports"
|
||||
color: Theme.primary
|
||||
}
|
||||
}
|
||||
|
||||
IconImage {
|
||||
|
||||
@@ -16,7 +16,6 @@ DankPopout {
|
||||
popupHeight: contentLoader.item ? contentLoader.item.implicitHeight : 500
|
||||
triggerWidth: 80
|
||||
screen: triggerScreen
|
||||
shouldBeVisible: dashVisible
|
||||
|
||||
property bool __focusArmed: false
|
||||
property bool __contentReady: false
|
||||
|
||||
@@ -44,6 +44,43 @@ Item {
|
||||
|
||||
property int __volumeHoverCount: 0
|
||||
|
||||
readonly property bool directionalEffect: Theme.isDirectionalEffect
|
||||
readonly property bool depthEffect: Theme.isDepthEffect
|
||||
|
||||
function panelMotionX(panelWidth, active) {
|
||||
if (active)
|
||||
return 0;
|
||||
if (directionalEffect) {
|
||||
const travel = Math.max(Theme.effectAnimOffset, panelWidth * 0.85);
|
||||
return isRightEdge ? -travel : travel;
|
||||
}
|
||||
if (depthEffect) {
|
||||
const travel = Math.max(Theme.effectAnimOffset * 0.7, panelWidth * 0.32);
|
||||
return isRightEdge ? -travel : travel;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function panelMotionY(panelType, panelHeight, active) {
|
||||
if (active)
|
||||
return 0;
|
||||
if (directionalEffect) {
|
||||
if (panelType === 2)
|
||||
return panelHeight * 0.08;
|
||||
if (panelType === 3)
|
||||
return -panelHeight * 0.08;
|
||||
return 0;
|
||||
}
|
||||
if (depthEffect) {
|
||||
if (panelType === 2)
|
||||
return panelHeight * 0.04;
|
||||
if (panelType === 3)
|
||||
return -panelHeight * 0.04;
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
function volumeAreaEntered() {
|
||||
__volumeHoverCount++;
|
||||
panelEntered();
|
||||
@@ -62,41 +99,62 @@ Item {
|
||||
visible: dropdownType === 1 && volumeAvailable
|
||||
width: 60
|
||||
height: 180
|
||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||
y: anchorPos.y - height / 2
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 1)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(1, height, dropdownType === 1)
|
||||
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
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 1 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
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 x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 1)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 1 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: volumeShadowLayer
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: volumePanel.radius
|
||||
targetColor: volumePanel.color
|
||||
borderColor: volumePanel.border.color
|
||||
borderWidth: volumePanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
@@ -193,44 +251,65 @@ Item {
|
||||
|
||||
Rectangle {
|
||||
id: audioDevicesPanel
|
||||
visible: dropdownType === 2
|
||||
visible: dropdownType === 2 && activePlayer !== null
|
||||
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
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 2)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(2, height, dropdownType === 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
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 2 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
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 x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 2)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 2 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: audioDevicesShadowLayer
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: audioDevicesPanel.radius
|
||||
targetColor: audioDevicesPanel.color
|
||||
borderColor: audioDevicesPanel.border.color
|
||||
borderWidth: audioDevicesPanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
Column {
|
||||
@@ -346,41 +425,62 @@ Item {
|
||||
visible: dropdownType === 3
|
||||
width: 240
|
||||
height: Math.max(180, Math.min(240, (allPlayers?.length || 0) * 50 + 80))
|
||||
x: isRightEdge ? anchorPos.x : anchorPos.x - width
|
||||
y: anchorPos.y - height / 2
|
||||
x: (isRightEdge ? anchorPos.x : anchorPos.x - width) + panelMotionX(width, dropdownType === 3)
|
||||
y: anchorPos.y - height / 2 + panelMotionY(3, height, dropdownType === 3)
|
||||
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
|
||||
opacity: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : 0)
|
||||
scale: Theme.isDirectionalEffect ? 1 : (dropdownType === 3 ? 1 : Theme.effectScaleCollapsed)
|
||||
transformOrigin: isRightEdge ? Item.Left : Item.Right
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
DankAnim {
|
||||
duration: Math.round(Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3) * Theme.variantOpacityDurationScale)
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on scale {
|
||||
enabled: !Theme.isDirectionalEffect
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.expressiveDefaultSpatial
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.expressiveDefaultSpatial
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
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 x {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: Theme.variantDuration(Theme.expressiveDurations.expressiveDefaultSpatial, dropdownType === 3)
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: dropdownType === 3 ? Theme.variantPopoutEnterCurve : Theme.variantPopoutExitCurve
|
||||
}
|
||||
}
|
||||
|
||||
ElevationShadow {
|
||||
id: playersShadowLayer
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel2
|
||||
fallbackOffset: 4
|
||||
targetRadius: playersPanel.radius
|
||||
targetColor: playersPanel.color
|
||||
borderColor: playersPanel.border.color
|
||||
borderWidth: playersPanel.border.width
|
||||
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
|
||||
Column {
|
||||
|
||||
@@ -529,14 +529,15 @@ Item {
|
||||
onClicked: activePlayer && activePlayer.togglePlaying()
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 0
|
||||
shadowBlur: 1.0
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.3)
|
||||
shadowOpacity: 0.3
|
||||
ElevationShadow {
|
||||
anchors.fill: parent
|
||||
z: -1
|
||||
level: Theme.elevationLevel1
|
||||
fallbackOffset: 1
|
||||
targetRadius: parent.radius
|
||||
targetColor: parent.color
|
||||
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,15 @@ Rectangle {
|
||||
|
||||
signal closeDash
|
||||
|
||||
function weekStartQt() {
|
||||
if (SettingsData.firstDayOfWeek >= 7 || SettingsData.firstDayOfWeek < 0) {
|
||||
return Qt.locale().firstDayOfWeek;
|
||||
}
|
||||
return SettingsData.firstDayOfWeek;
|
||||
}
|
||||
|
||||
function weekStartJs() {
|
||||
return Qt.locale().firstDayOfWeek % 7;
|
||||
return weekStartQt() % 7;
|
||||
}
|
||||
|
||||
function startOfWeek(dateObj) {
|
||||
@@ -179,7 +186,7 @@ Rectangle {
|
||||
StyledText {
|
||||
width: parent.width - 56
|
||||
height: 28
|
||||
text: calendarGrid.displayDate.toLocaleDateString(Qt.locale(), "MMMM yyyy")
|
||||
text: calendarGrid.displayDate.toLocaleDateString(I18n.locale(), "MMMM yyyy")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
@@ -223,11 +230,10 @@ Rectangle {
|
||||
Repeater {
|
||||
model: {
|
||||
const days = [];
|
||||
const loc = Qt.locale();
|
||||
const qtFirst = loc.firstDayOfWeek;
|
||||
const qtFirst = weekStartQt();
|
||||
for (let i = 0; i < 7; ++i) {
|
||||
const qtDay = ((qtFirst - 1 + i) % 7) + 1;
|
||||
days.push(loc.dayName(qtDay, Locale.ShortFormat));
|
||||
days.push(I18n.locale().dayName(qtDay, Locale.ShortFormat));
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ Card {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: systemClock?.date?.toLocaleDateString(Qt.locale(), "MMM dd")
|
||||
text: systemClock?.date?.toLocaleDateString(I18n.locale(), "MMM dd")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.7)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
@@ -241,14 +241,15 @@ Item {
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
layer.enabled: true
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||
shadowOpacity: 0.2
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel1)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel1, 1)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel1 && Theme.elevationLevel1.blurPx !== undefined ? Theme.elevationLevel1.blurPx : 4) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel1)
|
||||
shadowOpacity: Theme.elevationLevel1 && Theme.elevationLevel1.alpha !== undefined ? Theme.elevationLevel1.alpha : 0.2
|
||||
}
|
||||
}
|
||||
|
||||
@@ -812,14 +813,14 @@ Item {
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (moonPhase.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (moonPhase.height / 2) + skyBox.vMargin
|
||||
|
||||
layer.enabled: true
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||
shadowOpacity: 0.2
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -834,14 +835,14 @@ Item {
|
||||
x: (pos?.h ?? 0) * skyBox.effectiveWidth - (sun.width / 2) + skyBox.hMargin
|
||||
y: (pos?.v ?? 0) * -(skyBox.effectiveHeight / 2) + skyBox.effectiveHeight / 2 - (sun.height / 2) + skyBox.vMargin
|
||||
|
||||
layer.enabled: true
|
||||
layer.enabled: Theme.elevationEnabled
|
||||
layer.effect: MultiEffect {
|
||||
shadowEnabled: true
|
||||
shadowHorizontalOffset: 0
|
||||
shadowVerticalOffset: 4
|
||||
shadowBlur: 0.8
|
||||
shadowColor: Qt.rgba(0, 0, 0, 0.2)
|
||||
shadowOpacity: 0.2
|
||||
shadowEnabled: Theme.elevationEnabled
|
||||
shadowHorizontalOffset: Theme.elevationOffsetX(Theme.elevationLevel2)
|
||||
shadowVerticalOffset: Theme.elevationOffsetY(Theme.elevationLevel2, 4)
|
||||
shadowBlur: Theme.elevationEnabled ? Math.max(0, Math.min(1, (Theme.elevationLevel2 && Theme.elevationLevel2.blurPx !== undefined ? Theme.elevationLevel2.blurPx : 8) / Theme.elevationBlurMax)) : 0
|
||||
blurMax: Theme.elevationBlurMax
|
||||
shadowColor: Theme.elevationShadowColor(Theme.elevationLevel2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,21 +378,27 @@ Variants {
|
||||
const screenY = dock.screen ? (dock.screen.y || 0) : 0;
|
||||
const screenHeight = dock.screen ? dock.screen.height : 0;
|
||||
|
||||
const gap = Theme.spacingS;
|
||||
const bgMargin = barSpacing + SettingsData.dockMargin + 1 + dock.borderThickness;
|
||||
const btnW = dock.hoveredButton.width;
|
||||
const btnH = dock.hoveredButton.height;
|
||||
|
||||
if (!dock.isVertical) {
|
||||
const isBottom = SettingsData.dockPosition === SettingsData.Position.Bottom;
|
||||
const globalX = buttonGlobalPos.x + dock.hoveredButton.width / 2 + adjacentLeftBarWidth;
|
||||
const globalX = buttonGlobalPos.x + btnW / 2 + adjacentLeftBarWidth;
|
||||
const tooltipHeight = 32;
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + barSpacing + Theme.spacingM;
|
||||
const screenRelativeY = isBottom ? (screenHeight - tooltipOffset - tooltipHeight) : tooltipOffset;
|
||||
const totalFromEdge = bgMargin + dockBackground.height + dock.borderThickness + gap;
|
||||
const screenRelativeY = isBottom ? (screenHeight - totalFromEdge - tooltipHeight) : totalFromEdge;
|
||||
dockTooltip.show(tooltipText, globalX, screenRelativeY, dock.screen, false, false);
|
||||
return;
|
||||
}
|
||||
|
||||
const isLeft = SettingsData.dockPosition === SettingsData.Position.Left;
|
||||
const tooltipOffset = dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin + barSpacing + Theme.spacingM;
|
||||
const tooltipX = isLeft ? tooltipOffset : (dock.screen.width - tooltipOffset);
|
||||
const screenRelativeY = buttonGlobalPos.y - screenY + dock.hoveredButton.height / 2 + adjacentTopBarHeight;
|
||||
dockTooltip.show(tooltipText, screenX + tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft);
|
||||
const screenWidth = dock.screen ? dock.screen.width : 0;
|
||||
const totalFromEdge = bgMargin + dockBackground.width + dock.borderThickness + gap;
|
||||
const tooltipX = isLeft ? (screenX + totalFromEdge) : (screenX + screenWidth - totalFromEdge);
|
||||
const screenRelativeY = buttonGlobalPos.y - screenY + btnH / 2 + adjacentTopBarHeight;
|
||||
dockTooltip.show(tooltipText, tooltipX, screenRelativeY, dock.screen, isLeft, !isLeft);
|
||||
}
|
||||
|
||||
Connections {
|
||||
@@ -441,9 +447,8 @@ Variants {
|
||||
|
||||
height: {
|
||||
if (dock.isVertical) {
|
||||
if (!dock.reveal)
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5);
|
||||
return Math.min(dockBackground.height + 8 + dock.borderThickness, maxDockHeight);
|
||||
// Keep the taller hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(Math.max(dockBackground.height + 64, 200), screenHeight * 0.5);
|
||||
}
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
}
|
||||
@@ -451,8 +456,7 @@ Variants {
|
||||
if (dock.isVertical) {
|
||||
return dock.reveal ? px(dock.effectiveBarHeight + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin) : 1;
|
||||
}
|
||||
if (!dock.reveal)
|
||||
return Math.min(Math.max(dockBackground.width + 64, 200), screenWidth * 0.5);
|
||||
// Keep the wider hit area regardless of the reveal state to prevent shrinking loop
|
||||
return Math.min(dockBackground.width + 8 + dock.borderThickness, maxDockWidth);
|
||||
}
|
||||
anchors {
|
||||
|
||||
@@ -329,7 +329,7 @@ PanelWindow {
|
||||
|
||||
IconImage {
|
||||
anchors.fill: parent
|
||||
source: modelData.icon ? Quickshell.iconPath(modelData.icon, true) : ""
|
||||
source: modelData.icon ? Paths.resolveIconPath(modelData.icon) : ""
|
||||
smooth: true
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready
|
||||
|
||||
@@ -41,6 +41,11 @@ Singleton {
|
||||
property string lockDateFormat: ""
|
||||
property bool lockScreenShowPowerActions: true
|
||||
property bool lockScreenShowProfileImage: true
|
||||
property bool powerActionConfirm: true
|
||||
property real powerActionHoldDuration: 0.5
|
||||
property var powerMenuActions: ["reboot", "logout", "poweroff", "lock", "suspend", "restart"]
|
||||
property string powerMenuDefaultAction: "logout"
|
||||
property bool powerMenuGridLayout: false
|
||||
property var screenPreferences: ({})
|
||||
property int animationSpeed: 2
|
||||
property string wallpaperFillMode: "Fill"
|
||||
@@ -75,6 +80,11 @@ Singleton {
|
||||
lockDateFormat = settings.lockDateFormat !== undefined ? settings.lockDateFormat : "";
|
||||
lockScreenShowPowerActions = settings.lockScreenShowPowerActions !== undefined ? settings.lockScreenShowPowerActions : true;
|
||||
lockScreenShowProfileImage = settings.lockScreenShowProfileImage !== undefined ? settings.lockScreenShowProfileImage : true;
|
||||
powerActionConfirm = settings.powerActionConfirm !== undefined ? settings.powerActionConfirm : true;
|
||||
powerActionHoldDuration = settings.powerActionHoldDuration !== undefined ? settings.powerActionHoldDuration : 0.5;
|
||||
powerMenuActions = settings.powerMenuActions !== undefined ? settings.powerMenuActions : ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
|
||||
powerMenuDefaultAction = settings.powerMenuDefaultAction !== undefined ? settings.powerMenuDefaultAction : "logout";
|
||||
powerMenuGridLayout = settings.powerMenuGridLayout !== undefined ? settings.powerMenuGridLayout : false;
|
||||
screenPreferences = settings.screenPreferences !== undefined ? settings.screenPreferences : ({});
|
||||
animationSpeed = settings.animationSpeed !== undefined ? settings.animationSpeed : 2;
|
||||
wallpaperFillMode = settings.wallpaperFillMode !== undefined ? settings.wallpaperFillMode : "Fill";
|
||||
|
||||
@@ -218,7 +218,7 @@ Item {
|
||||
|
||||
property string fullTimeStr: {
|
||||
const format = GreetdSettings.getEffectiveTimeFormat();
|
||||
return systemClock.date.toLocaleTimeString(Qt.locale(), format);
|
||||
return systemClock.date.toLocaleTimeString(I18n.locale(), format);
|
||||
}
|
||||
property var timeParts: fullTimeStr.split(':')
|
||||
property string hours: timeParts[0] || ""
|
||||
@@ -328,9 +328,9 @@ Item {
|
||||
anchors.topMargin: 4
|
||||
text: {
|
||||
if (GreetdSettings.lockDateFormat && GreetdSettings.lockDateFormat.length > 0) {
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), GreetdSettings.lockDateFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), GreetdSettings.lockDateFormat);
|
||||
}
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
@@ -1231,6 +1231,12 @@ Item {
|
||||
LockPowerMenu {
|
||||
id: powerMenu
|
||||
showLogout: false
|
||||
powerActionConfirmOverride: GreetdSettings.powerActionConfirm
|
||||
powerActionHoldDurationOverride: GreetdSettings.powerActionHoldDuration
|
||||
powerMenuActionsOverride: GreetdSettings.powerMenuActions
|
||||
powerMenuDefaultActionOverride: GreetdSettings.powerMenuDefaultAction
|
||||
powerMenuGridLayoutOverride: GreetdSettings.powerMenuGridLayout
|
||||
requiredActions: ["poweroff"]
|
||||
onClosed: {
|
||||
if (isPrimaryScreen && inputField && inputField.forceActiveFocus) {
|
||||
Qt.callLater(() => inputField.forceActiveFocus());
|
||||
|
||||
@@ -24,13 +24,20 @@ Rectangle {
|
||||
property real holdProgress: 0
|
||||
property bool showHoldHint: false
|
||||
|
||||
readonly property bool needsConfirmation: SettingsData.powerActionConfirm
|
||||
readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
|
||||
property var powerActionConfirmOverride: undefined
|
||||
property var powerActionHoldDurationOverride: undefined
|
||||
property var powerMenuActionsOverride: undefined
|
||||
property var powerMenuDefaultActionOverride: undefined
|
||||
property var powerMenuGridLayoutOverride: undefined
|
||||
property var requiredActions: []
|
||||
|
||||
readonly property bool needsConfirmation: powerActionConfirmOverride !== undefined ? powerActionConfirmOverride : SettingsData.powerActionConfirm
|
||||
readonly property int holdDurationMs: (powerActionHoldDurationOverride !== undefined ? powerActionHoldDurationOverride : SettingsData.powerActionHoldDuration) * 1000
|
||||
|
||||
signal closed
|
||||
|
||||
function updateVisibleActions() {
|
||||
const allActions = (typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"];
|
||||
const allActions = powerMenuActionsOverride !== undefined ? powerMenuActionsOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuActions) ? SettingsData.powerMenuActions : ["logout", "suspend", "hibernate", "reboot", "poweroff"]);
|
||||
const hibernateSupported = (typeof SessionService !== "undefined" && SessionService.hibernateSupported) || false;
|
||||
let filtered = allActions.filter(action => {
|
||||
if (action === "hibernate" && !hibernateSupported)
|
||||
@@ -44,9 +51,14 @@ Rectangle {
|
||||
return true;
|
||||
});
|
||||
|
||||
for (const action of requiredActions) {
|
||||
if (!filtered.includes(action))
|
||||
filtered.push(action);
|
||||
}
|
||||
|
||||
visibleActions = filtered;
|
||||
|
||||
useGridLayout = (typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false;
|
||||
useGridLayout = powerMenuGridLayoutOverride !== undefined ? powerMenuGridLayoutOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuGridLayout !== undefined) ? SettingsData.powerMenuGridLayout : false);
|
||||
if (!useGridLayout)
|
||||
return;
|
||||
const count = visibleActions.length;
|
||||
@@ -73,7 +85,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
function getDefaultActionIndex() {
|
||||
const defaultAction = (typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend";
|
||||
const defaultAction = powerMenuDefaultActionOverride !== undefined ? powerMenuDefaultActionOverride : ((typeof SettingsData !== "undefined" && SettingsData.powerMenuDefaultAction) ? SettingsData.powerMenuDefaultAction : "suspend");
|
||||
const index = visibleActions.indexOf(defaultAction);
|
||||
return index >= 0 ? index : 0;
|
||||
}
|
||||
@@ -780,8 +792,9 @@ Rectangle {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||
readonly property real totalMs: root.holdDurationMs
|
||||
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||
readonly property real durationSec: root.holdDurationMs / 1000
|
||||
text: {
|
||||
if (root.showHoldHint)
|
||||
return I18n.tr("Hold longer to confirm");
|
||||
@@ -792,7 +805,7 @@ Rectangle {
|
||||
}
|
||||
if (totalMs < 1000)
|
||||
return I18n.tr("Hold to confirm (%1 ms)").arg(totalMs);
|
||||
return I18n.tr("Hold to confirm (%1s)").arg(SettingsData.powerActionHoldDuration);
|
||||
return I18n.tr("Hold to confirm (%1s)").arg(durationSec);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: root.showHoldHint ? Theme.warning : Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.6)
|
||||
|
||||
@@ -333,9 +333,9 @@ Item {
|
||||
visible: SettingsData.lockScreenShowDate
|
||||
text: {
|
||||
if (SettingsData.lockDateFormat && SettingsData.lockDateFormat.length > 0) {
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), SettingsData.lockDateFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), SettingsData.lockDateFormat);
|
||||
}
|
||||
return systemClock.date.toLocaleDateString(Qt.locale(), Locale.LongFormat);
|
||||
return systemClock.date.toLocaleDateString(I18n.locale(), Locale.LongFormat);
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: "white"
|
||||
@@ -687,14 +687,24 @@ Item {
|
||||
|
||||
anchors.centerIn: parent
|
||||
name: {
|
||||
if (pam.u2fPending)
|
||||
return "passkey";
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return "fingerprint_off";
|
||||
if (pam.fprint.active)
|
||||
return "fingerprint";
|
||||
if (pam.u2f.active)
|
||||
return "passkey";
|
||||
return "lock";
|
||||
}
|
||||
size: 20
|
||||
color: pam.fprint.tries >= SettingsData.maxFprintTries ? Theme.error : (passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText)
|
||||
color: {
|
||||
if (pam.fprint.tries >= SettingsData.maxFprintTries)
|
||||
return Theme.error;
|
||||
if (pam.u2fState !== "")
|
||||
return Theme.tertiary;
|
||||
return passwordField.activeFocus ? Theme.primary : Theme.surfaceVariantText;
|
||||
}
|
||||
opacity: pam.passwd.active ? 0 : 1
|
||||
|
||||
Behavior on opacity {
|
||||
@@ -745,8 +755,7 @@ Item {
|
||||
}
|
||||
}
|
||||
onAccepted: {
|
||||
if (!demoMode && !pam.passwd.active) {
|
||||
console.log("Enter pressed, starting PAM authentication");
|
||||
if (!demoMode && !pam.passwd.active && !pam.u2fPending) {
|
||||
pam.passwd.start();
|
||||
}
|
||||
}
|
||||
@@ -756,6 +765,11 @@ Item {
|
||||
}
|
||||
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
if (pam.u2fPending) {
|
||||
pam.cancelU2fPending();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
clear();
|
||||
}
|
||||
|
||||
@@ -820,6 +834,11 @@ Item {
|
||||
if (root.unlocking) {
|
||||
return "Unlocking...";
|
||||
}
|
||||
if (pam.u2fPending) {
|
||||
if (pam.u2fState === "insert")
|
||||
return "Insert your security key...";
|
||||
return "Touch your security key...";
|
||||
}
|
||||
if (pam.passwd.active) {
|
||||
return "Authenticating...";
|
||||
}
|
||||
@@ -894,7 +913,7 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard"
|
||||
buttonSize: 32
|
||||
visible: !demoMode && !pam.passwd.active && !root.unlocking
|
||||
visible: !demoMode && !pam.passwd.active && !root.unlocking && !pam.u2fPending
|
||||
enabled: visible
|
||||
onClicked: {
|
||||
if (keyboardController.isKeyboardActive) {
|
||||
@@ -995,11 +1014,10 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
iconName: "keyboard_return"
|
||||
buttonSize: 36
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking))
|
||||
visible: (demoMode || (!pam.passwd.active && !root.unlocking && !pam.u2fPending))
|
||||
enabled: !demoMode
|
||||
onClicked: {
|
||||
if (!demoMode) {
|
||||
console.log("Enter button clicked, starting PAM authentication");
|
||||
if (!demoMode && !pam.u2fPending) {
|
||||
pam.passwd.start();
|
||||
}
|
||||
}
|
||||
@@ -1025,6 +1043,12 @@ Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: 20
|
||||
text: {
|
||||
if (pam.u2fState === "insert" && !pam.u2fPending) {
|
||||
return "Insert your security key...";
|
||||
}
|
||||
if (pam.u2fState === "waiting" && !pam.u2fPending) {
|
||||
return "Touch your security key...";
|
||||
}
|
||||
if (root.pamState === "error") {
|
||||
return "Authentication error - try again";
|
||||
}
|
||||
@@ -1036,10 +1060,10 @@ Item {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
color: Theme.error
|
||||
color: (pam.u2fState === "waiting" || pam.u2fState === "insert") ? Theme.outline : Theme.error
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
opacity: root.pamState !== "" ? 1 : 0
|
||||
opacity: (root.pamState !== "" || ((pam.u2fState === "waiting" || pam.u2fState === "insert") && !pam.u2fPending)) ? 1 : 0
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
@@ -1607,6 +1631,14 @@ Item {
|
||||
root.passwordBuffer = "";
|
||||
}
|
||||
}
|
||||
onU2fPendingChanged: {
|
||||
if (u2fPending) {
|
||||
passwordField.text = "";
|
||||
root.passwordBuffer = "";
|
||||
if (keyboardController.isKeyboardActive)
|
||||
keyboardController.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binding {
|
||||
|
||||
@@ -2,8 +2,9 @@ pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
|
||||
Rectangle {
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
required property WlSessionLock lock
|
||||
@@ -14,7 +15,17 @@ Rectangle {
|
||||
signal passwordChanged(string newPassword)
|
||||
signal unlockRequested
|
||||
|
||||
color: "transparent"
|
||||
Keys.onPressed: event => {
|
||||
if (videoScreensaver.active && videoScreensaver.inputEnabled) {
|
||||
videoScreensaver.dismiss();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
LockScreenContent {
|
||||
id: lockContent
|
||||
@@ -23,17 +34,38 @@ Rectangle {
|
||||
demoMode: false
|
||||
passwordBuffer: root.sharedPasswordBuffer
|
||||
screenName: root.screenName
|
||||
enabled: !videoScreensaver.active
|
||||
focus: !videoScreensaver.active
|
||||
opacity: videoScreensaver.active ? 0 : 1
|
||||
onUnlockRequested: root.unlockRequested()
|
||||
onPasswordBufferChanged: {
|
||||
if (root.sharedPasswordBuffer !== passwordBuffer) {
|
||||
root.passwordChanged(passwordBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 200
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
VideoScreensaver {
|
||||
id: videoScreensaver
|
||||
anchors.fill: parent
|
||||
screenName: root.screenName
|
||||
}
|
||||
|
||||
Component.onCompleted: forceActiveFocus()
|
||||
|
||||
onIsLockedChanged: {
|
||||
if (isLocked) {
|
||||
forceActiveFocus();
|
||||
lockContent.resetLockState();
|
||||
if (SettingsData.lockScreenVideoEnabled) {
|
||||
videoScreensaver.start();
|
||||
}
|
||||
return;
|
||||
}
|
||||
lockContent.unlocking = false;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user