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

Compare commits

...

47 Commits

Author SHA1 Message Date
purian23 c49a875ec2 Workflow updates 2025-11-23 14:34:07 -05:00
bbedward 2a002304b9 migrate default font family props to Theme 2025-11-23 13:26:04 -05:00
bbedward d9522818ae greeter: fix custom themes and font family
fixes #776
2025-11-23 13:21:16 -05:00
bbedward 800588e121 modal: remove targetScreen usage
fixes #798
2025-11-23 13:03:32 -05:00
bbedward 991c31ebdb i18n: update translations 2025-11-23 12:49:29 -05:00
bbedward 48f77e1691 processlist: convert to floating window 2025-11-23 12:16:03 -05:00
bbedward 42de6fd074 modals: apply same pattern of multi-window
- fixes excessive repaints
fixes #716
2025-11-23 12:07:45 -05:00
bbedward 62845b470c popout: fix excessive repaints
- Size content window to content size, buffer for shadow
- Add second window for click outside behavior
- User overriding the layer disables the click outside behavior

part of #716
2025-11-23 10:49:59 -05:00
bbedward fd20986cf8 settings: make responsive, view-stack style 2025-11-23 10:01:26 -05:00
purian23 61369cde9e Update gitignore 2025-11-23 03:16:00 -05:00
purian23 644384ce8b feat: Mult-Distro support - Debian, Ubuntu, OpenSuse 2025-11-23 02:39:24 -05:00
Lucas 97c11a2482 bar: fix auto-hide not hiding after popout closes (#796) 2025-11-23 01:38:58 -05:00
bbedward 1e7e1c2d78 settings: clamp max content width 2025-11-23 01:38:16 -05:00
bbedward 1c7201fb04 settings: make settings and file browser normal windows
- add default floating rules for dankinstall
2025-11-23 01:23:06 -05:00
bbedward 61ec0c697a gamma: dont transition before destroying controls 2025-11-23 00:48:23 -05:00
bbedward 4b5fce1bfc dankbar: hide settings when bar is disabled 2025-11-23 00:45:12 -05:00
Lucas 6cc6e7c8e9 Media volume scroll on DankBar widget and media volume OSD (#795)
* osd: add media volume OSD

* media: scroll on widget changes media volume

* dash: use media volume in media tab
2025-11-23 00:42:06 -05:00
bbedward 89298fce30 bar: don't apply opacity to sth color
- legacy thing that already has it
2025-11-22 16:15:07 -05:00
bbedward a3a27e07fa dankbar: support multiple bars and per-display bars
- Migrate settings to v2
  - Up to 4 bars
  - Per-bar settings instead of global
2025-11-22 15:28:06 -05:00
bbedward 4f32376f22 gamma: remove display sync on destruction 2025-11-22 15:26:05 -05:00
bbedward 58bf189941 launcher: set default launch prefix, if launching from systemd
- prevents apps dying when stopping the systemd unit
2025-11-22 00:23:06 -05:00
bbedward bcfa508da5 weather: fix fahrenheit conversion 2025-11-21 22:07:44 -05:00
mbpowers c0ae3ef58b fix: bar and dock flickering autohide (#784) 2025-11-21 21:49:31 -05:00
mbpowers 1e70d7b4c3 fix: remove useFahrenheit refresh, fetch Celcius convert locally (#785)
* fix: remove useFahrenheit refresh, fetch Celcius convert locally

* fix: typo in change unit button
2025-11-21 21:41:12 -05:00
bbedward f8dc6ad2bc update CONTRIBUTING 2025-11-21 17:30:54 -05:00
bbedward e22482988f weather: fix display when 0 temp
fixes #782
2025-11-21 17:06:57 -05:00
bbedward 4eb896629d net: fix VPN prompting for password 2025-11-21 12:59:12 -05:00
bbedward b310e66275 themes: shift catpuccin palete 2025-11-21 09:30:58 -05:00
bbedward b39da1bea7 cc: bit of extra height for some detail items 2025-11-21 09:15:59 -05:00
Pi Home Server fa575d0574 Fix background color of the privacy widget (#779) 2025-11-21 09:05:56 -05:00
bbedward dfe2f3771b theme: add colorful bar widget option 2025-11-21 00:07:23 -05:00
bbedward 46caeb0445 sounds: only play audio changed when trigger by us 2025-11-20 23:38:06 -05:00
bbedward 59cc9c7006 niri: ensure overview spotlight is hidden when main window is brought up 2025-11-20 21:23:56 -05:00
bbedward 12e91534eb niri: empty input region & disable spotlight content when not open 2025-11-20 16:44:46 -05:00
bbedward d9da88ceb5 niri: embed spotlight to same window as overview layer 2025-11-20 16:28:26 -05:00
bbedward 2dbfec0307 niri: close spotlight when closing overview 2025-11-20 13:56:35 -05:00
Lucas 09cf8c9641 niri: add spotlight on overview typing functionality (#774) 2025-11-20 13:48:30 -05:00
Pi Home Server f1bed4d6a3 Feature/privacy widget fix (#772)
* Fix active camera icon

* Fix active camera icon
2025-11-20 12:30:23 -05:00
bbedward 2ed6c33c83 missing import 2025-11-19 19:14:47 -05:00
bbedward 7ad532ed17 dankinstall: add ultramarine 2025-11-19 18:53:41 -05:00
bbedward 92fe8c5b14 hyprland: restore focus grab to tray menus 2025-11-19 17:24:14 -05:00
bbedward 8e95572589 modals: move HyprFocusGrab out of common Modal 2025-11-19 17:16:51 -05:00
bbedward 62da862a66 modal: round textureSize pixels 2025-11-19 14:36:08 -05:00
bbedward 993e34f548 dankinstall: weakdeps for niri/system 2025-11-19 09:35:22 -05:00
github-actions[bot] e39465aece chore: bump version to v0.6.2 2025-11-19 13:54:50 +00:00
bbedward 8fd616b680 osd: suppression fix from cc 2025-11-19 08:52:37 -05:00
bbedward cc054b27de filebrowser: fix auto closing from ddash 2025-11-19 08:33:07 -05:00
204 changed files with 15850 additions and 8729 deletions
+62
View File
@@ -386,6 +386,68 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
trigger-obs-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Update OBS packages
run: |
VERSION="${{ github.ref_name }}"
cd distro
bash scripts/obs-upload.sh dms "Update to $VERSION"
trigger-ppa-update:
runs-on: ubuntu-latest
needs: release
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Upload to PPA
run: |
VERSION="${{ github.ref_name }}"
cd distro/ubuntu/ppa
bash create-and-upload.sh ../dms dms questing
copr-build: copr-build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: release needs: release
@@ -1,4 +1,4 @@
name: DMS Copr Stable Release (Manual) name: DMS Copr Stable Release
on: on:
workflow_dispatch: workflow_dispatch:
+114
View File
@@ -0,0 +1,114 @@
name: Update OBS Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to update (dms, dms-git, or all)'
required: false
default: 'all'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 to increment spec Release)'
required: false
default: ''
push:
tags:
- 'v*'
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
update-obs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine packages to update
id: packages
run: |
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/ ]]; then
echo "packages=dms" >> $GITHUB_OUTPUT
VERSION="${GITHUB_REF#refs/tags/}"
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Triggered by tag: $VERSION"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Triggered by schedule: updating git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=all" >> $GITHUB_OUTPUT
fi
- name: Update version in packaging files
if: steps.packages.outputs.version != ''
run: |
VERSION="${{ steps.packages.outputs.version }}"
VERSION_NO_V="${VERSION#v}"
echo "Updating packaging to version $VERSION_NO_V"
# Update openSUSE spec files
sed -i "s/^Version:.*/Version: $VERSION_NO_V/" distro/opensuse/*.spec
# Update Debian _service files
for service in distro/debian/*/_service; do
if [[ -f "$service" ]]; then
sed -i "s|<param name=\"revision\">v[0-9.]*</param>|<param name=\"revision\">$VERSION</param>|" "$service"
fi
done
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add distro/
git commit -m "chore: update packaging to $VERSION" || echo "No changes to commit"
- name: Install OSC
run: |
sudo apt-get update
sudo apt-get install -y osc
mkdir -p ~/.config/osc
cat > ~/.config/osc/oscrc << EOF
[general]
apiurl = https://api.opensuse.org
[https://api.opensuse.org]
user = ${{ secrets.OBS_USERNAME }}
pass = ${{ secrets.OBS_PASSWORD }}
EOF
chmod 600 ~/.config/osc/oscrc
- name: Upload to OBS
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
MESSAGE="Automated update from GitHub Actions"
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
MESSAGE="Update to ${{ steps.packages.outputs.version }}"
fi
cd distro
if [[ "$PACKAGES" == "all" ]]; then
bash scripts/obs-upload.sh dms "$MESSAGE"
bash scripts/obs-upload.sh dms-git "Automated git update"
else
bash scripts/obs-upload.sh "$PACKAGES" "$MESSAGE"
fi
- name: Summary
run: |
echo "### OBS Package Update Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "- **Project**: https://build.opensuse.org/project/show/home:AvengeMedia" >> $GITHUB_STEP_SUMMARY
+110
View File
@@ -0,0 +1,110 @@
name: Update PPA Packages
on:
workflow_dispatch:
inputs:
package:
description: 'Package to upload (dms, dms-git, or all)'
required: false
default: 'dms-git'
rebuild_release:
description: 'Release number for rebuilds (e.g., 2, 3, 4 for ppa2, ppa3, ppa4)'
required: false
default: ''
schedule:
- cron: '0 */3 * * *' # Every 3 hours for dms-git builds
jobs:
upload-ppa:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install build dependencies
run: |
sudo apt-get update
sudo apt-get install -y \
debhelper \
devscripts \
dput \
lftp \
build-essential \
fakeroot \
dpkg-dev
- name: Configure GPG
env:
GPG_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
run: |
echo "$GPG_KEY" | gpg --import
GPG_KEY_ID=$(gpg --list-secret-keys --keyid-format LONG | grep sec | awk '{print $2}' | cut -d'/' -f2)
echo "DEBSIGN_KEYID=$GPG_KEY_ID" >> $GITHUB_ENV
- name: Determine packages to upload
id: packages
run: |
if [[ "${{ github.event_name }}" == "schedule" ]]; then
echo "packages=dms-git" >> $GITHUB_OUTPUT
echo "Triggered by schedule: uploading git package"
elif [[ -n "${{ github.event.inputs.package }}" ]]; then
echo "packages=${{ github.event.inputs.package }}" >> $GITHUB_OUTPUT
echo "Manual trigger: ${{ github.event.inputs.package }}"
else
echo "packages=dms-git" >> $GITHUB_OUTPUT
fi
- name: Upload to PPA
env:
REBUILD_RELEASE: ${{ github.event.inputs.rebuild_release }}
run: |
PACKAGES="${{ steps.packages.outputs.packages }}"
cd distro/ubuntu/ppa
if [[ "$PACKAGES" == "all" ]]; then
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms to PPA..."
if [ -n "$REBUILD_RELEASE" ]; then
echo "🔄 Using rebuild release number: ppa$REBUILD_RELEASE"
fi
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash create-and-upload.sh "../dms" dms questing
echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading dms-git to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash create-and-upload.sh "../dms-git" dms-git questing
else
PPA_NAME="$PACKAGES"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Uploading $PACKAGES to PPA..."
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
bash create-and-upload.sh "../$PACKAGES" "$PPA_NAME" questing
fi
- name: Summary
run: |
echo "### PPA Package Upload Complete" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Packages**: ${{ steps.packages.outputs.packages }}" >> $GITHUB_STEP_SUMMARY
PACKAGES="${{ steps.packages.outputs.packages }}"
if [[ "$PACKAGES" == "all" ]]; then
echo "- **PPA dms**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
echo "- **PPA dms-git**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages" >> $GITHUB_STEP_SUMMARY
elif [[ "$PACKAGES" == "dms-git" ]]; then
echo "- **PPA**: https://launchpad.net/~avengemedia/+archive/ubuntu/dms-git/+packages" >> $GITHUB_STEP_SUMMARY
fi
if [[ -n "${{ steps.packages.outputs.version }}" ]]; then
echo "- **Version**: ${{ steps.packages.outputs.version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "Builds will appear once Launchpad processes the uploads." >> $GITHUB_STEP_SUMMARY
+6
View File
@@ -136,3 +136,9 @@ go.work.sum
# .vscode/ # .vscode/
bin/ bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/
+29 -15
View File
@@ -2,28 +2,42 @@
Contributions are welcome and encouraged. Contributions are welcome and encouraged.
## Formatting To contribute fork this repository, make your changes, and open a pull request.
The preferred tool for formatting files is [qmlfmt](https://github.com/jesperhh/qmlfmt) (also available on aur as qmlfmt-git). It actually kinda sucks, but `qmlformat` doesn't work with null safe operators and ternarys and pragma statements and a bunch of other things that are supported. ## VSCode Setup
We need some consistent style, so this at least gives the same formatter that Qt Creator uses. This is a monorepo, the easiest thing to do is to open an editor in either `quickshell`, `core`, or both depending on which part of the project you are working on.
You can configure it to format on save in vscode by configuring the "custom local formatters" extension then adding this to settings json. ### QML (`quickshell` directory)
1. Install the [QML Extension](https://doc.qt.io/vscodeext/)
2. Configure `ctrl+shift+p` -> user preferences (json) with qmlls path
```json ```json
"customLocalFormatters.formatters": [ {
{ "qt-qml.doNotAskForQmllsDownload": true,
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"", "qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
"languages": ["qml"] }
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
``` ```
Sometimes it just breaks code though. Like turning `"_\""` into `"_""`, so you may not want to do formatOnSave. 3. Create empty `.qmlls.ini` file in `quickshell/` directory
```bash
cd quickshell
touch .qmlls.ini
```
4. Restart dms to generate the `.qmlls.ini` file
5. Make your changes, test, and open a pull request.
### GO (`core` directory)
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
2. Ensure code is formatted with `make fmt`
3. Add appropriate test coverage and ensure tests pass with `make test`
4. Run `go mod tidy`
5. Open pull request
## Pull request ## Pull request
+13
View File
@@ -57,6 +57,11 @@ func getRuntimeDir() string {
return os.TempDir() return os.TempDir()
} }
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string { func getPIDFilePath() string {
return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid())) return filepath.Join(getRuntimeDir(), fmt.Sprintf("danklinux-%d.pid", os.Getpid()))
} }
@@ -165,6 +170,10 @@ func runShellInteractive(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
} }
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {
@@ -387,6 +396,10 @@ func runShellDaemon(session bool) {
cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules) cmd.Env = append(cmd.Env, "QT_LOGGING_RULES="+qtRules)
} }
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir() homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" { if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) { if !strings.HasPrefix(configPath, homeDir) {
@@ -125,6 +125,8 @@ windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$ windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$ windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0 windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = noanim, ^(quickshell)$ layerrule = noanim, ^(quickshell)$
+5
View File
@@ -218,6 +218,11 @@ window-rule {
geometry-corner-radius 12 geometry-corner-radius 12
clip-to-geometry true clip-to-geometry true
} }
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
open-floating true
}
binds { binds {
// === System & Overview === // === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; } Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }
+11 -1
View File
@@ -19,10 +19,12 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution { Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan) return NewFedoraDistribution(config, logChan)
}) })
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
} }
type FedoraDistribution struct { type FedoraDistribution struct {
@@ -506,6 +508,14 @@ func (f *FedoraDistribution) installDNFPackages(ctx context.Context, packages []
f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", "))) f.log(fmt.Sprintf("Installing DNF packages: %s", strings.Join(packages, ", ")))
args := []string{"dnf", "install", "-y"} args := []string{"dnf", "install", "-y"}
for _, pkg := range packages {
if pkg == "niri" || pkg == "niri-git" {
args = append(args, "--setopt=install_weak_deps=False")
break
}
}
args = append(args, packages...) args = append(args, packages...)
progressChan <- InstallProgressMsg{ progressChan <- InstallProgressMsg{
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"time" "time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs" "github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -125,8 +126,9 @@ func (a *SecretAgent) GetSecrets(
connType, displayName, vpnSvc := readConnTypeAndName(conn) connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn) ssid := readSSID(conn)
fields := fieldsNeeded(settingName, hints) fields := fieldsNeeded(settingName, hints)
vpnPasswordFlags := readVPNPasswordFlags(conn, settingName)
log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d", connType, displayName, vpnSvc, fields, flags) log.Infof("[SecretAgent] connType=%s, name=%s, vpnSvc=%s, fields=%v, flags=%d, vpnPasswordFlags=%d", connType, displayName, vpnSvc, fields, flags, vpnPasswordFlags)
if a.backend != nil { if a.backend != nil {
a.backend.stateMutex.RLock() a.backend.stateMutex.RLock()
@@ -163,57 +165,70 @@ func (a *SecretAgent) GetSecrets(
} }
if len(fields) == 0 { if len(fields) == 0 {
// For VPN connections with no hints, we can't provide a proper UI.
// Defer to other agents (like nm-applet or VPN-specific auth dialogs)
// that can handle the VPN type properly (e.g., OpenConnect with SAML, etc.)
if settingName == "vpn" { if settingName == "vpn" {
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc) if a.backend != nil {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) a.backend.stateMutex.RLock()
} isConnectingVPN := a.backend.state.IsConnectingVPN
a.backend.stateMutex.RUnlock()
const ( if !isConnectingVPN {
NM_SETTING_SECRET_FLAG_NONE = 0 log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1 return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2 }
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
)
var passwordFlags uint32 = 0xFFFF log.Infof("[SecretAgent] VPN with empty hints but we're connecting - prompting for password")
switch settingName { fields = []string{"password"}
case "802-11-wireless-security": } else {
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok { log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok { return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
passwordFlags = pwdFlags
}
}
}
case "802-1x":
if dot1xSettings, ok := conn["802-1x"]; ok {
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
passwordFlags = pwdFlags
}
}
} }
} }
if passwordFlags == 0xFFFF { if len(fields) == 0 {
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error") const (
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) NM_SETTING_SECRET_FLAG_NONE = 0
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 { NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags) NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
out := nmSettingMap{} NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
out[settingName] = nmVariantMap{} )
return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 { var passwordFlags uint32 = 0xFFFF
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags) switch settingName {
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil) case "802-11-wireless-security":
} else { if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags) if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
out := nmSettingMap{} if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
out[settingName] = nmVariantMap{} passwordFlags = pwdFlags
return out, nil }
}
}
case "802-1x":
if dot1xSettings, ok := conn["802-1x"]; ok {
if flagsVariant, ok := dot1xSettings["password-flags"]; ok {
if pwdFlags, ok := flagsVariant.Value().(uint32); ok {
passwordFlags = pwdFlags
}
}
}
}
if passwordFlags == 0xFFFF {
log.Warnf("[SecretAgent] Could not determine password-flags for empty hints - returning NoSecrets error")
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
} else if passwordFlags&NM_SETTING_SECRET_FLAG_NOT_REQUIRED != 0 {
log.Infof("[SecretAgent] Secrets not required (flags=%d)", passwordFlags)
out := nmSettingMap{}
out[settingName] = nmVariantMap{}
return out, nil
} else if passwordFlags&NM_SETTING_SECRET_FLAG_AGENT_OWNED != 0 {
log.Warnf("[SecretAgent] Secrets are agent-owned but we don't store secrets (flags=%d) - returning NoSecrets error", passwordFlags)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
} else {
log.Infof("[SecretAgent] No secrets needed, using system stored secrets (flags=%d)", passwordFlags)
out := nmSettingMap{}
out[settingName] = nmVariantMap{}
return out, nil
}
} }
} }
@@ -343,13 +358,11 @@ func (a *SecretAgent) GetSecrets(
// Update settings based on type // Update settings based on type
switch settingName { switch settingName {
case "vpn": case "vpn":
// Set password-flags=0 and add secrets to vpn section
vpn, ok := existingSettings["vpn"] vpn, ok := existingSettings["vpn"]
if !ok { if !ok {
vpn = make(map[string]dbus.Variant) vpn = make(map[string]dbus.Variant)
} }
// Get existing data map (vpn.data is string->string)
var data map[string]string var data map[string]string
if dataVariant, ok := vpn["data"]; ok { if dataVariant, ok := vpn["data"]; ok {
if dm, ok := dataVariant.Value().(map[string]string); ok { if dm, ok := dataVariant.Value().(map[string]string); ok {
@@ -364,11 +377,9 @@ func (a *SecretAgent) GetSecrets(
data = make(map[string]string) data = make(map[string]string)
} }
// Update password-flags to 0 (system-stored)
data["password-flags"] = "0" data["password-flags"] = "0"
vpn["data"] = dbus.MakeVariant(data) vpn["data"] = dbus.MakeVariant(data)
// Add secrets (vpn.secrets is string->string)
secs := make(map[string]string) secs := make(map[string]string)
for k, v := range reply.Secrets { for k, v := range reply.Secrets {
secs[k] = v secs[k] = v
@@ -379,14 +390,12 @@ func (a *SecretAgent) GetSecrets(
log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs)) log.Infof("[SecretAgent] Updated VPN settings: password-flags=0, secrets with %d fields", len(secs))
case "802-11-wireless-security": case "802-11-wireless-security":
// Set psk-flags=0 for WiFi
wifiSec, ok := existingSettings["802-11-wireless-security"] wifiSec, ok := existingSettings["802-11-wireless-security"]
if !ok { if !ok {
wifiSec = make(map[string]dbus.Variant) wifiSec = make(map[string]dbus.Variant)
} }
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0)) wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
// Add PSK secret
if psk, ok := reply.Secrets["psk"]; ok { if psk, ok := reply.Secrets["psk"]; ok {
wifiSec["psk"] = dbus.MakeVariant(psk) wifiSec["psk"] = dbus.MakeVariant(psk)
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0") log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
@@ -394,14 +403,12 @@ func (a *SecretAgent) GetSecrets(
settings["802-11-wireless-security"] = wifiSec settings["802-11-wireless-security"] = wifiSec
case "802-1x": case "802-1x":
// Set password-flags=0 for 802.1x
dot1x, ok := existingSettings["802-1x"] dot1x, ok := existingSettings["802-1x"]
if !ok { if !ok {
dot1x = make(map[string]dbus.Variant) dot1x = make(map[string]dbus.Variant)
} }
dot1x["password-flags"] = dbus.MakeVariant(uint32(0)) dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
// Add password secret
if password, ok := reply.Secrets["password"]; ok { if password, ok := reply.Secrets["password"]; ok {
dot1x["password"] = dbus.MakeVariant(password) dot1x["password"] = dbus.MakeVariant(password)
log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0") log.Infof("[SecretAgent] Updated 802.1x settings: password-flags=0")
@@ -507,6 +514,39 @@ func fieldsNeeded(setting string, hints []string) []string {
} }
} }
func readVPNPasswordFlags(conn map[string]nmVariantMap, settingName string) uint32 {
if settingName != "vpn" {
return 0xFFFF
}
vpnSettings, ok := conn["vpn"]
if !ok {
return 0xFFFF
}
dataVariant, ok := vpnSettings["data"]
if !ok {
return 0xFFFF
}
dataMap, ok := dataVariant.Value().(map[string]string)
if !ok {
return 0xFFFF
}
flagsStr, ok := dataMap["password-flags"]
if !ok {
return 0xFFFF
}
flags64, err := strconv.ParseUint(flagsStr, 10, 32)
if err != nil {
return 0xFFFF
}
return uint32(flags64)
}
func reasonFromFlags(flags uint32) string { func reasonFromFlags(flags uint32) string {
const ( const (
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0 NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0
@@ -235,7 +235,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
} }
nm := b.nmConn.(gonetworkmanager.NetworkManager) nm := b.nmConn.(gonetworkmanager.NetworkManager)
activeConn, err := nm.ActivateConnection(targetConn, nil, nil) _, err = nm.ActivateConnection(targetConn, nil, nil)
if err != nil { if err != nil {
b.stateMutex.Lock() b.stateMutex.Lock()
b.state.IsConnectingVPN = false b.state.IsConnectingVPN = false
@@ -249,20 +249,6 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
return fmt.Errorf("failed to activate VPN: %w", err) return fmt.Errorf("failed to activate VPN: %w", err)
} }
if activeConn != nil {
state, _ := activeConn.GetPropertyState()
if state == 2 {
b.stateMutex.Lock()
b.state.IsConnectingVPN = false
b.state.ConnectingVPNUUID = ""
b.stateMutex.Unlock()
b.ListActiveVPN()
if b.onStateChange != nil {
b.onStateChange()
}
}
}
return nil return nil
} }
+25 -73
View File
@@ -607,41 +607,6 @@ func (m *Manager) transitionWorker() {
if finalTarget == targetTemp { if finalTarget == targetTemp {
log.Debugf("Transition complete: now at %dK", targetTemp) log.Debugf("Transition complete: now at %dK", targetTemp)
m.configMutex.RLock()
enabled := m.config.Enabled
identityTemp := m.config.HighTemp
m.configMutex.RUnlock()
if !enabled && targetTemp == identityTemp && m.controlsInitialized {
m.post(func() {
log.Info("Destroying gamma controls after transition to identity")
m.outputs.Range(func(id uint32, out *outputState) bool {
if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id)
}
return true
})
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false
m.transitionMutex.Lock()
m.currentTemp = identityTemp
m.targetTemp = identityTemp
m.transitionMutex.Unlock()
if _, err := m.display.Sync(); err != nil {
log.Warnf("Failed to sync Wayland display after destroying controls: %v", err)
}
log.Info("All gamma controls destroyed")
})
}
} }
} }
} }
@@ -1262,46 +1227,33 @@ func (m *Manager) SetEnabled(enabled bool) {
} }
} else { } else {
if m.controlsInitialized { if m.controlsInitialized {
m.configMutex.RLock() m.post(func() {
identityTemp := m.config.HighTemp log.Info("Disabling gamma, destroying controls immediately")
m.configMutex.RUnlock() m.outputs.Range(func(id uint32, out *outputState) bool {
if out.gammaControl != nil {
m.transitionMutex.RLock() control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
currentTemp := m.currentTemp control.Destroy()
m.transitionMutex.RUnlock() log.Debugf("Destroyed gamma control for output %d", id)
if currentTemp == identityTemp {
m.post(func() {
log.Infof("Already at %dK, destroying gamma controls immediately", identityTemp)
m.outputs.Range(func(id uint32, out *outputState) bool {
if out.gammaControl != nil {
control := out.gammaControl.(*wlr_gamma_control.ZwlrGammaControlV1)
control.Destroy()
log.Debugf("Destroyed gamma control for output %d", id)
}
return true
})
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
return true
})
m.controlsInitialized = false
m.transitionMutex.Lock()
m.currentTemp = identityTemp
m.targetTemp = identityTemp
m.transitionMutex.Unlock()
if _, err := m.display.Sync(); err != nil {
log.Warnf("Failed to sync Wayland display after destroying controls: %v", err)
} }
return true
log.Info("All gamma controls destroyed")
}) })
} else { m.outputs.Range(func(key uint32, value *outputState) bool {
log.Infof("Disabling: transitioning to %dK before destroying controls", identityTemp) m.outputs.Delete(key)
m.startTransition(identityTemp) return true
} })
m.controlsInitialized = false
m.configMutex.RLock()
identityTemp := m.config.HighTemp
m.configMutex.RUnlock()
m.transitionMutex.Lock()
m.currentTemp = identityTemp
m.targetTemp = identityTemp
m.transitionMutex.Unlock()
log.Info("All gamma controls destroyed")
})
} }
} }
} }
+24
View File
@@ -0,0 +1,24 @@
<services>
<!-- Pull full git repository for master branch -->
<service name="tar_scm" mode="disabled">
<param name="scm">git</param>
<param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param>
<param name="revision">master</param>
<param name="filename">dms-git-source</param>
</service>
<service name="recompress" mode="disabled">
<param name="file">*.tar</param>
<param name="compression">gz</param>
</service>
<!-- Download pre-built binaries (fallback for Debian 13 with Go 1.22) -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz</param>
</service>
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param>
</service>
</services>
+8
View File
@@ -0,0 +1,8 @@
dms-git (0.6.2+git) nightly; urgency=medium
* Build dms binary from source for true git version strings
* Match Fedora COPR git build behavior
* Now shows proper git version (e.g., v0.6.2-11-g12e91534)
* Add golang-go and make as build dependencies
-- Avenge Media <AvengeMedia.US@gmail.com> Fri, 22 Nov 2025 00:00:00 -0500
+50
View File
@@ -0,0 +1,50 @@
Source: dms-git
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-git
Architecture: amd64 arm64
Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
qml6-module-qtcore,
qml6-module-qtmultimedia,
qml6-module-qtqml,
qml6-module-qtquick,
qml6-module-qtquick-controls,
qml6-module-qtquick-dialogs,
qml6-module-qtquick-effects,
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
Provides: dms
Conflicts: dms
Replaces: dms
Description: DankMaterialShell - Modern Wayland Desktop Shell (git nightly)
DMS (DankMaterialShell) is a feature-rich desktop shell built on
Quickshell, providing a modern and customizable user interface for
Wayland compositors like niri, hyprland, and sway.
.
This is the nightly/git version built from the latest master branch.
.
Features include:
- Material Design inspired UI
- Customizable themes and appearance
- Built-in application launcher
- System tray and notifications
- Network and Bluetooth management
- Audio controls
- Systemd integration
+27
View File
@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2025 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dms-git_0.6.0+git2061.5ddea836ppa1_source.buildinfo x11 optional
+54
View File
@@ -0,0 +1,54 @@
#!/usr/bin/make -f
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
%:
dh $@
override_dh_auto_build:
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
if [ -f dms-distropkg-amd64.gz ]; then \
gunzip -c dms-distropkg-amd64.gz > dms; \
elif [ -f ../SOURCES/dms-distropkg-amd64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-amd64.gz > dms; \
else \
echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1; \
fi \
elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
if [ -f dms-distropkg-arm64.gz ]; then \
gunzip -c dms-distropkg-arm64.gz > dms; \
elif [ -f ../SOURCES/dms-distropkg-arm64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-arm64.gz > dms; \
else \
echo "ERROR: dms-distropkg-arm64.gz not found!" && exit 1; \
fi \
else \
echo "Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \
fi
chmod +x dms
override_dh_auto_install:
install -Dm755 dms debian/dms-git/usr/bin/dms
mkdir -p debian/dms-git/usr/share/quickshell/dms debian/dms-git/usr/lib/systemd/user
if [ -d quickshell ]; then \
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
elif [ -d dms-git-source/quickshell ]; then \
cp -r dms-git-source/quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
install -Dm644 dms-git-source/quickshell/assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
else \
echo "ERROR: quickshell directory not found (checked root and dms-git-source/)!" && \
echo "Contents of current directory:" && ls -la && \
exit 1; \
fi
rm -rf debian/dms-git/usr/share/quickshell/dms/core \
debian/dms-git/usr/share/quickshell/dms/distro
override_dh_auto_clean:
rm -f dms
[ ! -d dms-git-source ] || rm -rf dms-git-source
dh_auto_clean
@@ -0,0 +1 @@
3.0 (native)
@@ -0,0 +1 @@
dms-distropkg-amd64.gz
@@ -0,0 +1,4 @@
# Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-git-repo
+21
View File
@@ -0,0 +1,21 @@
<services>
<!-- Download source tarball from GitHub releases -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v0.6.2.tar.gz</param>
<param name="filename">dms-source.tar.gz</param>
</service>
<!-- Download amd64 binary -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-amd64.gz</param>
</service>
<!-- Download arm64 binary -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v0.6.2/dms-distropkg-arm64.gz</param>
</service>
</services>
+7
View File
@@ -0,0 +1,7 @@
dms (0.6.2) stable; urgency=medium
* Update to v0.6.2 release
* Fix binary download paths for OBS builds
* Native format: removed revisions
-- Avenge Media <AvengeMedia.US@gmail.com> Tue, 19 Nov 2025 10:00:00 -0500
+47
View File
@@ -0,0 +1,47 @@
Source: dms
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms
Architecture: amd64
Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
qml6-module-qtcore,
qml6-module-qtmultimedia,
qml6-module-qtqml,
qml6-module-qtquick,
qml6-module-qtquick-controls,
qml6-module-qtquick-dialogs,
qml6-module-qtquick-effects,
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
Conflicts: dms-git
Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell
DMS (DankMaterialShell) is a feature-rich desktop shell built on
Quickshell, providing a modern and customizable user interface for
Wayland compositors like niri, hyprland, and sway.
.
Features include:
- Material Design inspired UI
- Customizable themes and appearance
- Built-in application launcher
- System tray and notifications
- Network and Bluetooth management
- Audio controls
- Systemd integration
+27
View File
@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2025 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dms_0.6.0ppa2_source.buildinfo x11 optional
+64
View File
@@ -0,0 +1,64 @@
#!/usr/bin/make -f
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
%:
dh $@
override_dh_auto_build:
if [ "$(DEB_HOST_ARCH)" = "amd64" ]; then \
if [ -f dms-distropkg-amd64.gz ]; then \
gunzip -c dms-distropkg-amd64.gz > dms; \
elif [ -f ../SOURCES/dms-distropkg-amd64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-amd64.gz > dms; \
elif [ -f ../../SOURCES/dms-distropkg-amd64.gz ]; then \
gunzip -c ../../SOURCES/dms-distropkg-amd64.gz > dms; \
else \
echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1; \
fi \
elif [ "$(DEB_HOST_ARCH)" = "arm64" ]; then \
if [ -f dms-distropkg-arm64.gz ]; then \
gunzip -c dms-distropkg-arm64.gz > dms; \
elif [ -f ../SOURCES/dms-distropkg-arm64.gz ]; then \
gunzip -c ../SOURCES/dms-distropkg-arm64.gz > dms; \
elif [ -f ../../SOURCES/dms-distropkg-arm64.gz ]; then \
gunzip -c ../../SOURCES/dms-distropkg-arm64.gz > dms; \
else \
echo "ERROR: dms-distropkg-arm64.gz not found!" && exit 1; \
fi \
else \
echo "Unsupported architecture: $(DEB_HOST_ARCH)" && exit 1; \
fi
chmod +x dms
if [ ! -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
if [ -f ../SOURCES/dms-source.tar.gz ]; then \
tar -xzf ../SOURCES/dms-source.tar.gz; \
elif [ -f dms-source.tar.gz ]; then \
tar -xzf dms-source.tar.gz; \
fi; \
fi
override_dh_auto_install:
install -Dm755 dms debian/dms/usr/bin/dms
mkdir -p debian/dms/usr/share/quickshell/dms debian/dms/usr/lib/systemd/user
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
else \
echo "ERROR: DankMaterialShell-$(UPSTREAM_VERSION) directory not found!" && \
echo "Contents of current directory:" && ls -la && \
exit 1; \
fi
rm -rf debian/dms/usr/share/quickshell/dms/core \
debian/dms/usr/share/quickshell/dms/distro
override_dh_auto_clean:
rm -f dms
rm -rf DankMaterialShell-$(UPSTREAM_VERSION)
dh_auto_clean
+1
View File
@@ -0,0 +1 @@
3.0 (native)
@@ -0,0 +1,2 @@
dms-distropkg-amd64.gz
dms-source.tar.gz
+4
View File
@@ -0,0 +1,4 @@
# Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad
tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-source.tar.gz
+24
View File
@@ -0,0 +1,24 @@
<services>
<!-- Pull full git repository for master branch (QML code) -->
<service name="tar_scm">
<param name="scm">git</param>
<param name="url">https://github.com/AvengeMedia/DankMaterialShell.git</param>
<param name="revision">master</param>
<param name="filename">dms-git-source</param>
</service>
<service name="recompress">
<param name="file">*.tar</param>
<param name="compression">gz</param>
</service>
<!-- Download pre-built binaries -->
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz</param>
</service>
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-arm64.gz</param>
</service>
</services>
+107
View File
@@ -0,0 +1,107 @@
%global debug_package %{nil}
Name: dms-git
Version: 0.6.2+git
Release: 5%{?dist}
Epoch: 1
Summary: DankMaterialShell - Material 3 inspired shell (git nightly)
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-git-source.tar.gz
Source1: dms-distropkg-amd64.gz
Source2: dms-distropkg-arm64.gz
BuildRequires: gzip
BuildRequires: systemd-rpm-macros
Requires: (quickshell-git or quickshell)
Requires: accountsservice
Requires: dgop
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: quickshell-git
Recommends: wl-clipboard
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Suggests: qt6ct
Provides: dms
Conflicts: dms
Obsoletes: dms
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for niri, Hyprland, Sway, and other wlroots compositors.
This git version tracks the master branch and includes the latest features
and fixes. Includes pre-built dms CLI binary and QML shell files.
%prep
%setup -q -n dms-git-source
%ifarch x86_64
gunzip -c %{SOURCE1} > dms
%endif
%ifarch aarch64
gunzip -c %{SOURCE2} > dms
%endif
chmod +x dms
%build
%install
install -Dm755 dms %{buildroot}%{_bindir}/dms
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
./dms completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 quickshell/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r quickshell/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
fi
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
%doc CONTRIBUTING.md
%doc quickshell/README.md
%{_bindir}/dms
%dir %{_datadir}/fish
%dir %{_datadir}/fish/vendor_completions.d
%{_datadir}/fish/vendor_completions.d/dms.fish
%dir %{_datadir}/zsh
%dir %{_datadir}/zsh/site-functions
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/bash-completion/completions/dms
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%changelog
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2+git-5
- Git nightly build from master branch
- Multi-arch support (x86_64, aarch64)
+107
View File
@@ -0,0 +1,107 @@
# Spec for DMS for OpenSUSE/OBS
%global debug_package %{nil}
Name: dms
Version: 0.6.2
Release: 1%{?dist}
Summary: DankMaterialShell - Material 3 inspired shell for Wayland compositors
License: MIT
URL: https://github.com/AvengeMedia/DankMaterialShell
Source0: dms-source.tar.gz
Source1: dms-distropkg-amd64.gz
Source2: dms-distropkg-arm64.gz
BuildRequires: gzip
BuildRequires: systemd-rpm-macros
# Core requirements
Requires: (quickshell-git or quickshell)
Requires: accountsservice
Requires: dgop
# Core utilities (Highly recommended for DMS functionality)
Recommends: cava
Recommends: cliphist
Recommends: danksearch
Recommends: matugen
Recommends: NetworkManager
Recommends: qt6-qtmultimedia
Recommends: wl-clipboard
Suggests: qt6ct
%description
DankMaterialShell (DMS) is a modern Wayland desktop shell built with Quickshell
and optimized for niri, Hyprland, Sway, and other wlroots compositors. Features
notifications, app launcher, wallpaper customization, and plugin system.
Includes auto-theming for GTK/Qt apps with matugen, 20+ customizable widgets,
process monitoring, notification center, clipboard history, dock, control center,
lock screen, and comprehensive plugin system.
%prep
%setup -q -n DankMaterialShell-%{version}
%ifarch x86_64
gunzip -c %{SOURCE1} > dms
%endif
%ifarch aarch64
gunzip -c %{SOURCE2} > dms
%endif
chmod +x dms
%build
%install
install -Dm755 dms %{buildroot}%{_bindir}/dms
install -d %{buildroot}%{_datadir}/bash-completion/completions
install -d %{buildroot}%{_datadir}/zsh/site-functions
install -d %{buildroot}%{_datadir}/fish/vendor_completions.d
./dms completion bash > %{buildroot}%{_datadir}/bash-completion/completions/dms || :
./dms completion zsh > %{buildroot}%{_datadir}/zsh/site-functions/_dms || :
./dms completion fish > %{buildroot}%{_datadir}/fish/vendor_completions.d/dms.fish || :
install -Dm644 quickshell/assets/systemd/dms.service %{buildroot}%{_userunitdir}/dms.service
install -dm755 %{buildroot}%{_datadir}/quickshell/dms
cp -r quickshell/* %{buildroot}%{_datadir}/quickshell/dms/
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.git*
rm -f %{buildroot}%{_datadir}/quickshell/dms/.gitignore
rm -rf %{buildroot}%{_datadir}/quickshell/dms/.github
rm -rf %{buildroot}%{_datadir}/quickshell/dms/distro
rm -rf %{buildroot}%{_datadir}/quickshell/dms/core
%posttrans
if [ -d "%{_sysconfdir}/xdg/quickshell/dms" ]; then
rmdir "%{_sysconfdir}/xdg/quickshell/dms" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg/quickshell" 2>/dev/null || true
rmdir "%{_sysconfdir}/xdg" 2>/dev/null || true
fi
if [ "$1" -ge 2 ]; then
pkill -USR1 -x dms >/dev/null 2>&1 || true
fi
%files
%license LICENSE
%doc CONTRIBUTING.md
%doc quickshell/README.md
%{_bindir}/dms
%dir %{_datadir}/fish
%dir %{_datadir}/fish/vendor_completions.d
%{_datadir}/fish/vendor_completions.d/dms.fish
%dir %{_datadir}/zsh
%dir %{_datadir}/zsh/site-functions
%{_datadir}/zsh/site-functions/_dms
%{_datadir}/bash-completion/completions/dms
%dir %{_datadir}/quickshell
%{_datadir}/quickshell/dms/
%{_userunitdir}/dms.service
%changelog
* Fri Nov 22 2025 AvengeMedia <maintainer@avengemedia.com> - 0.6.2-1
- Stable release build with pre-built binaries
- Multi-arch support (x86_64, aarch64)
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# Unified OBS status checker for dms packages
# Checks all platforms (Debian, OpenSUSE) and architectures (x86_64, aarch64)
# Only pulls logs if build failed
# Usage: ./distro/scripts/obs-status.sh [package-name]
#
# Examples:
# ./distro/scripts/obs-status.sh # Check all packages
# ./distro/scripts/obs-status.sh dms # Check specific package
OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts"
ALL_PACKAGES=(dms dms-git)
REPOS=("Debian_13" "openSUSE_Tumbleweed" "16.0")
ARCHES=("x86_64" "aarch64")
if [[ -n "$1" ]]; then
PACKAGES=("$1")
else
PACKAGES=("${ALL_PACKAGES[@]}")
fi
cd "$OBS_BASE"
for pkg in "${PACKAGES[@]}"; do
case "$pkg" in
dms)
PROJECT="$OBS_BASE_PROJECT:dms"
;;
dms-git)
PROJECT="$OBS_BASE_PROJECT:dms-git"
;;
*)
echo "Error: Unknown package '$pkg'"
continue
;;
esac
echo "=========================================="
echo "=== $pkg ==="
echo "=========================================="
# Checkout if needed
if [[ ! -d "$PROJECT/$pkg" ]]; then
osc co "$PROJECT/$pkg" 2>&1 | tail -1
fi
cd "$PROJECT/$pkg"
ALL_RESULTS=$(osc results 2>&1)
# Check each repository and architecture
FAILED_BUILDS=()
for repo in "${REPOS[@]}"; do
for arch in "${ARCHES[@]}"; do
STATUS=$(echo "$ALL_RESULTS" | grep "$repo.*$arch" | awk '{print $NF}' | head -1)
if [[ -n "$STATUS" ]]; then
# Color code status
case "$STATUS" in
succeeded)
COLOR="\033[0;32m" # Green
SYMBOL="✅"
;;
failed)
COLOR="\033[0;31m" # Red
SYMBOL="❌"
FAILED_BUILDS+=("$repo $arch")
;;
unresolvable)
COLOR="\033[0;33m" # Yellow
SYMBOL="⚠️"
;;
*)
COLOR="\033[0;37m" # White
SYMBOL="⏳"
;;
esac
echo -e " $SYMBOL $repo $arch: ${COLOR}$STATUS\033[0m"
fi
done
done
# Pull logs for failed builds
if [[ ${#FAILED_BUILDS[@]} -gt 0 ]]; then
echo ""
echo " 📋 Fetching logs for failed builds..."
for build in "${FAILED_BUILDS[@]}"; do
read -r repo arch <<< "$build"
echo ""
echo " ────────────────────────────────────────────"
echo " Build log: $repo $arch"
echo " ────────────────────────────────────────────"
osc remotebuildlog "$PROJECT" "$pkg" "$repo" "$arch" 2>&1 | tail -100
done
fi
echo ""
cd - > /dev/null
done
echo "=========================================="
echo "Status check complete!"
+733
View File
@@ -0,0 +1,733 @@
#!/bin/bash
# Unified OBS upload script for dms packages
# Handles Debian and OpenSUSE builds for both x86_64 and aarch64
# Usage: ./distro/scripts/obs-upload.sh [distro] <package-name> [commit-message]
#
# Examples:
# ./distro/scripts/obs-upload.sh dms "Update to v0.6.2"
# ./distro/scripts/obs-upload.sh debian dms
# ./distro/scripts/obs-upload.sh opensuse dms-git
set -e
UPLOAD_DEBIAN=true
UPLOAD_OPENSUSE=true
PACKAGE=""
MESSAGE=""
for arg in "$@"; do
case "$arg" in
debian)
UPLOAD_DEBIAN=true
UPLOAD_OPENSUSE=false
;;
opensuse)
UPLOAD_DEBIAN=false
UPLOAD_OPENSUSE=true
;;
*)
if [[ -z "$PACKAGE" ]]; then
PACKAGE="$arg"
elif [[ -z "$MESSAGE" ]]; then
MESSAGE="$arg"
fi
;;
esac
done
OBS_BASE_PROJECT="home:AvengeMedia"
OBS_BASE="$HOME/.cache/osc-checkouts"
# Available packages
AVAILABLE_PACKAGES=(dms dms-git)
if [[ -z "$PACKAGE" ]]; then
echo "Available packages:"
echo ""
echo " 1. dms - Stable DMS"
echo " 2. dms-git - Nightly DMS"
echo " a. all"
echo ""
read -p "Select package (1-${#AVAILABLE_PACKAGES[@]}, a): " selection
if [[ "$selection" == "a" ]] || [[ "$selection" == "all" ]]; then
PACKAGE="all"
elif [[ "$selection" =~ ^[0-9]+$ ]] && [[ "$selection" -ge 1 ]] && [[ "$selection" -le ${#AVAILABLE_PACKAGES[@]} ]]; then
PACKAGE="${AVAILABLE_PACKAGES[$((selection-1))]}"
else
echo "Error: Invalid selection"
exit 1
fi
fi
if [[ -z "$MESSAGE" ]]; then
MESSAGE="Update packaging"
fi
# Get repo root (2 levels up from distro/scripts/)
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT"
# Ensure we're in repo root
if [[ ! -d "distro/debian" ]]; then
echo "Error: Run this script from the repository root"
exit 1
fi
# Handle "all" option
if [[ "$PACKAGE" == "all" ]]; then
echo "==> Uploading all packages"
DISTRO_ARG=""
if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == false ]]; then
DISTRO_ARG="debian"
elif [[ "$UPLOAD_DEBIAN" == false && "$UPLOAD_OPENSUSE" == true ]]; then
DISTRO_ARG="opensuse"
fi
echo ""
FAILED=()
for pkg in "${AVAILABLE_PACKAGES[@]}"; do
if [[ -d "distro/debian/$pkg" ]]; then
echo "=========================================="
echo "Uploading $pkg..."
echo "=========================================="
if [[ -n "$DISTRO_ARG" ]]; then
if bash "$0" "$DISTRO_ARG" "$pkg" "$MESSAGE"; then
echo "$pkg uploaded successfully"
else
echo "$pkg failed to upload"
FAILED+=("$pkg")
fi
else
if bash "$0" "$pkg" "$MESSAGE"; then
echo "$pkg uploaded successfully"
else
echo "$pkg failed to upload"
FAILED+=("$pkg")
fi
fi
echo ""
else
echo "⚠️ Skipping $pkg (not found in distro/debian/)"
fi
done
if [[ ${#FAILED[@]} -eq 0 ]]; then
echo "✅ All packages uploaded successfully!"
exit 0
else
echo "❌ Some packages failed: ${FAILED[*]}"
exit 1
fi
fi
# Check if package exists
if [[ ! -d "distro/debian/$PACKAGE" ]]; then
echo "Error: Package '$PACKAGE' not found in distro/debian/"
exit 1
fi
case "$PACKAGE" in
dms)
PROJECT="dms"
;;
dms-git)
PROJECT="dms-git"
;;
*)
echo "Error: Unknown package '$PACKAGE'"
exit 1
;;
esac
OBS_PROJECT="${OBS_BASE_PROJECT}:${PROJECT}"
echo "==> Target: $OBS_PROJECT / $PACKAGE"
echo "==> Message: $MESSAGE"
if [[ "$UPLOAD_DEBIAN" == true && "$UPLOAD_OPENSUSE" == true ]]; then
echo "==> Distributions: Debian + OpenSUSE"
elif [[ "$UPLOAD_DEBIAN" == true ]]; then
echo "==> Distribution: Debian only"
elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
echo "==> Distribution: OpenSUSE only"
fi
# Create .obs directory if it doesn't exist
mkdir -p "$OBS_BASE"
# Check out package if not already present
if [[ ! -d "$OBS_BASE/$OBS_PROJECT/$PACKAGE" ]]; then
echo "Checking out $OBS_PROJECT/$PACKAGE..."
cd "$OBS_BASE"
osc co "$OBS_PROJECT/$PACKAGE"
cd "$REPO_ROOT"
fi
WORK_DIR="$OBS_BASE/$OBS_PROJECT/$PACKAGE"
echo "==> Preparing $PACKAGE for OBS upload"
# Clean working directory (keep osc metadata)
find "$WORK_DIR" -maxdepth 1 -type f \( -name "*.tar.gz" -o -name "*.spec" -o -name "_service" -o -name "*.dsc" \) -delete 2>/dev/null || true
if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
echo " - Copying _service (for binary downloads)"
cp "distro/debian/$PACKAGE/_service" "$WORK_DIR/"
fi
# Copy OpenSUSE spec if it exists and handle auto-increment
if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
echo " - Copying $PACKAGE.spec for OpenSUSE"
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
# Auto-increment Release if same Version is being rebuilt
if [[ -f "$WORK_DIR/.osc/$PACKAGE.spec" ]]; then
NEW_VERSION=$(grep "^Version:" "$WORK_DIR/$PACKAGE.spec" | awk '{print $2}' | head -1)
NEW_RELEASE=$(grep "^Release:" "$WORK_DIR/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
OLD_VERSION=$(grep "^Version:" "$WORK_DIR/.osc/$PACKAGE.spec" | awk '{print $2}' | head -1)
OLD_RELEASE=$(grep "^Release:" "$WORK_DIR/.osc/$PACKAGE.spec" | sed 's/^Release:[[:space:]]*//' | sed 's/%{?dist}//' | head -1)
if [[ "$NEW_VERSION" == "$OLD_VERSION" ]]; then
# Same version - increment release number
if [[ "$OLD_RELEASE" =~ ^([0-9]+) ]]; then
BASE_RELEASE="${BASH_REMATCH[1]}"
NEXT_RELEASE=$((BASE_RELEASE + 1))
echo " - Detected rebuild of same version $NEW_VERSION (release $OLD_RELEASE -> $NEXT_RELEASE)"
sed -i "s/^Release:[[:space:]]*${NEW_RELEASE}%{?dist}/Release: ${NEXT_RELEASE}%{?dist}/" "$WORK_DIR/$PACKAGE.spec"
fi
else
echo " - New version detected: $OLD_VERSION -> $NEW_VERSION (keeping release $NEW_RELEASE)"
fi
else
echo " - First upload to OBS (no previous spec found)"
fi
elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
echo " - Warning: OpenSUSE spec file not found, skipping OpenSUSE upload"
fi
# Handle OpenSUSE-only uploads (create tarball without Debian processing)
if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
echo " - OpenSUSE-only upload: creating source tarball"
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Check _service file to determine how to get source
if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
# Check for tar_scm (git source)
if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*<param name="url">\(.*\)<\/param>.*/\1/')
GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*<param name="revision">\(.*\)<\/param>.*/\1/')
if [[ -n "$GIT_URL" ]]; then
echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
SOURCE_DIR="$TEMP_DIR/dms-git-source"
if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
cd "$SOURCE_DIR"
if [[ -n "$GIT_REVISION" ]]; then
git checkout "$GIT_REVISION" 2>/dev/null || true
fi
SOURCE_DIR=$(pwd)
cd "$REPO_ROOT"
fi
fi
fi
fi
if [[ -n "$SOURCE_DIR" && -d "$SOURCE_DIR" ]]; then
# Extract Source0 from spec file
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1)
if [[ -n "$SOURCE0" ]]; then
OBS_TARBALL_DIR=$(mktemp -d -t obs-tarball-XXXXXX)
cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in
dms)
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
;;
dms-git)
EXPECTED_DIR="dms-git-source"
;;
*)
EXPECTED_DIR=$(basename "$SOURCE_DIR")
;;
esac
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
cd "$REPO_ROOT"
rm -rf "$OBS_TARBALL_DIR"
fi
else
echo " - Warning: Could not obtain source for OpenSUSE tarball"
fi
fi
# Generate .dsc file and handle source format (for Debian only)
if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; then
# Get version from changelog
CHANGELOG_VERSION=$(grep -m1 "^$PACKAGE" distro/debian/$PACKAGE/debian/changelog 2>/dev/null | sed 's/.*(\([^)]*\)).*/\1/' || echo "0.1.11")
# Determine source format
SOURCE_FORMAT=$(cat "distro/debian/$PACKAGE/debian/source/format" 2>/dev/null || echo "3.0 (quilt)")
# Handle native format (3.0 native)
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
echo " - Native format detected: creating combined tarball"
VERSION="$CHANGELOG_VERSION"
# Create temp directory for building combined tarball
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
# Determine tarball name for native format (use version without revision)
COMBINED_TARBALL="${PACKAGE}_${VERSION}.tar.gz"
SOURCE_DIR=""
# Check _service file to determine how to get source
if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
# Check for tar_scm first (git source) - this takes priority for git packages
if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
# For dms-git, use tar_scm to get git source
GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*<param name="url">\(.*\)<\/param>.*/\1/')
GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*<param name="revision">\(.*\)<\/param>.*/\1/')
if [[ -n "$GIT_URL" ]]; then
echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
SOURCE_DIR="$TEMP_DIR/dms-git-source"
if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
cd "$SOURCE_DIR"
if [[ -n "$GIT_REVISION" ]]; then
git checkout "$GIT_REVISION" 2>/dev/null || true
fi
SOURCE_DIR=$(pwd)
cd "$REPO_ROOT"
else
echo "Error: Failed to clone git repository"
exit 1
fi
fi
elif grep -q "download_url" "distro/debian/$PACKAGE/_service" && [[ "$PACKAGE" != "dms-git" ]]; then
# Extract download_url for source (skip binary downloads)
# Look for download_url with "source" in path or .tar.gz/.tar.xz archives
# Skip binaries (distropkg, standalone .gz files, etc.)
# Extract all paths from download_url services
ALL_PATHS=$(grep -A 5 '<service name="download_url">' "distro/debian/$PACKAGE/_service" | \
grep '<param name="path">' | \
sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/')
# Find source path (has "source" or ends with .tar.gz/.tar.xz, but not distropkg)
SOURCE_PATH=""
for path in $ALL_PATHS; do
if echo "$path" | grep -qE "(source|archive|\.tar\.(gz|xz|bz2))" && \
! echo "$path" | grep -qE "(distropkg|binary)"; then
SOURCE_PATH="$path"
break
fi
done
# If no source found, try first path that ends with .tar.gz/.tar.xz
if [[ -z "$SOURCE_PATH" ]]; then
for path in $ALL_PATHS; do
if echo "$path" | grep -qE "\.tar\.(gz|xz|bz2)$"; then
SOURCE_PATH="$path"
break
fi
done
fi
if [[ -n "$SOURCE_PATH" ]]; then
# Extract the service block containing this path
SOURCE_BLOCK=$(awk -v target="$SOURCE_PATH" '
/<service name="download_url">/ { in_block=1; block="" }
in_block { block=block"\n"$0 }
/<\/service>/ {
if (in_block && block ~ target) {
print block
exit
}
in_block=0
}
' "distro/debian/$PACKAGE/_service")
URL_PROTOCOL=$(echo "$SOURCE_BLOCK" | grep "protocol" | sed 's/.*<param name="protocol">\(.*\)<\/param>.*/\1/' | head -1)
URL_HOST=$(echo "$SOURCE_BLOCK" | grep "host" | sed 's/.*<param name="host">\(.*\)<\/param>.*/\1/' | head -1)
URL_PATH="$SOURCE_PATH"
fi
if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then
SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}"
echo " Downloading source from: $SOURCE_URL"
if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL"; then
cd "$TEMP_DIR"
if [[ "$SOURCE_URL" == *.tar.xz ]]; then
tar -xJf source-archive
elif [[ "$SOURCE_URL" == *.tar.gz ]] || [[ "$SOURCE_URL" == *.tgz ]]; then
tar -xzf source-archive
fi
# GitHub archives extract to DankMaterialShell-VERSION/ or similar
SOURCE_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
if [[ -z "$SOURCE_DIR" ]]; then
# Try to find any extracted directory
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Failed to extract source archive or find source directory"
echo "Contents of $TEMP_DIR:"
ls -la "$TEMP_DIR"
cd "$REPO_ROOT"
exit 1
fi
# Convert to absolute path
SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd)
cd "$REPO_ROOT"
else
echo "Error: Failed to download source from $SOURCE_URL"
exit 1
fi
fi
fi
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Could not determine or obtain source for $PACKAGE"
echo "SOURCE_DIR: $SOURCE_DIR"
if [[ -d "$TEMP_DIR" ]]; then
echo "Contents of temp directory:"
ls -la "$TEMP_DIR"
fi
exit 1
fi
echo " Found source directory: $SOURCE_DIR"
# Create OpenSUSE-compatible source tarballs BEFORE adding debian/ directory
# (OpenSUSE doesn't need debian/ directory)
if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ -f "distro/opensuse/$PACKAGE.spec" ]]; then
# If SOURCE_DIR is not set (OpenSUSE-only upload), detect source now
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo " - Detecting source for OpenSUSE-only upload"
if [[ -z "$TEMP_DIR" ]]; then
TEMP_DIR=$(mktemp -d)
trap "rm -rf $TEMP_DIR" EXIT
fi
# Check _service file to determine how to get source
if [[ -f "distro/debian/$PACKAGE/_service" ]]; then
# Check for tar_scm first (git source) - this takes priority for git packages
if grep -q "tar_scm" "distro/debian/$PACKAGE/_service"; then
# For dms-git, use tar_scm to get git source
GIT_URL=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "url" | sed 's/.*<param name="url">\(.*\)<\/param>.*/\1/')
GIT_REVISION=$(grep -A 5 'name="tar_scm"' "distro/debian/$PACKAGE/_service" | grep "revision" | sed 's/.*<param name="revision">\(.*\)<\/param>.*/\1/')
if [[ -n "$GIT_URL" ]]; then
echo " Cloning git source from: $GIT_URL (revision: ${GIT_REVISION:-master})"
SOURCE_DIR="$TEMP_DIR/dms-git-source"
if git clone --depth 1 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null || \
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
cd "$SOURCE_DIR"
if [[ -n "$GIT_REVISION" ]]; then
git checkout "$GIT_REVISION" 2>/dev/null || true
fi
SOURCE_DIR=$(pwd)
cd "$REPO_ROOT"
else
echo "Error: Failed to clone git repository"
exit 1
fi
fi
elif grep -q "download_url" "distro/debian/$PACKAGE/_service" && [[ "$PACKAGE" != "dms-git" ]]; then
# Extract download_url for source (skip binary downloads)
ALL_PATHS=$(grep -A 5 '<service name="download_url">' "distro/debian/$PACKAGE/_service" | \
grep '<param name="path">' | \
sed 's/.*<param name="path">\(.*\)<\/param>.*/\1/')
# Find source path (has "source" or ends with .tar.gz/.tar.xz, but not distropkg)
SOURCE_PATH=""
for path in $ALL_PATHS; do
if echo "$path" | grep -qE "(source|archive|\.tar\.(gz|xz|bz2))" && \
! echo "$path" | grep -qE "(distropkg|binary)"; then
SOURCE_PATH="$path"
break
fi
done
# If no source found, try first path that ends with .tar.gz/.tar.xz
if [[ -z "$SOURCE_PATH" ]]; then
for path in $ALL_PATHS; do
if echo "$path" | grep -qE "\.tar\.(gz|xz|bz2)$"; then
SOURCE_PATH="$path"
break
fi
done
fi
if [[ -n "$SOURCE_PATH" ]]; then
# Extract the service block containing this path
SOURCE_BLOCK=$(awk -v target="$SOURCE_PATH" '
/<service name="download_url">/ { in_block=1; block="" }
in_block { block=block"\n"$0 }
/<\/service>/ {
if (in_block && block ~ target) {
print block
exit
}
in_block=0
}
' "distro/debian/$PACKAGE/_service")
URL_PROTOCOL=$(echo "$SOURCE_BLOCK" | grep "protocol" | sed 's/.*<param name="protocol">\(.*\)<\/param>.*/\1/' | head -1)
URL_HOST=$(echo "$SOURCE_BLOCK" | grep "host" | sed 's/.*<param name="host">\(.*\)<\/param>.*/\1/' | head -1)
URL_PATH="$SOURCE_PATH"
fi
if [[ -n "$URL_PROTOCOL" && -n "$URL_HOST" && -n "$URL_PATH" ]]; then
SOURCE_URL="${URL_PROTOCOL}://${URL_HOST}${URL_PATH}"
echo " Downloading source from: $SOURCE_URL"
if wget -q -O "$TEMP_DIR/source-archive" "$SOURCE_URL"; then
cd "$TEMP_DIR"
if [[ "$SOURCE_URL" == *.tar.xz ]]; then
tar -xJf source-archive
elif [[ "$SOURCE_URL" == *.tar.gz ]] || [[ "$SOURCE_URL" == *.tgz ]]; then
tar -xzf source-archive
fi
# GitHub archives extract to DankMaterialShell-VERSION/ or similar
SOURCE_DIR=$(find . -maxdepth 1 -type d -name "DankMaterialShell-*" | head -1)
if [[ -z "$SOURCE_DIR" ]]; then
# Try to find any extracted directory
SOURCE_DIR=$(find . -maxdepth 1 -type d ! -name "." | head -1)
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Failed to extract source archive or find source directory"
echo "Contents of $TEMP_DIR:"
ls -la "$TEMP_DIR"
cd "$REPO_ROOT"
exit 1
fi
# Convert to absolute path
SOURCE_DIR=$(cd "$SOURCE_DIR" && pwd)
cd "$REPO_ROOT"
else
echo "Error: Failed to download source from $SOURCE_URL"
exit 1
fi
fi
fi
fi
if [[ -z "$SOURCE_DIR" || ! -d "$SOURCE_DIR" ]]; then
echo "Error: Could not determine or obtain source for $PACKAGE (OpenSUSE-only upload)"
echo "SOURCE_DIR: $SOURCE_DIR"
if [[ -d "$TEMP_DIR" ]]; then
echo "Contents of temp directory:"
ls -la "$TEMP_DIR"
fi
exit 1
fi
echo " Found source directory: $SOURCE_DIR"
fi
echo " - Creating OpenSUSE-compatible source tarballs"
# Extract Source0 from spec file
SOURCE0=$(grep "^Source0:" "distro/opensuse/$PACKAGE.spec" | awk '{print $2}' | head -1); if [[ -z "$SOURCE0" && "$PACKAGE" == "dms-git" ]]; then SOURCE0="dms-git-source.tar.gz"; fi
if [[ -n "$SOURCE0" ]]; then
# Create a separate temporary directory for OpenSUSE tarball creation to avoid conflicts
OBS_TARBALL_DIR=$(mktemp -d -t obs-tarball-XXXXXX)
cd "$OBS_TARBALL_DIR"
case "$PACKAGE" in
dms)
# dms spec expects DankMaterialShell-%{version} directory (from %setup -q -n DankMaterialShell-%{version})
# Extract version from spec file
DMS_VERSION=$(grep "^Version:" "$REPO_ROOT/distro/opensuse/$PACKAGE.spec" | sed 's/^Version:[[:space:]]*//' | head -1)
EXPECTED_DIR="DankMaterialShell-${DMS_VERSION}"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
fi
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
dms-git)
# dms-git spec expects dms-git-source directory (from %setup -q -n dms-git-source)
EXPECTED_DIR="dms-git-source"
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
fi
rm -rf "$EXPECTED_DIR"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
*)
# Generic handling
DIR_NAME=$(basename "$SOURCE_DIR")
echo " Creating $SOURCE0 (directory: $DIR_NAME)"
cp -r "$SOURCE_DIR" "$DIR_NAME"
if [[ "$SOURCE0" == *.tar.xz ]]; then
tar -cJf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar -cjf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
else
tar -czf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
fi
rm -rf "$DIR_NAME"
echo " Created $SOURCE0 ($(stat -c%s "$WORK_DIR/$SOURCE0" 2>/dev/null || echo 0) bytes)"
;;
esac
# Clean up the tarball work directory
cd "$REPO_ROOT"
rm -rf "$OBS_TARBALL_DIR"
echo " - OpenSUSE source tarballs created"
fi
# Copy spec file
cp "distro/opensuse/$PACKAGE.spec" "$WORK_DIR/"
fi
# Copy debian/ directory into source (for Debian builds only)
if [[ "$UPLOAD_DEBIAN" == true ]]; then
echo " Copying debian/ directory into source"
cp -r "distro/debian/$PACKAGE/debian" "$SOURCE_DIR/"
# Create combined tarball
echo " Creating combined tarball: $COMBINED_TARBALL"
cd "$(dirname "$SOURCE_DIR")"
TARBALL_BASE=$(basename "$SOURCE_DIR")
tar -czf "$WORK_DIR/$COMBINED_TARBALL" "$TARBALL_BASE"
cd "$REPO_ROOT"
# Generate .dsc file for native format
TARBALL_SIZE=$(stat -c%s "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null || stat -f%z "$WORK_DIR/$COMBINED_TARBALL" 2>/dev/null)
TARBALL_MD5=$(md5sum "$WORK_DIR/$COMBINED_TARBALL" | cut -d' ' -f1)
# Extract Build-Depends from control file
BUILD_DEPS="debhelper-compat (= 13)"
if [[ -f "distro/debian/$PACKAGE/debian/control" ]]; then
CONTROL_DEPS=$(sed -n '/^Build-Depends:/,/^[A-Z]/p' "distro/debian/$PACKAGE/debian/control" | \
sed '/^Build-Depends:/s/^Build-Depends: *//' | \
sed '/^[A-Z]/d' | \
tr '\n' ' ' | \
sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/[[:space:]]\+/ /g')
if [[ -n "$CONTROL_DEPS" && "$CONTROL_DEPS" != "" ]]; then
BUILD_DEPS="$CONTROL_DEPS"
fi
fi
cat > "$WORK_DIR/$PACKAGE.dsc" << EOF
Format: 3.0 (native)
Source: $PACKAGE
Binary: $PACKAGE
Architecture: any
Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: $BUILD_DEPS
Files:
$TARBALL_MD5 $TARBALL_SIZE $COMBINED_TARBALL
EOF
echo " - Generated $PACKAGE.dsc for native format"
fi
else
# Quilt format (legacy) - for Debian only
if [[ "$UPLOAD_DEBIAN" == true ]]; then
# For quilt format, version can have revision
if [[ "$CHANGELOG_VERSION" == *"-"* ]]; then
VERSION="$CHANGELOG_VERSION"
else
VERSION="${CHANGELOG_VERSION}-1"
fi
echo " - Quilt format detected: creating debian.tar.gz"
tar -czf "$WORK_DIR/debian.tar.gz" -C "distro/debian/$PACKAGE" debian/
echo " - Generating $PACKAGE.dsc for quilt format"
cat > "$WORK_DIR/$PACKAGE.dsc" << EOF
Format: 3.0 (quilt)
Source: $PACKAGE
Binary: $PACKAGE
Architecture: any
Version: $VERSION
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13), wget, gzip
DEBTRANSFORM-TAR: debian.tar.gz
Files:
00000000000000000000000000000000 1 debian.tar.gz
EOF
fi
fi
fi
# Change to working directory and commit
cd "$WORK_DIR"
echo "==> Staging changes"
# List files to be uploaded
echo "Files to upload:"
# Only list files relevant to the selected upload type
if [[ "$UPLOAD_DEBIAN" == true ]] && [[ "$UPLOAD_OPENSUSE" == true ]]; then
ls -lh *.tar.gz *.tar.xz *.tar *.spec *.dsc _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
elif [[ "$UPLOAD_DEBIAN" == true ]]; then
ls -lh *.tar.gz *.dsc _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
elif [[ "$UPLOAD_OPENSUSE" == true ]]; then
ls -lh *.tar.gz *.tar.xz *.tar *.spec _service 2>/dev/null | awk '{print " " $9 " (" $5 ")"}'
fi
echo ""
osc addremove
echo "==> Committing to OBS"
osc commit -m "$MESSAGE"
echo "==> Checking build status"
osc results
echo ""
echo "Upload complete! Monitor builds with:"
echo " cd $WORK_DIR && osc results"
echo " cd $WORK_DIR && osc buildlog <repo> <arch>"
echo ""
# Don't cleanup - keep checkout for status checking
echo ""
echo "Upload complete! Build status:"
cd "$WORK_DIR"
osc results 2>&1 | head -10
cd "$REPO_ROOT"
echo ""
echo "To check detailed status:"
echo " cd $WORK_DIR && osc results"
echo " cd $WORK_DIR && osc remotebuildlog $OBS_PROJECT $PACKAGE Debian_13 x86_64"
echo ""
echo "NOTE: Checkout kept at $WORK_DIR for status checking"
echo ""
echo "✅ Upload complete!"
echo ""
echo "Check build status with:"
echo " ./distro/scripts/obs-status.sh $PACKAGE"
+169
View File
@@ -0,0 +1,169 @@
#!/bin/bash
# Manual testing script for DMS packaging
# Tests OBS (Debian/openSUSE) and PPA (Ubuntu) workflows
# Usage: ./distro/test-packaging.sh [obs|ppa|all]
set -e
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
DISTRO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
REPO_ROOT="$(cd "$DISTRO_DIR/.." && pwd)"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
TEST_MODE="${1:-all}"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "DMS Packaging Test Suite"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
# Test 1: OBS Upload (Debian + openSUSE)
if [[ "$TEST_MODE" == "obs" ]] || [[ "$TEST_MODE" == "all" ]]; then
echo "═══════════════════════════════════════════════════════════════════"
echo "TEST 1: OBS Upload (Debian + openSUSE)"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
OBS_SCRIPT="$SCRIPT_DIR/obs-upload.sh"
if [[ ! -f "$OBS_SCRIPT" ]]; then
error "OBS script not found: $OBS_SCRIPT"
exit 1
fi
info "OBS script location: $OBS_SCRIPT"
info "Available packages: dms, dms-git"
echo ""
warn "This will upload to OBS (home:AvengeMedia)"
read -p "Continue with OBS test? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
info "Select package to test:"
echo " 1. dms (stable)"
echo " 2. dms-git (nightly)"
echo " 3. all (both packages)"
read -p "Choice [1]: " -n 1 -r PKG_CHOICE
echo
echo ""
PKG_CHOICE="${PKG_CHOICE:-1}"
cd "$REPO_ROOT"
case "$PKG_CHOICE" in
1)
info "Testing OBS upload for 'dms' package..."
bash "$OBS_SCRIPT" dms "Test packaging update"
;;
2)
info "Testing OBS upload for 'dms-git' package..."
bash "$OBS_SCRIPT" dms-git "Test packaging update"
;;
3)
info "Testing OBS upload for all packages..."
bash "$OBS_SCRIPT" all "Test packaging update"
;;
*)
error "Invalid choice"
exit 1
;;
esac
echo ""
success "OBS test completed"
echo ""
info "Check build status: https://build.opensuse.org/project/monitor/home:AvengeMedia"
else
warn "OBS test skipped"
fi
echo ""
fi
# Test 2: PPA Upload (Ubuntu)
if [[ "$TEST_MODE" == "ppa" ]] || [[ "$TEST_MODE" == "all" ]]; then
echo "═══════════════════════════════════════════════════════════════════"
echo "TEST 2: PPA Upload (Ubuntu)"
echo "═══════════════════════════════════════════════════════════════════"
echo ""
PPA_SCRIPT="$DISTRO_DIR/ubuntu/ppa/create-and-upload.sh"
if [[ ! -f "$PPA_SCRIPT" ]]; then
error "PPA script not found: $PPA_SCRIPT"
exit 1
fi
info "PPA script location: $PPA_SCRIPT"
info "Available PPAs: dms, dms-git"
info "Ubuntu series: questing (25.10)"
echo ""
warn "This will upload to Launchpad PPA (ppa:avengemedia/dms)"
read -p "Continue with PPA test? [y/N] " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
info "Select package to test:"
echo " 1. dms (stable)"
echo " 2. dms-git (nightly)"
read -p "Choice [1]: " -n 1 -r PKG_CHOICE
echo
echo ""
PKG_CHOICE="${PKG_CHOICE:-1}"
case "$PKG_CHOICE" in
1)
info "Testing PPA upload for 'dms' package..."
DMS_PKG="$DISTRO_DIR/ubuntu/dms"
PPA_NAME="dms"
;;
2)
info "Testing PPA upload for 'dms-git' package..."
DMS_PKG="$DISTRO_DIR/ubuntu/dms-git"
PPA_NAME="dms-git"
;;
*)
error "Invalid choice"
exit 1
;;
esac
echo ""
if [[ ! -d "$DMS_PKG" ]]; then
error "DMS package directory not found: $DMS_PKG"
exit 1
fi
bash "$PPA_SCRIPT" "$DMS_PKG" "$PPA_NAME" questing
echo ""
success "PPA test completed"
echo ""
info "Check build status: https://launchpad.net/~avengemedia/+archive/ubuntu/dms/+packages"
else
warn "PPA test skipped"
fi
echo ""
fi
# Summary
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "Testing Summary"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
@@ -0,0 +1,5 @@
danksearch (0.0.7ppa3) questing; urgency=medium
* Rebuild for packaging fixes (ppa3)
-- Avenge Media <AvengeMedia.US@gmail.com> Fri, 21 Nov 2025 14:19:58 -0500
@@ -0,0 +1,24 @@
Source: danksearch
Section: utils
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/danksearch
Vcs-Browser: https://github.com/AvengeMedia/danksearch
Vcs-Git: https://github.com/AvengeMedia/danksearch.git
Package: danksearch
Architecture: amd64 arm64
Depends: ${misc:Depends}
Description: Fast file search utility for DMS
DankSearch is a fast file search utility designed for DankMaterialShell.
It provides efficient file and content search capabilities with minimal
dependencies. This package contains the pre-built binary from the official
GitHub release.
.
Features include:
- Fast file searching
- Lightweight and efficient
- Designed for DMS integration
- Minimal resource usage
@@ -0,0 +1,24 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: danksearch
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/danksearch
Files: *
Copyright: 2025 Avenge Media LLC
License: GPL-3.0-only
License: GPL-3.0-only
This package is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License version 3 as
published by the Free Software Foundation.
.
This package is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>
.
On Debian systems, the complete text of the GNU General
Public License version 3 can be found in "/usr/share/common-licenses/GPL-3".
@@ -0,0 +1 @@
danksearch_0.0.7ppa3_source.buildinfo utils optional
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
# Detect architecture for selecting correct binary
DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
# Map Debian arch to binary filename
ifeq ($(DEB_HOST_ARCH),amd64)
BINARY_FILE := dsearch-amd64
else ifeq ($(DEB_HOST_ARCH),arm64)
BINARY_FILE := dsearch-arm64
else
$(error Unsupported architecture: $(DEB_HOST_ARCH))
endif
%:
dh $@
override_dh_auto_build:
# Binary is already included in source package (native format)
# Downloaded by build-source.sh before upload
# Just verify it exists and is executable
test -f $(BINARY_FILE) || (echo "ERROR: $(BINARY_FILE) not found!" && exit 1)
chmod +x $(BINARY_FILE)
override_dh_auto_install:
# Install binary as danksearch
install -Dm755 $(BINARY_FILE) debian/danksearch/usr/bin/danksearch
override_dh_auto_clean:
# Don't delete binaries - they're part of the source package (native format)
dh_auto_clean
@@ -0,0 +1 @@
3.0 (native)
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,9 @@
dgop (0.1.11ppa2) questing; urgency=medium
* Rebuild for Questing (25.10) - Ubuntu 25.10+ only
* Stateless CPU/GPU monitoring tool
* Support for NVIDIA and AMD GPUs
* JSON output for integration
* Pre-built binary package for amd64 and arm64
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 16 Nov 2025 22:50:00 -0500
@@ -0,0 +1,27 @@
Source: dgop
Section: utils
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13),
wget,
gzip
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/dgop
Vcs-Browser: https://github.com/AvengeMedia/dgop
Vcs-Git: https://github.com/AvengeMedia/dgop.git
Package: dgop
Architecture: amd64 arm64
Depends: ${misc:Depends}
Description: Stateless CPU/GPU monitor for DankMaterialShell
DGOP is a stateless system monitoring tool that provides CPU, GPU,
memory, and network statistics. Designed for integration with
DankMaterialShell but can be used standalone.
.
Features:
- CPU usage monitoring
- GPU usage and temperature (NVIDIA, AMD)
- Memory and swap statistics
- Network traffic monitoring
- Zero-state design (no background processes)
- JSON output for easy integration
@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dgop
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/dgop
Files: *
Copyright: 2025 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
@@ -0,0 +1 @@
dgop_0.1.11ppa2_source.buildinfo utils optional
+38
View File
@@ -0,0 +1,38 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
# Extract version from debian/changelog
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
# Get upstream version (strip -1ppa1 suffix)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
# Detect architecture for downloading correct binary
DEB_HOST_ARCH := $(shell dpkg-architecture -qDEB_HOST_ARCH)
# Map Debian arch to GitHub release arch names
ifeq ($(DEB_HOST_ARCH),amd64)
GITHUB_ARCH := amd64
else ifeq ($(DEB_HOST_ARCH),arm64)
GITHUB_ARCH := arm64
else
$(error Unsupported architecture: $(DEB_HOST_ARCH))
endif
%:
dh $@
override_dh_auto_build:
# Binary is already included in source package (native format)
# Just verify it exists and is executable
test -f dgop || (echo "ERROR: dgop binary not found!" && exit 1)
chmod +x dgop
override_dh_auto_install:
# Install binary
install -Dm755 dgop debian/dgop/usr/bin/dgop
override_dh_auto_clean:
# Don't delete dgop binary - it's part of the source package (native format)
rm -f dgop.gz
dh_auto_clean
@@ -0,0 +1 @@
3.0 (native)
+5
View File
@@ -0,0 +1,5 @@
dms-git (0.6.2+git2094.6cc6e7c8ppa1) questing; urgency=medium
* Git snapshot (commit 2094: 6cc6e7c8)
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 23 Nov 2025 00:43:28 -0500
+50
View File
@@ -0,0 +1,50 @@
Source: dms-git
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms-git
Architecture: amd64
Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
qml6-module-qtcore,
qml6-module-qtmultimedia,
qml6-module-qtqml,
qml6-module-qtquick,
qml6-module-qtquick-controls,
qml6-module-qtquick-dialogs,
qml6-module-qtquick-effects,
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
Provides: dms
Conflicts: dms
Replaces: dms
Description: DankMaterialShell - Modern Wayland Desktop Shell (git nightly)
DMS (DankMaterialShell) is a feature-rich desktop shell built on
Quickshell, providing a modern and customizable user interface for
Wayland compositors like niri, hyprland, and sway.
.
This is the nightly/git version built from the latest master branch.
.
Features include:
- Material Design inspired UI
- Customizable themes and appearance
- Built-in application launcher
- System tray and notifications
- Network and Bluetooth management
- Audio controls
- Systemd integration
+27
View File
@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2025 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dms-git_0.6.2+git2094.6cc6e7c8ppa1_source.buildinfo x11 optional
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
# Get git commit date for version
GIT_DATE := $(shell date +%Y%m%d)
GIT_COMMIT := HEAD
%:
dh $@
override_dh_auto_build:
# Git source is already included in source package (cloned by build-source.sh)
# Launchpad build environment has no internet access
test -d dms-git-repo || (echo "ERROR: dms-git-repo directory not found!" && exit 1)
test -f dms-distropkg-amd64.gz || (echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1)
# Extract pre-built binary from latest release
# Note: For git versions, we use the latest release binary
# The QML files come from git master
gunzip -c dms-distropkg-amd64.gz > dms
chmod +x dms
override_dh_auto_install:
# Install binary
install -Dm755 dms debian/dms-git/usr/bin/dms
# Install QML files from git clone
mkdir -p debian/dms-git/usr/share/quickshell/dms
cp -r dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
# Remove unnecessary directories
rm -rf debian/dms-git/usr/share/quickshell/dms/core
rm -rf debian/dms-git/usr/share/quickshell/dms/distro
# Install systemd user service
install -Dm644 dms-git-repo/quickshell/assets/systemd/dms.service \
debian/dms-git/usr/lib/systemd/user/dms.service
override_dh_auto_clean:
# Don't delete dms-git-repo directory - it's part of the source package (native format)
# Clean up build artifacts (but keep dms-distropkg-amd64.gz for Launchpad)
rm -f dms
# Don't remove dms-distropkg-amd64.gz - it needs to be included in the source package for Launchpad builds
dh_auto_clean
@@ -0,0 +1 @@
3.0 (native)
@@ -0,0 +1 @@
dms-distropkg-amd64.gz
@@ -0,0 +1,4 @@
# Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad (which has no internet access)
tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-git-repo
+5
View File
@@ -0,0 +1,5 @@
dms (0.6.2ppa3) questing; urgency=medium
* Rebuild for packaging fixes (ppa3)
-- Avenge Media <AvengeMedia.US@gmail.com> Sun, 23 Nov 2025 00:40:41 -0500
+47
View File
@@ -0,0 +1,47 @@
Source: dms
Section: x11
Priority: optional
Maintainer: Avenge Media <AvengeMedia.US@gmail.com>
Build-Depends: debhelper-compat (= 13)
Standards-Version: 4.6.2
Homepage: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Browser: https://github.com/AvengeMedia/DankMaterialShell
Vcs-Git: https://github.com/AvengeMedia/DankMaterialShell.git
Package: dms
Architecture: amd64
Depends: ${misc:Depends},
quickshell-git | quickshell,
accountsservice,
cava,
cliphist,
danksearch,
dgop,
matugen,
qml6-module-qtcore,
qml6-module-qtmultimedia,
qml6-module-qtqml,
qml6-module-qtquick,
qml6-module-qtquick-controls,
qml6-module-qtquick-dialogs,
qml6-module-qtquick-effects,
qml6-module-qtquick-layouts,
qml6-module-qtquick-templates,
qml6-module-qtquick-window,
qt6ct,
wl-clipboard
Conflicts: dms-git
Replaces: dms-git
Description: DankMaterialShell - Modern Wayland Desktop Shell
DMS (DankMaterialShell) is a feature-rich desktop shell built on
Quickshell, providing a modern and customizable user interface for
Wayland compositors like niri, hyprland, and sway.
.
Features include:
- Material Design inspired UI
- Customizable themes and appearance
- Built-in application launcher
- System tray and notifications
- Network and Bluetooth management
- Audio controls
- Systemd integration
+27
View File
@@ -0,0 +1,27 @@
Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: dms
Upstream-Contact: Avenge Media LLC <AvengeMedia.US@gmail.com>
Source: https://github.com/AvengeMedia/DankMaterialShell
Files: *
Copyright: 2025 Avenge Media LLC
License: MIT
License: MIT
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
.
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dms_0.6.2ppa3_source.buildinfo x11 optional
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/make -f
export DH_VERBOSE = 1
# Extract version from debian/changelog
DEB_VERSION := $(shell dpkg-parsechangelog -S Version)
# Get upstream version (strip -1ppa1 suffix)
UPSTREAM_VERSION := $(shell echo $(DEB_VERSION) | sed 's/-[^-]*$$//')
# Strip ppa suffix and handle git versions
# Examples: 0.5.2ppa9 -> 0.5.2, 0.5.2+git20251116 -> 0.5.2
BASE_VERSION := $(shell echo $(UPSTREAM_VERSION) | sed 's/ppa[0-9]*$$//' | sed 's/+git.*//')
%:
dh $@
override_dh_auto_build:
# All files are included in source package (downloaded by build-source.sh)
# Launchpad build environment has no internet access
test -f dms-distropkg-amd64.gz || (echo "ERROR: dms-distropkg-amd64.gz not found!" && exit 1)
test -f dms-source.tar.gz || (echo "ERROR: dms-source.tar.gz not found!" && exit 1)
# Extract pre-built binary
gunzip -c dms-distropkg-amd64.gz > dms
chmod +x dms
# Extract source tarball for QML files
tar -xzf dms-source.tar.gz
# Find the extracted directory (it might have various names)
# and create a symlink to expected name for consistent install
SOURCE_DIR=$$(find . -maxdepth 1 -type d -name "DankMaterialShell*" | head -n1); \
if [ -n "$$SOURCE_DIR" ]; then \
ln -sf $$SOURCE_DIR DankMaterialShell-$(BASE_VERSION); \
fi
override_dh_auto_install:
# Install binary
install -Dm755 dms debian/dms/usr/bin/dms
# Install QML files from source tarball
mkdir -p debian/dms/usr/share/quickshell/dms
cp -r DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
# Remove unnecessary directories
rm -rf debian/dms/usr/share/quickshell/dms/core
rm -rf debian/dms/usr/share/quickshell/dms/distro
# Install systemd user service
install -Dm644 DankMaterialShell-$(BASE_VERSION)/quickshell/assets/systemd/dms.service \
debian/dms/usr/lib/systemd/user/dms.service
# Generate and install shell completions (if applicable)
# Uncomment if dms supports completion generation
# ./dms completion bash > dms.bash
# ./dms completion zsh > dms.zsh
# install -Dm644 dms.bash debian/dms/usr/share/bash-completion/completions/dms
# install -Dm644 dms.zsh debian/dms/usr/share/zsh/vendor-completions/_dms
override_dh_auto_clean:
rm -f dms
rm -rf DankMaterialShell-*
# Don't remove dms-distropkg-amd64.gz and dms-source.tar.gz
# They need to be included in the source package for Launchpad builds
dh_auto_clean
+1
View File
@@ -0,0 +1 @@
3.0 (native)
@@ -0,0 +1,2 @@
dms-distropkg-amd64.gz
dms-source.tar.gz
+4
View File
@@ -0,0 +1,4 @@
# Include files that are normally excluded by .gitignore
# These are needed for the build process on Launchpad (which has no internet access)
tar-ignore = !dms-distropkg-amd64.gz
tar-ignore = !dms-source.tar.gz
+44
View File
@@ -0,0 +1,44 @@
# dput configuration for AvengeMedia DMS PPAs
# Copy this to ~/.dput.cf (or merge with existing ~/.dput.cf)
#
# Usage:
# dput ppa:avengemedia/dms ../package_version_source.changes
# dput ppa:avengemedia/dms-git ../package_version_source.changes
# Stable DMS PPA - for release versions
[ppa:avengemedia/dms]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms/
login = anonymous
allow_unsigned_uploads = 0
# Nightly/Git DMS PPA - for development builds
[ppa:avengemedia/dms-git]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms-git/
login = anonymous
allow_unsigned_uploads = 0
# Alternative: Use HTTPS instead of FTP (more reliable through firewalls)
# Uncomment these if FTP doesn't work:
#
# [ppa:avengemedia/dms-https]
# fqdn = ppa.launchpad.net
# method = https
# incoming = ~avengemedia/ubuntu/dms/
# login = anonymous
# allow_unsigned_uploads = 0
#
# [ppa:avengemedia/dms-git-https]
# fqdn = ppa.launchpad.net
# method = https
# incoming = ~avengemedia/ubuntu/dms-git/
# login = anonymous
# allow_unsigned_uploads = 0
# Notes:
# - allow_unsigned_uploads = 0 enforces GPG signing (required by Launchpad)
# - anonymous login is standard for PPA uploads
# - The incoming path must match your Launchpad username and PPA name
+246
View File
@@ -0,0 +1,246 @@
#!/bin/bash
# Build and upload PPA package with automatic cleanup
# Usage: ./create-and-upload.sh <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]
#
# Example:
# ./create-and-upload.sh ../dms dms questing
# ./create-and-upload.sh ../danklinux/dgop danklinux questing --keep-builds
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
# Parse arguments
KEEP_BUILDS=false
ARGS=()
for arg in "$@"; do
if [ "$arg" = "--keep-builds" ]; then
KEEP_BUILDS=true
else
ARGS+=("$arg")
fi
done
if [ ${#ARGS[@]} -lt 2 ]; then
error "Usage: $0 <package-dir> <ppa-name> [ubuntu-series] [--keep-builds]"
echo
echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms, ../danklinux/dgop)"
echo " ppa-name : PPA name (danklinux, dms, dms-git)"
echo " ubuntu-series : Ubuntu series (optional, default: questing)"
echo " Supported: questing (25.10) and newer only"
echo " Note: Requires Qt 6.6+ (quickshell requirement)"
echo " --keep-builds : Keep build artifacts after upload (optional)"
echo
echo "Examples:"
echo " $0 ../dms dms questing"
echo " $0 ../danklinux/dgop danklinux questing --keep-builds"
echo " $0 ../dms-git dms-git # Defaults to questing"
exit 1
fi
PACKAGE_DIR="${ARGS[0]}"
PPA_NAME="${ARGS[1]}"
UBUNTU_SERIES="${ARGS[2]:-questing}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BUILD_SCRIPT="$SCRIPT_DIR/create-source.sh"
UPLOAD_SCRIPT="$SCRIPT_DIR/upload-ppa.sh"
# Validate scripts exist
if [ ! -f "$BUILD_SCRIPT" ]; then
error "Build script not found: $BUILD_SCRIPT"
exit 1
fi
# Get absolute path
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
PARENT_DIR=$(dirname "$PACKAGE_DIR")
info "Building and uploading: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR"
info "PPA: ppa:avengemedia/$PPA_NAME"
info "Ubuntu series: $UBUNTU_SERIES"
echo
# Step 1: Build source package
info "Step 1: Building source package..."
if ! "$BUILD_SCRIPT" "$PACKAGE_DIR" "$UBUNTU_SERIES"; then
error "Build failed!"
exit 1
fi
# Find the changes file
CHANGES_FILE=$(find "$PARENT_DIR" -maxdepth 1 -name "${PACKAGE_NAME}_*_source.changes" -type f | sort -V | tail -1)
if [ -z "$CHANGES_FILE" ]; then
error "Changes file not found in $PARENT_DIR"
exit 1
fi
info "Found changes file: $CHANGES_FILE"
echo
# Step 2: Upload to PPA
info "Step 2: Uploading to PPA..."
# Check if using lftp (for all PPAs) or dput
if [ "$PPA_NAME" = "danklinux" ] || [ "$PPA_NAME" = "dms" ] || [ "$PPA_NAME" = "dms-git" ]; then
warn "Using lftp for upload"
# Extract version from changes file
VERSION=$(grep "^Version:" "$CHANGES_FILE" | awk '{print $2}')
SOURCE_NAME=$(grep "^Source:" "$CHANGES_FILE" | awk '{print $2}')
# Find all files to upload
BUILD_DIR=$(dirname "$CHANGES_FILE")
CHANGES_BASENAME=$(basename "$CHANGES_FILE")
DSC_FILE="${CHANGES_BASENAME/_source.changes/.dsc}"
TARBALL="${CHANGES_BASENAME/_source.changes/.tar.xz}"
BUILDINFO="${CHANGES_BASENAME/_source.changes/_source.buildinfo}"
# Check all files exist
MISSING_FILES=()
[ ! -f "$BUILD_DIR/$DSC_FILE" ] && MISSING_FILES+=("$DSC_FILE")
[ ! -f "$BUILD_DIR/$TARBALL" ] && MISSING_FILES+=("$TARBALL")
[ ! -f "$BUILD_DIR/$BUILDINFO" ] && MISSING_FILES+=("$BUILDINFO")
if [ ${#MISSING_FILES[@]} -gt 0 ]; then
error "Missing required files:"
for file in "${MISSING_FILES[@]}"; do
error " - $file"
done
exit 1
fi
info "Uploading files:"
info " - $CHANGES_BASENAME"
info " - $DSC_FILE"
info " - $TARBALL"
info " - $BUILDINFO"
echo
# lftp build dir change
LFTP_SCRIPT=$(mktemp)
cat > "$LFTP_SCRIPT" <<EOF
cd ~avengemedia/ubuntu/$PPA_NAME/
lcd $BUILD_DIR
mput $CHANGES_BASENAME
mput $DSC_FILE
mput $TARBALL
mput $BUILDINFO
bye
EOF
if lftp -d ftp://anonymous:@ppa.launchpad.net < "$LFTP_SCRIPT"; then
success "Upload successful!"
rm -f "$LFTP_SCRIPT"
else
error "Upload failed!"
rm -f "$LFTP_SCRIPT"
exit 1
fi
else
# Use dput for other PPAs
if [ ! -f "$UPLOAD_SCRIPT" ]; then
error "Upload script not found: $UPLOAD_SCRIPT"
exit 1
fi
# Auto-confirm upload (pipe 'y' to the confirmation prompt)
if ! echo "y" | "$UPLOAD_SCRIPT" "$CHANGES_FILE" "$PPA_NAME"; then
error "Upload failed!"
exit 1
fi
fi
echo
success "Package uploaded successfully!"
info "Monitor build progress at:"
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
echo
# Step 3: Cleanup (unless --keep-builds is specified)
if [ "$KEEP_BUILDS" = "false" ]; then
info "Step 3: Cleaning up build artifacts..."
# Find all build artifacts in parent directory
ARTIFACTS=(
"${PACKAGE_NAME}_*.dsc"
"${PACKAGE_NAME}_*.tar.xz"
"${PACKAGE_NAME}_*.tar.gz"
"${PACKAGE_NAME}_*_source.changes"
"${PACKAGE_NAME}_*_source.buildinfo"
"${PACKAGE_NAME}_*_source.build"
)
REMOVED=0
for pattern in "${ARTIFACTS[@]}"; do
for file in "$PARENT_DIR"/$pattern; do
if [ -f "$file" ]; then
rm -f "$file"
REMOVED=$((REMOVED + 1))
fi
done
done
# Clean up downloaded binaries in package directory
case "$PACKAGE_NAME" in
danksearch)
if [ -f "$PACKAGE_DIR/dsearch-amd64" ]; then
rm -f "$PACKAGE_DIR/dsearch-amd64"
REMOVED=$((REMOVED + 1))
fi
if [ -f "$PACKAGE_DIR/dsearch-arm64" ]; then
rm -f "$PACKAGE_DIR/dsearch-arm64"
REMOVED=$((REMOVED + 1))
fi
;;
dms)
# Remove downloaded binaries and source
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
REMOVED=$((REMOVED + 1))
fi
if [ -f "$PACKAGE_DIR/dms-source.tar.gz" ]; then
rm -f "$PACKAGE_DIR/dms-source.tar.gz"
REMOVED=$((REMOVED + 1))
fi
;;
dms-git)
# Remove downloaded binary
if [ -f "$PACKAGE_DIR/dms-distropkg-amd64.gz" ]; then
rm -f "$PACKAGE_DIR/dms-distropkg-amd64.gz"
REMOVED=$((REMOVED + 1))
fi
# Remove git source directory
if [ -d "$PACKAGE_DIR/dms-git-repo" ]; then
rm -rf "$PACKAGE_DIR/dms-git-repo"
REMOVED=$((REMOVED + 1))
fi
;;
esac
if [ $REMOVED -gt 0 ]; then
success "Removed $REMOVED build artifact(s)"
else
info "No build artifacts to clean up"
fi
else
info "Keeping build artifacts (--keep-builds specified)"
info "Build artifacts in: $PARENT_DIR"
fi
echo
success "Done!"
+566
View File
@@ -0,0 +1,566 @@
#!/bin/bash
# Generic source package builder for DMS PPA packages
# Usage: ./create-source.sh <package-dir> [ubuntu-series]
#
# Example:
# ./create-source.sh ../dms questing
# ./create-source.sh ../dms-git questing
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
if [ $# -lt 1 ]; then
error "Usage: $0 <package-dir> [ubuntu-series]"
echo
echo "Arguments:"
echo " package-dir : Path to package directory (e.g., ../dms)"
echo " ubuntu-series : Ubuntu series (optional, default: noble)"
echo " Options: noble, jammy, oracular, mantic"
echo
echo "Examples:"
echo " $0 ../dms questing"
echo " $0 ../dms-git questing"
exit 1
fi
PACKAGE_DIR="$1"
UBUNTU_SERIES="${2:-noble}"
# Validate package directory
if [ ! -d "$PACKAGE_DIR" ]; then
error "Package directory not found: $PACKAGE_DIR"
exit 1
fi
if [ ! -d "$PACKAGE_DIR/debian" ]; then
error "No debian/ directory found in $PACKAGE_DIR"
exit 1
fi
# Get absolute path
PACKAGE_DIR=$(cd "$PACKAGE_DIR" && pwd)
PACKAGE_NAME=$(basename "$PACKAGE_DIR")
info "Building source package for: $PACKAGE_NAME"
info "Package directory: $PACKAGE_DIR"
info "Target Ubuntu series: $UBUNTU_SERIES"
# Check for required files
REQUIRED_FILES=(
"debian/control"
"debian/rules"
"debian/changelog"
"debian/copyright"
"debian/source/format"
)
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$PACKAGE_DIR/$file" ]; then
error "Required file missing: $file"
exit 1
fi
done
# Verify GPG key is set up
info "Checking GPG key setup..."
if ! gpg --list-secret-keys &> /dev/null; then
error "No GPG secret keys found. Please set up GPG first!"
error "See GPG_SETUP.md for instructions"
exit 1
fi
success "GPG key found"
# Check if debuild is installed
if ! command -v debuild &> /dev/null; then
error "debuild not found. Install devscripts:"
error " sudo dnf install devscripts"
exit 1
fi
# Extract package info from changelog
cd "$PACKAGE_DIR"
CHANGELOG_VERSION=$(dpkg-parsechangelog -S Version)
SOURCE_NAME=$(dpkg-parsechangelog -S Source)
info "Source package: $SOURCE_NAME"
info "Version: $CHANGELOG_VERSION"
# Check if version targets correct Ubuntu series
CHANGELOG_SERIES=$(dpkg-parsechangelog -S Distribution)
if [ "$CHANGELOG_SERIES" != "$UBUNTU_SERIES" ] && [ "$CHANGELOG_SERIES" != "UNRELEASED" ]; then
warn "Changelog targets '$CHANGELOG_SERIES' but building for '$UBUNTU_SERIES'"
warn "Consider updating changelog with: dch -r '' -D $UBUNTU_SERIES"
fi
# Detect package type and update version automatically
cd "$PACKAGE_DIR"
# Function to get latest tag from GitHub
get_latest_tag() {
local repo="$1"
# Try GitHub API first (faster)
if command -v curl &> /dev/null; then
LATEST_TAG=$(curl -s "https://api.github.com/repos/$repo/releases/latest" 2>/dev/null | grep '"tag_name":' | sed 's/.*"tag_name": "\(.*\)".*/\1/' | head -1)
if [ -n "$LATEST_TAG" ]; then
echo "$LATEST_TAG" | sed 's/^v//'
return
fi
fi
# Fallback: clone and get latest tag
TEMP_REPO=$(mktemp -d)
if git clone --depth=1 --quiet "https://github.com/$repo.git" "$TEMP_REPO" 2>/dev/null; then
LATEST_TAG=$(cd "$TEMP_REPO" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "")
rm -rf "$TEMP_REPO"
echo "$LATEST_TAG"
fi
}
# Detect if package is git-based
IS_GIT_PACKAGE=false
GIT_REPO=""
SOURCE_DIR=""
# Check package name for -git suffix
if [[ "$PACKAGE_NAME" == *"-git" ]]; then
IS_GIT_PACKAGE=true
fi
# Check rules file for git clone patterns and extract repo
if grep -q "git clone" debian/rules 2>/dev/null; then
IS_GIT_PACKAGE=true
# Extract GitHub repo URL from rules
GIT_URL=$(grep -o "git clone.*https://github.com/[^/]*/[^/]*\.git" debian/rules 2>/dev/null | head -1 | sed 's/.*github\.com\///' | sed 's/\.git.*//' || echo "")
if [ -n "$GIT_URL" ]; then
GIT_REPO="$GIT_URL"
fi
fi
# Special handling for known packages
case "$PACKAGE_NAME" in
dms-git)
IS_GIT_PACKAGE=true
GIT_REPO="AvengeMedia/DankMaterialShell"
SOURCE_DIR="dms-git-repo"
;;
dms)
GIT_REPO="AvengeMedia/DankMaterialShell"
info "Downloading pre-built binaries and source for dms..."
# Get version from changelog (remove ppa suffix for both quilt and native formats)
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
# Download amd64 binary (will be included in source package)
if [ ! -f "dms-distropkg-amd64.gz" ]; then
info "Downloading dms binary for amd64..."
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-distropkg-amd64.gz"; then
success "amd64 binary downloaded"
else
error "Failed to download dms-distropkg-amd64.gz"
exit 1
fi
fi
# Download source tarball for QML files
if [ ! -f "dms-source.tar.gz" ]; then
info "Downloading dms source for QML files..."
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${VERSION}.tar.gz"; then
success "source tarball downloaded"
else
error "Failed to download dms-source.tar.gz"
exit 1
fi
fi
;;
danksearch)
# danksearch uses pre-built binary from releases, like dgop
GIT_REPO="AvengeMedia/danksearch"
;;
dgop)
# dgop uses pre-built binary from releases
GIT_REPO="AvengeMedia/dgop"
;;
esac
# Handle git packages
if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
info "Detected git package: $PACKAGE_NAME"
# Determine source directory name
if [ -z "$SOURCE_DIR" ]; then
# Default: use package name without -git suffix + -source or -repo
BASE_NAME=$(echo "$PACKAGE_NAME" | sed 's/-git$//')
if [ -d "${BASE_NAME}-source" ] 2>/dev/null; then
SOURCE_DIR="${BASE_NAME}-source"
elif [ -d "${BASE_NAME}-repo" ] 2>/dev/null; then
SOURCE_DIR="${BASE_NAME}-repo"
elif [ -d "$BASE_NAME" ] 2>/dev/null; then
SOURCE_DIR="$BASE_NAME"
else
SOURCE_DIR="${BASE_NAME}-source"
fi
fi
# Always clone fresh source to get latest commit info
info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
TEMP_CLONE=$(mktemp -d)
if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
# Get git commit info from fresh clone
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
# Get upstream version from latest git tag (e.g., 0.2.1)
# Sort all tags by version and get the latest one (not just the one reachable from HEAD)
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
if [ -z "$UPSTREAM_VERSION" ]; then
# Fallback: try without v prefix
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+' | sort -V | tail -1)
fi
if [ -z "$UPSTREAM_VERSION" ]; then
# Last resort: use git describe
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.1")
fi
# Verify we got valid commit info
if [ -z "$GIT_COMMIT_COUNT" ] || [ "$GIT_COMMIT_COUNT" = "0" ]; then
error "Failed to get commit count from $GIT_REPO"
rm -rf "$TEMP_CLONE"
exit 1
fi
if [ -z "$GIT_COMMIT_HASH" ]; then
error "Failed to get commit hash from $GIT_REPO"
rm -rf "$TEMP_CLONE"
exit 1
fi
success "Got commit info: $GIT_COMMIT_COUNT ($GIT_COMMIT_HASH), upstream: $UPSTREAM_VERSION"
# Update changelog with git commit info
info "Updating changelog with git commit info..."
# Format: 0.2.1+git705.fdbb86appa1
# Check if we're rebuilding the same commit (increment PPA number if so)
BASE_VERSION="${UPSTREAM_VERSION}+git${GIT_COMMIT_COUNT}.${GIT_COMMIT_HASH}"
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
PPA_NUM=1
# If current version matches the base version, increment PPA number
# Escape special regex characters in BASE_VERSION for pattern matching
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/+/\\+/g')
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
info "Detected rebuild of same commit (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
else
info "New commit or first build, using PPA number $PPA_NUM"
fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
# Use sed to update changelog (non-interactive, faster)
# Get current changelog content - find the next package header line (starts with package name)
# Skip the first entry entirely by finding the second occurrence of the package name at start of line
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
# Found second entry, use everything from there
CHANGELOG_CONTENT=$(tail -n +$OLD_ENTRY_START debian/changelog)
else
# No second entry found, changelog will only have new entry
CHANGELOG_CONTENT=""
fi
# Create new changelog entry with proper format
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* Git snapshot (commit ${GIT_COMMIT_COUNT}: ${GIT_COMMIT_HASH})
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
# Write new changelog (new entry, blank line, then old entries)
echo "$CHANGELOG_ENTRY" > debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >> debian/changelog
echo "$CHANGELOG_CONTENT" >> debian/changelog
fi
success "Version updated to $NEW_VERSION"
# Now clone to source directory (without .git for inclusion in package)
rm -rf "$SOURCE_DIR"
cp -r "$TEMP_CLONE" "$SOURCE_DIR"
rm -rf "$SOURCE_DIR/.git"
rm -rf "$TEMP_CLONE"
# Vendor Rust dependencies for packages that need it
if false; then
# No current packages need Rust vendoring
if [ -f "$SOURCE_DIR/Cargo.toml" ]; then
info "Vendoring Rust dependencies (Launchpad has no internet access)..."
cd "$SOURCE_DIR"
# Clean up any existing vendor directory and .orig files
# (prevents cargo from including .orig files in checksums)
rm -rf vendor .cargo
find . -type f -name "*.orig" -exec rm -f {} + || true
# Download all dependencies (crates.io + git repos) to vendor/
# cargo vendor outputs the config to stderr, capture it
mkdir -p .cargo
cargo vendor 2>&1 | awk '
/^\[source\.crates-io\]/ { printing=1 }
printing { print }
/^directory = "vendor"$/ { exit }
' > .cargo/config.toml
# Verify vendor directory was created
if [ ! -d "vendor" ]; then
error "Failed to vendor dependencies"
exit 1
fi
# Verify config was created
if [ ! -s .cargo/config.toml ]; then
error "Failed to create cargo config"
exit 1
fi
# CRITICAL: Remove ALL .orig files from vendor directory
# These break cargo checksums when dh_clean tries to use them
info "Cleaning .orig files from vendor directory..."
find vendor -type f -name "*.orig" -exec rm -fv {} + || true
find vendor -type f -name "*.rej" -exec rm -fv {} + || true
# Verify no .orig files remain
ORIG_COUNT=$(find vendor -type f -name "*.orig" | wc -l)
if [ "$ORIG_COUNT" -gt 0 ]; then
warn "Found $ORIG_COUNT .orig files still in vendor directory"
fi
success "Rust dependencies vendored (including git dependencies)"
cd "$PACKAGE_DIR"
fi
fi
# Download pre-built binary for dms-git
# dms-git uses latest release binary with git master QML files
if [ "$PACKAGE_NAME" = "dms-git" ]; then
info "Downloading latest release binary for dms-git..."
if [ ! -f "dms-distropkg-amd64.gz" ]; then
if wget -O dms-distropkg-amd64.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/latest/download/dms-distropkg-amd64.gz"; then
success "Latest release binary downloaded"
else
error "Failed to download dms-distropkg-amd64.gz"
exit 1
fi
else
info "Release binary already downloaded"
fi
fi
success "Source prepared for packaging"
else
error "Failed to clone $GIT_REPO"
rm -rf "$TEMP_CLONE"
exit 1
fi
# Handle stable packages - get latest tag
elif [ -n "$GIT_REPO" ]; then
info "Detected stable package: $PACKAGE_NAME"
info "Fetching latest tag from $GIT_REPO..."
LATEST_TAG=$(get_latest_tag "$GIT_REPO")
if [ -n "$LATEST_TAG" ]; then
# Check source format - native packages can't use dashes
SOURCE_FORMAT=$(cat debian/source/format 2>/dev/null | head -1 || echo "3.0 (quilt)")
# Get current version to check if we need to increment PPA number
CURRENT_VERSION=$(dpkg-parsechangelog -S Version 2>/dev/null || echo "")
PPA_NUM=1
if [[ "$SOURCE_FORMAT" == *"native"* ]]; then
# Native format: 0.2.1ppa1 (no dash, no revision)
BASE_VERSION="${LATEST_TAG}"
# Check if we're rebuilding the same version (increment PPA number if so)
if [[ "$CURRENT_VERSION" =~ ^${LATEST_TAG}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
else
info "New version or first build, using PPA number $PPA_NUM"
fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
else
# Quilt format: 0.2.1-1ppa1 (with revision)
BASE_VERSION="${LATEST_TAG}-1"
# Check if we're rebuilding the same version (increment PPA number if so)
ESCAPED_BASE=$(echo "$BASE_VERSION" | sed 's/\./\\./g' | sed 's/-/\\-/g')
if [[ "$CURRENT_VERSION" =~ ^${ESCAPED_BASE}ppa([0-9]+)$ ]]; then
PPA_NUM=$((BASH_REMATCH[1] + 1))
info "Detected rebuild of same version (current: $CURRENT_VERSION), incrementing PPA number to $PPA_NUM"
else
info "New version or first build, using PPA number $PPA_NUM"
fi
NEW_VERSION="${BASE_VERSION}ppa${PPA_NUM}"
fi
# Check if version needs updating (either new version or PPA number changed)
if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
if [ "$PPA_NUM" -gt 1 ]; then
info "Updating changelog for rebuild (PPA number incremented to $PPA_NUM)"
else
info "Updating changelog to latest tag: $LATEST_TAG"
fi
# Use sed to update changelog (non-interactive)
# Get current changelog content - find the next package header line
OLD_ENTRY_START=$(grep -n "^${SOURCE_NAME} (" debian/changelog | sed -n '2p' | cut -d: -f1)
if [ -n "$OLD_ENTRY_START" ]; then
CHANGELOG_CONTENT=$(tail -n +$OLD_ENTRY_START debian/changelog)
else
CHANGELOG_CONTENT=""
fi
# Create appropriate changelog message
if [ "$PPA_NUM" -gt 1 ]; then
CHANGELOG_MSG="Rebuild for packaging fixes (ppa${PPA_NUM})"
else
CHANGELOG_MSG="Upstream release ${LATEST_TAG}"
fi
CHANGELOG_ENTRY="${SOURCE_NAME} (${NEW_VERSION}) ${UBUNTU_SERIES}; urgency=medium
* ${CHANGELOG_MSG}
-- Avenge Media <AvengeMedia.US@gmail.com> $(date -R)"
echo "$CHANGELOG_ENTRY" > debian/changelog
if [ -n "$CHANGELOG_CONTENT" ]; then
echo "" >> debian/changelog
echo "$CHANGELOG_CONTENT" >> debian/changelog
fi
success "Version updated to $NEW_VERSION"
else
info "Version already at latest tag: $LATEST_TAG"
fi
else
warn "Could not determine latest tag for $GIT_REPO, using existing version"
fi
fi
# Handle packages that need pre-built binaries downloaded
cd "$PACKAGE_DIR"
case "$PACKAGE_NAME" in
danksearch)
info "Downloading pre-built binaries for danksearch..."
# Get version from changelog (remove ppa suffix for both quilt and native formats)
# Native: 0.5.2ppa1 -> 0.5.2, Quilt: 0.5.2-1ppa1 -> 0.5.2
VERSION=$(dpkg-parsechangelog -S Version | sed 's/-[^-]*$//' | sed 's/ppa[0-9]*$//')
# Download both amd64 and arm64 binaries (will be included in source package)
# Launchpad can't download during build, so we include both architectures
if [ ! -f "dsearch-amd64" ]; then
info "Downloading dsearch binary for amd64..."
if wget -O dsearch-amd64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-amd64.gz"; then
gunzip dsearch-amd64.gz
chmod +x dsearch-amd64
success "amd64 binary downloaded"
else
error "Failed to download dsearch-amd64.gz"
exit 1
fi
fi
if [ ! -f "dsearch-arm64" ]; then
info "Downloading dsearch binary for arm64..."
if wget -O dsearch-arm64.gz "https://github.com/AvengeMedia/danksearch/releases/download/v${VERSION}/dsearch-linux-arm64.gz"; then
gunzip dsearch-arm64.gz
chmod +x dsearch-arm64
success "arm64 binary downloaded"
else
error "Failed to download dsearch-arm64.gz"
exit 1
fi
fi
;;
dgop)
# dgop binary should already be committed in the repo
if [ ! -f "dgop" ]; then
warn "dgop binary not found - should be committed to repo"
fi
;;
esac
cd - > /dev/null
# Check if this version already exists on PPA (only in CI environment)
if command -v rmadison >/dev/null 2>&1; then
info "Checking if version already exists on PPA..."
PPA_VERSION_CHECK=$(rmadison -u ppa:avengemedia/dms "$PACKAGE_NAME" 2>/dev/null | grep "$VERSION" || true)
if [ -n "$PPA_VERSION_CHECK" ]; then
warn "Version $VERSION already exists on PPA:"
echo "$PPA_VERSION_CHECK"
echo
warn "Skipping upload to avoid duplicate. If this is a rebuild, increment the ppa number."
cd "$PACKAGE_DIR"
# Still clean up extracted sources
case "$PACKAGE_NAME" in
dms-git)
rm -rf DankMaterialShell-*
success "Cleaned up DankMaterialShell-*/ directory"
;;
esac
exit 0
fi
fi
# Build source package
info "Building source package..."
echo
# Determine if we need to include orig tarball (-sa) or just debian changes (-sd)
# Check if .orig.tar.xz already exists in parent directory (previous build)
ORIG_TARBALL="${PACKAGE_NAME}_${VERSION%.ppa*}.orig.tar.xz"
if [ -f "../$ORIG_TARBALL" ]; then
info "Found existing orig tarball, using -sd (debian changes only)"
DEBUILD_SOURCE_FLAG="-sd"
else
info "No existing orig tarball found, using -sa (include original source)"
DEBUILD_SOURCE_FLAG="-sa"
fi
# Use -S for source only, -sa/-sd for source inclusion
# -d skips dependency checking (we're building on Fedora, not Ubuntu)
# Pipe yes to automatically answer prompts (e.g., "continue anyway?")
if yes | DEBIAN_FRONTEND=noninteractive debuild -S $DEBUILD_SOURCE_FLAG -d; then
echo
success "Source package built successfully!"
# List generated files
info "Generated files in $(dirname "$PACKAGE_DIR"):"
ls -lh "$(dirname "$PACKAGE_DIR")"/${SOURCE_NAME}_${CHANGELOG_VERSION}* 2>/dev/null || true
# Show what to do next
echo
info "Next steps:"
echo " 1. Review the source package:"
echo " cd $(dirname "$PACKAGE_DIR")"
echo " ls -lh ${SOURCE_NAME}_${CHANGELOG_VERSION}*"
echo
echo " 2. Upload to PPA (stable):"
echo " dput ppa:avengemedia/dms ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
echo
echo " 3. Or upload to PPA (nightly):"
echo " dput ppa:avengemedia/dms-git ${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes"
echo
echo " 4. Or use the upload script:"
echo " ./upload-ppa.sh $(dirname "$PACKAGE_DIR")/${SOURCE_NAME}_${CHANGELOG_VERSION}_source.changes dms"
else
error "Source package build failed!"
exit 1
fi
+179
View File
@@ -0,0 +1,179 @@
#!/bin/bash
# Generic PPA uploader for DMS packages
# Usage: ./upload-ppa.sh <changes-file> <ppa-name>
#
# Example:
# ./upload-ppa.sh ../dms_0.5.2ppa1_source.changes dms
# ./upload-ppa.sh ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git
set -e
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[SUCCESS]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
error() { echo -e "${RED}[ERROR]${NC} $1"; }
if [ $# -lt 2 ]; then
error "Usage: $0 <changes-file> <ppa-name>"
echo
echo "Arguments:"
echo " changes-file : Path to .changes file (e.g., ../dms_0.5.2ppa1_source.changes)"
echo " ppa-name : PPA to upload to (dms or dms-git)"
echo
echo "Examples:"
echo " $0 ../dms_0.5.2ppa1_source.changes dms"
echo " $0 ../dms_0.5.2+git705.fdbb86appa1_source.changes dms-git"
exit 1
fi
CHANGES_FILE="$1"
PPA_NAME="$2"
# Validate changes file
if [ ! -f "$CHANGES_FILE" ]; then
error "Changes file not found: $CHANGES_FILE"
exit 1
fi
if [[ ! "$CHANGES_FILE" =~ \.changes$ ]]; then
error "File must be a .changes file"
exit 1
fi
# Validate PPA name
if [ "$PPA_NAME" != "dms" ] && [ "$PPA_NAME" != "dms-git" ] && [ "$PPA_NAME" != "danklinux" ]; then
error "PPA name must be 'dms', 'dms-git', or 'danklinux'"
exit 1
fi
# Get absolute path
CHANGES_FILE=$(realpath "$CHANGES_FILE")
info "Uploading to PPA: ppa:avengemedia/$PPA_NAME"
info "Changes file: $CHANGES_FILE"
# Check if dput or lftp is installed
UPLOAD_METHOD=""
if command -v dput &> /dev/null; then
UPLOAD_METHOD="dput"
elif command -v lftp &> /dev/null; then
UPLOAD_METHOD="lftp"
warn "dput not found, using lftp as fallback"
else
error "Neither dput nor lftp found. Install one with:"
error " sudo dnf install dput-ng # Preferred but broken on Fedora"
error " sudo dnf install lftp # Alternative upload method"
exit 1
fi
# Check if ~/.dput.cf exists
if [ ! -f "$HOME/.dput.cf" ]; then
error "~/.dput.cf not found!"
echo
info "Create it from template:"
echo " cp $(dirname "$0")/../dput.cf.template ~/.dput.cf"
echo
info "Or create it manually with:"
cat <<'EOF'
[ppa:avengemedia/dms]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms/
login = anonymous
allow_unsigned_uploads = 0
[ppa:avengemedia/dms-git]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/dms-git/
login = anonymous
allow_unsigned_uploads = 0
EOF
exit 1
fi
# Check if PPA is configured in dput.cf
if ! grep -q "^\[ppa:avengemedia/$PPA_NAME\]" "$HOME/.dput.cf"; then
error "PPA 'ppa:avengemedia/$PPA_NAME' not found in ~/.dput.cf"
echo
info "Add this to ~/.dput.cf:"
cat <<EOF
[ppa:avengemedia/$PPA_NAME]
fqdn = ppa.launchpad.net
method = ftp
incoming = ~avengemedia/ubuntu/$PPA_NAME/
login = anonymous
allow_unsigned_uploads = 0
EOF
exit 1
fi
# Extract package info from changes file
PACKAGE_NAME=$(grep "^Source:" "$CHANGES_FILE" | awk '{print $2}')
VERSION=$(grep "^Version:" "$CHANGES_FILE" | awk '{print $2}')
info "Package: $PACKAGE_NAME"
info "Version: $VERSION"
# Show files that will be uploaded
echo
info "Files to be uploaded:"
grep "^ [a-f0-9]" "$CHANGES_FILE" | awk '{print " - " $5}' || true
# Verify GPG signature
info "Verifying GPG signature..."
if gpg --verify "$CHANGES_FILE" 2>/dev/null; then
success "GPG signature valid"
else
error "GPG signature verification failed!"
error "The .changes file must be signed with your GPG key"
exit 1
fi
# Ask for confirmation
echo
warn "About to upload to: ppa:avengemedia/$PPA_NAME"
read -p "Continue? (y/N) " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
info "Upload cancelled"
exit 0
fi
# Upload to PPA
info "Uploading to Launchpad..."
echo
if dput "ppa:avengemedia/$PPA_NAME" "$CHANGES_FILE"; then
echo
success "Upload successful!"
echo
info "Monitor build progress at:"
echo " https://launchpad.net/~avengemedia/+archive/ubuntu/$PPA_NAME/+packages"
echo
info "Builds typically take 5-30 minutes depending on:"
echo " - Build queue length"
echo " - Package complexity"
echo " - Number of target Ubuntu series"
echo
info "Once built, users can install with:"
echo " sudo add-apt-repository ppa:avengemedia/$PPA_NAME"
echo " sudo apt update"
echo " sudo apt install $PACKAGE_NAME"
else
error "Upload failed!"
echo
info "Common issues:"
echo " - GPG key not verified on Launchpad (check https://launchpad.net/~/+editpgpkeys)"
echo " - Version already uploaded (must increment version number)"
echo " - Network/firewall blocking FTP (try HTTPS method in dput.cf)"
echo " - Email in changelog doesn't match GPG key email"
exit 1
fi
+77 -65
View File
@@ -9,155 +9,167 @@ Singleton {
property var currentPopoutsByScreen: ({}) property var currentPopoutsByScreen: ({})
property var currentPopoutTriggers: ({}) property var currentPopoutTriggers: ({})
function showPopout(popout) { signal popoutOpening
if (!popout || !popout.screen) return signal popoutChanged
const screenName = popout.screen.name function showPopout(popout) {
if (!popout || !popout.screen)
return;
popoutOpening();
const screenName = popout.screen.name;
for (const otherScreenName in currentPopoutsByScreen) { for (const otherScreenName in currentPopoutsByScreen) {
const otherPopout = currentPopoutsByScreen[otherScreenName] const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout || otherPopout === popout) continue if (!otherPopout || otherPopout === popout)
continue;
if (otherPopout.dashVisible !== undefined) { if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false otherPopout.dashVisible = false;
} else if (otherPopout.notificationHistoryVisible !== undefined) { } else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false otherPopout.notificationHistoryVisible = false;
} else { } else {
otherPopout.close() otherPopout.close();
} }
} }
currentPopoutsByScreen[screenName] = popout currentPopoutsByScreen[screenName] = popout;
ModalManager.closeAllModalsExcept(null) popoutChanged();
TrayMenuManager.closeAllMenus() ModalManager.closeAllModalsExcept(null);
} }
function hidePopout(popout) { function hidePopout(popout) {
if (!popout || !popout.screen) return if (!popout || !popout.screen)
return;
const screenName = popout.screen.name const screenName = popout.screen.name;
if (currentPopoutsByScreen[screenName] === popout) { if (currentPopoutsByScreen[screenName] === popout) {
currentPopoutsByScreen[screenName] = null currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null currentPopoutTriggers[screenName] = null;
popoutChanged();
} }
} }
function closeAllPopouts() { function closeAllPopouts() {
for (const screenName in currentPopoutsByScreen) { for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName] const popout = currentPopoutsByScreen[screenName];
if (!popout) continue if (!popout)
continue;
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = false popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) { } else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false popout.notificationHistoryVisible = false;
} else { } else {
popout.close() popout.close();
} }
} }
currentPopoutsByScreen = {} currentPopoutsByScreen = {};
} }
function getActivePopout(screen) { function getActivePopout(screen) {
if (!screen) return null if (!screen)
return currentPopoutsByScreen[screen.name] || null return null;
return currentPopoutsByScreen[screen.name] || null;
} }
function requestPopout(popout, tabIndex, triggerSource) { function requestPopout(popout, tabIndex, triggerSource) {
if (!popout || !popout.screen) return if (!popout || !popout.screen)
return;
const screenName = popout.screen.name;
const currentPopout = currentPopoutsByScreen[screenName];
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex;
const screenName = popout.screen.name const willOpen = !(currentPopout === popout && popout.shouldBeVisible && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
const currentPopout = currentPopoutsByScreen[screenName] if (willOpen) {
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex popoutOpening();
}
let justClosedSamePopout = false let justClosedSamePopout = false;
for (const otherScreenName in currentPopoutsByScreen) { for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName) continue if (otherScreenName === screenName)
const otherPopout = currentPopoutsByScreen[otherScreenName] continue;
if (!otherPopout) continue const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (otherPopout === popout) { if (otherPopout === popout) {
justClosedSamePopout = true justClosedSamePopout = true;
} }
if (otherPopout.dashVisible !== undefined) { if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false otherPopout.dashVisible = false;
} else if (otherPopout.notificationHistoryVisible !== undefined) { } else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false otherPopout.notificationHistoryVisible = false;
} else { } else {
otherPopout.close() otherPopout.close();
} }
} }
if (currentPopout && currentPopout !== popout) { if (currentPopout && currentPopout !== popout) {
if (currentPopout.dashVisible !== undefined) { if (currentPopout.dashVisible !== undefined) {
currentPopout.dashVisible = false currentPopout.dashVisible = false;
} else if (currentPopout.notificationHistoryVisible !== undefined) { } else if (currentPopout.notificationHistoryVisible !== undefined) {
currentPopout.notificationHistoryVisible = false currentPopout.notificationHistoryVisible = false;
} else { } else {
currentPopout.close() currentPopout.close();
} }
} }
if (currentPopout === popout && popout.shouldBeVisible) { if (currentPopout === popout && popout.shouldBeVisible) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) { if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = false popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) { } else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false popout.notificationHistoryVisible = false;
} else { } else {
popout.close() popout.close();
} }
return return;
} }
if (triggerId === undefined) { if (triggerId === undefined) {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = false popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) { } else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false popout.notificationHistoryVisible = false;
} else { } else {
popout.close() popout.close();
} }
return return;
} }
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) { if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex popout.currentTabIndex = tabIndex;
} }
currentPopoutTriggers[screenName] = triggerId currentPopoutTriggers[screenName] = triggerId;
return
} }
currentPopoutTriggers[screenName] = triggerId currentPopoutTriggers[screenName] = triggerId;
currentPopoutsByScreen[screenName] = popout currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) { if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex popout.currentTabIndex = tabIndex;
} }
if (currentPopout !== popout) { if (currentPopout !== popout) {
ModalManager.closeAllModalsExcept(null) ModalManager.closeAllModalsExcept(null);
} }
TrayMenuManager.closeAllMenus()
if (justClosedSamePopout) { if (justClosedSamePopout) {
Qt.callLater(() => { Qt.callLater(() => {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = true popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) { } else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true popout.notificationHistoryVisible = true;
} else { } else {
popout.open() popout.open();
} }
}) });
} else { } else {
if (popout.dashVisible !== undefined) { if (popout.dashVisible !== undefined) {
popout.dashVisible = true popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) { } else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true popout.notificationHistoryVisible = true;
} else { } else {
popout.open() popout.open();
} }
} }
} }
+18 -4
View File
@@ -67,13 +67,21 @@ Singleton {
}) })
out.streamFinished.connect(function() { out.streamFinished.connect(function() {
capturedOut = out.text || "" try {
capturedOut = out.text || ""
} catch (e) {
capturedOut = ""
}
outSeen = true outSeen = true
maybeComplete() maybeComplete()
}) })
err.streamFinished.connect(function() { err.streamFinished.connect(function() {
capturedErr = err.text || "" try {
capturedErr = err.text || ""
} catch (e) {
capturedErr = ""
}
errSeen = true errSeen = true
maybeComplete() maybeComplete()
}) })
@@ -88,8 +96,14 @@ Singleton {
function maybeComplete() { function maybeComplete() {
if (!exitSeen || !outSeen || !errSeen) return if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop() timeoutTimer.stop()
if (typeof entry.callback === "function") { if (entry && entry.callback && typeof entry.callback === "function") {
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) } try {
const safeOutput = capturedOut !== null && capturedOut !== undefined ? capturedOut : ""
const safeExitCode = exitCodeValue !== null && exitCodeValue !== undefined ? exitCodeValue : -1
entry.callback(safeOutput, safeExitCode)
} catch (e) {
console.warn("runCommand callback error for command:", entry.command, "Error:", e)
}
} }
try { proc.destroy() } catch (_) {} try { proc.destroy() } catch (_) {}
try { timeoutTimer.destroy() } catch (_) {} try { timeoutTimer.destroy() } catch (_) {}
File diff suppressed because it is too large Load Diff
+6 -6
View File
@@ -2,16 +2,16 @@
// Separated from Theme.qml to keep that file clean // Separated from Theme.qml to keep that file clean
const CatppuccinMocha = { const CatppuccinMocha = {
surface: "#313244", surface: "#181825",
surfaceText: "#cdd6f4", surfaceText: "#cdd6f4",
surfaceVariant: "#313244", surfaceVariant: "#1e1e2e",
surfaceVariantText: "#a6adc8", surfaceVariantText: "#a6adc8",
background: "#1e1e2e", background: "#181825",
backgroundText: "#cdd6f4", backgroundText: "#cdd6f4",
outline: "#6c7086", outline: "#6c7086",
surfaceContainer: "#45475a", surfaceContainer: "#1e1e2e",
surfaceContainerHigh: "#585b70", surfaceContainerHigh: "#313244",
surfaceContainerHighest: "#6c7086" surfaceContainerHighest: "#45475a"
} }
const CatppuccinLatte = { const CatppuccinLatte = {
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -6,26 +6,30 @@ import QtQuick
Singleton { Singleton {
id: root id: root
property var activeTrayBars: ({}) property var activeTrayMenus: ({})
function register(screenName, trayBar) { function registerMenu(screenName, menu) {
if (!screenName || !trayBar) return if (!screenName || !menu) return
activeTrayBars[screenName] = trayBar const newMenus = Object.assign({}, activeTrayMenus)
newMenus[screenName] = menu
activeTrayMenus = newMenus
} }
function unregister(screenName) { function unregisterMenu(screenName) {
if (!screenName) return if (!screenName) return
delete activeTrayBars[screenName] const newMenus = Object.assign({}, activeTrayMenus)
delete newMenus[screenName]
activeTrayMenus = newMenus
} }
function closeAllMenus() { function closeAllMenus() {
for (const screenName in activeTrayBars) { for (const screenName in activeTrayMenus) {
const trayBar = activeTrayBars[screenName] const menu = activeTrayMenus[screenName]
if (!trayBar) continue if (!menu) continue
if (typeof menu.close === "function") {
trayBar.menuOpen = false menu.close()
if (trayBar.currentTrayMenu) { } else if (menu.showMenu !== undefined) {
trayBar.currentTrayMenu.showMenu = false menu.showMenu = false
} }
} }
} }
+37 -31
View File
@@ -12,12 +12,11 @@ var SPEC = {
runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" }, runUserMatugenTemplates: { def: true, onChange: "regenSystemThemes" },
matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" }, matugenTargetMonitor: { def: "", onChange: "regenSystemThemes" },
dankBarTransparency: { def: 1.0, coerce: percentToUnit, migrate: ["topBarTransparency"] },
dankBarWidgetTransparency: { def: 1.0, coerce: percentToUnit, migrate: ["topBarWidgetTransparency"] },
popupTransparency: { def: 1.0, coerce: percentToUnit }, popupTransparency: { def: 1.0, coerce: percentToUnit },
dockTransparency: { def: 1.0, coerce: percentToUnit }, dockTransparency: { def: 1.0, coerce: percentToUnit },
widgetBackgroundColor: { def: "sch" }, widgetBackgroundColor: { def: "sch" },
widgetColorMode: { def: "default" },
cornerRadius: { def: 12, onChange: "updateNiriLayout" }, cornerRadius: { def: 12, onChange: "updateNiriLayout" },
use24HourClock: { def: true }, use24HourClock: { def: true },
@@ -88,15 +87,11 @@ var SPEC = {
lockDateFormat: { def: "" }, lockDateFormat: { def: "" },
mediaSize: { def: 1 }, mediaSize: { def: 1 },
dankBarLeftWidgets: { def: ["launcherButton", "workspaceSwitcher", "focusedWindow"], migrate: ["topBarLeftWidgets"] },
dankBarCenterWidgets: { def: ["music", "clock", "weather"], migrate: ["topBarCenterWidgets"] },
dankBarRightWidgets: { def: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"], migrate: ["topBarRightWidgets"] },
dankBarWidgetOrder: { def: [] },
appLauncherViewMode: { def: "list" }, appLauncherViewMode: { def: "list" },
spotlightModalViewMode: { def: "list" }, spotlightModalViewMode: { def: "list" },
sortAppsAlphabetically: { def: false }, sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 }, appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
weatherLocation: { def: "New York, NY" }, weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" }, weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -125,7 +120,6 @@ var SPEC = {
monoFontFamily: { def: "Fira Code" }, monoFontFamily: { def: "Fira Code" },
fontWeight: { def: 400 }, fontWeight: { def: 400 },
fontScale: { def: 1.0 }, fontScale: { def: 1.0 },
dankBarFontScale: { def: 1.0 },
notepadUseMonospace: { def: true }, notepadUseMonospace: { def: true },
notepadFontFamily: { def: "" }, notepadFontFamily: { def: "" },
@@ -175,31 +169,9 @@ var SPEC = {
dockIndicatorStyle: { def: "circle" }, dockIndicatorStyle: { def: "circle" },
notificationOverlayEnabled: { def: false }, notificationOverlayEnabled: { def: false },
dankBarAutoHide: { def: false, migrate: ["topBarAutoHide"] },
dankBarAutoHideDelay: { def: 250 },
dankBarOpenOnOverview: { def: false, migrate: ["topBarOpenOnOverview"] },
dankBarVisible: { def: true, migrate: ["topBarVisible"] },
overviewRows: { def: 2, persist: false }, overviewRows: { def: 2, persist: false },
overviewColumns: { def: 5, persist: false }, overviewColumns: { def: 5, persist: false },
overviewScale: { def: 0.16, persist: false }, overviewScale: { def: 0.16, persist: false },
dankBarSpacing: { def: 4, migrate: ["topBarSpacing"], onChange: "updateNiriLayout" },
dankBarBottomGap: { def: 0, migrate: ["topBarBottomGap"] },
dankBarInnerPadding: { def: 4, migrate: ["topBarInnerPadding"] },
dankBarPosition: { def: 0, migrate: ["dankBarAtBottom", "topBarAtBottom"] },
dankBarIsVertical: { def: false, persist: false },
dankBarSquareCorners: { def: false, migrate: ["topBarSquareCorners"] },
dankBarNoBackground: { def: false, migrate: ["topBarNoBackground"] },
dankBarGothCornersEnabled: { def: false, migrate: ["topBarGothCornersEnabled"] },
dankBarGothCornerRadiusOverride: { def: false },
dankBarGothCornerRadiusValue: { def: 12 },
dankBarBorderEnabled: { def: false },
dankBarBorderColor: { def: "surfaceText" },
dankBarBorderOpacity: { def: 1.0 },
dankBarBorderThickness: { def: 1 },
popupGapsAuto: { def: true },
popupGapsManual: { def: 4 },
modalDarkenBackground: { def: true }, modalDarkenBackground: { def: true },
@@ -217,6 +189,7 @@ var SPEC = {
osdAlwaysShowValue: { def: false }, osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 }, osdPosition: { def: 5 },
osdVolumeEnabled: { def: true }, osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled : { def: true },
osdBrightnessEnabled: { def: true }, osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true }, osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true }, osdMicMuteEnabled: { def: true },
@@ -240,7 +213,40 @@ var SPEC = {
displayNameMode: { def: "system" }, displayNameMode: { def: "system" },
screenPreferences: { def: {} }, screenPreferences: { def: {} },
showOnLastDisplay: { def: {} } showOnLastDisplay: { def: {} },
barConfigs: { def: [{
id: "default",
name: "Main Bar",
enabled: true,
position: 0,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: ["music", "clock", "weather"],
rightWidgets: ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: 4,
innerPadding: 4,
bottomGap: 0,
transparency: 1.0,
widgetTransparency: 1.0,
squareCorners: false,
noBackground: false,
gothCornersEnabled: false,
gothCornerRadiusOverride: false,
gothCornerRadiusValue: 12,
borderEnabled: false,
borderColor: "surfaceText",
borderOpacity: 1.0,
borderThickness: 1,
fontScale: 1.0,
autoHide: false,
autoHideDelay: 250,
openOnOverview: false,
visible: true,
popupGapsAuto: true,
popupGapsManual: 4
}], onChange: "updateBarConfigs" }
}; };
function getValidKeys() { function getValidKeys() {
+75 -53
View File
@@ -5,6 +5,7 @@
function parse(root, jsonObj) { function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC; var SPEC = SpecModule.SPEC;
for (var k in SPEC) { for (var k in SPEC) {
if (k === "pluginSettings") continue;
var spec = SPEC[k]; var spec = SPEC[k];
root[k] = spec.def; root[k] = spec.def;
} }
@@ -13,6 +14,7 @@ function parse(root, jsonObj) {
for (var k in jsonObj) { for (var k in jsonObj) {
if (!SPEC[k]) continue; if (!SPEC[k]) continue;
if (k === "pluginSettings") continue;
var raw = jsonObj[k]; var raw = jsonObj[k];
var spec = SPEC[k]; var spec = SPEC[k];
var coerce = spec.coerce; var coerce = spec.coerce;
@@ -25,72 +27,93 @@ function toJson(root) {
var out = {}; var out = {};
for (var k in SPEC) { for (var k in SPEC) {
if (SPEC[k].persist === false) continue; if (SPEC[k].persist === false) continue;
if (k === "pluginSettings") continue;
out[k] = root[k]; out[k] = root[k];
} }
out.configVersion = root.settingsConfigVersion; out.configVersion = root.settingsConfigVersion;
return out; return out;
} }
function migrate(root, jsonObj) { function migrateToVersion(obj, targetVersion) {
var SPEC = SpecModule.SPEC; if (!obj) return null;
if (!jsonObj) return;
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) { var settings = JSON.parse(JSON.stringify(obj));
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"]; var currentVersion = settings.configVersion || 0;
if (jsonObj.themeIsDynamic) {
root.currentThemeName = "dynamic"; if (currentVersion >= targetVersion) {
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) { return null;
root.currentThemeName = themeNames[jsonObj.themeIndex];
}
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
} }
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) || if (currentVersion < 2) {
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) { console.info("Migrating settings from version", currentVersion, "to version 2");
if (jsonObj.dankBarLeftWidgets === undefined && jsonObj.dankBarCenterWidgets === undefined && jsonObj.dankBarRightWidgets === undefined) {
var widgetOrder = jsonObj.dankBarWidgetOrder || jsonObj.topBarWidgetOrder;
root.dankBarLeftWidgets = widgetOrder.filter(function(w) { return ["launcherButton", "workspaceSwitcher", "focusedWindow"].indexOf(w) >= 0; });
root.dankBarCenterWidgets = widgetOrder.filter(function(w) { return ["clock", "music", "weather"].indexOf(w) >= 0; });
root.dankBarRightWidgets = widgetOrder.filter(function(w) { return ["systemTray", "clipboard", "systemResources", "notificationButton", "battery", "controlCenterButton"].indexOf(w) >= 0; });
}
}
if (jsonObj.useOSLogo !== undefined) { if (settings.barConfigs === undefined) {
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps"; var position = 0;
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : ""; if (settings.dankBarAtBottom !== undefined || settings.topBarAtBottom !== undefined) {
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5; var atBottom = settings.dankBarAtBottom !== undefined ? settings.dankBarAtBottom : settings.topBarAtBottom;
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1; position = atBottom ? 1 : 0;
} } else if (settings.dankBarPosition !== undefined) {
position = settings.dankBarPosition;
if (jsonObj.mediaCompactMode !== undefined && jsonObj.mediaSize === undefined) {
root.mediaSize = jsonObj.mediaCompactMode ? 0 : 1;
}
for (var k in SPEC) {
var spec = SPEC[k];
if (!spec.migrate) continue;
for (var i = 0; i < spec.migrate.length; i++) {
var oldKey = spec.migrate[i];
if (jsonObj[oldKey] !== undefined && jsonObj[k] === undefined) {
var raw = jsonObj[oldKey];
var coerce = spec.coerce;
root[k] = coerce ? (coerce(raw) !== undefined ? coerce(raw) : root[k]) : raw;
break;
} }
var defaultConfig = {
id: "default",
name: "Main Bar",
enabled: true,
position: position,
screenPreferences: ["all"],
showOnLastDisplay: true,
leftWidgets: settings.dankBarLeftWidgets || ["launcherButton", "workspaceSwitcher", "focusedWindow"],
centerWidgets: settings.dankBarCenterWidgets || ["music", "clock", "weather"],
rightWidgets: settings.dankBarRightWidgets || ["systemTray", "clipboard", "cpuUsage", "memUsage", "notificationButton", "battery", "controlCenterButton"],
spacing: settings.dankBarSpacing !== undefined ? settings.dankBarSpacing : 4,
innerPadding: settings.dankBarInnerPadding !== undefined ? settings.dankBarInnerPadding : 4,
bottomGap: settings.dankBarBottomGap !== undefined ? settings.dankBarBottomGap : 0,
transparency: settings.dankBarTransparency !== undefined ? settings.dankBarTransparency : 1.0,
widgetTransparency: settings.dankBarWidgetTransparency !== undefined ? settings.dankBarWidgetTransparency : 1.0,
squareCorners: settings.dankBarSquareCorners !== undefined ? settings.dankBarSquareCorners : false,
noBackground: settings.dankBarNoBackground !== undefined ? settings.dankBarNoBackground : false,
gothCornersEnabled: settings.dankBarGothCornersEnabled !== undefined ? settings.dankBarGothCornersEnabled : false,
gothCornerRadiusOverride: settings.dankBarGothCornerRadiusOverride !== undefined ? settings.dankBarGothCornerRadiusOverride : false,
gothCornerRadiusValue: settings.dankBarGothCornerRadiusValue !== undefined ? settings.dankBarGothCornerRadiusValue : 12,
borderEnabled: settings.dankBarBorderEnabled !== undefined ? settings.dankBarBorderEnabled : false,
borderColor: settings.dankBarBorderColor || "surfaceText",
borderOpacity: settings.dankBarBorderOpacity !== undefined ? settings.dankBarBorderOpacity : 1.0,
borderThickness: settings.dankBarBorderThickness !== undefined ? settings.dankBarBorderThickness : 1,
fontScale: settings.dankBarFontScale !== undefined ? settings.dankBarFontScale : 1.0,
autoHide: settings.dankBarAutoHide !== undefined ? settings.dankBarAutoHide : false,
autoHideDelay: settings.dankBarAutoHideDelay !== undefined ? settings.dankBarAutoHideDelay : 250,
openOnOverview: settings.dankBarOpenOnOverview !== undefined ? settings.dankBarOpenOnOverview : false,
visible: settings.dankBarVisible !== undefined ? settings.dankBarVisible : true,
popupGapsAuto: settings.popupGapsAuto !== undefined ? settings.popupGapsAuto : true,
popupGapsManual: settings.popupGapsManual !== undefined ? settings.popupGapsManual : 4
};
settings.barConfigs = [defaultConfig];
var legacyKeys = [
"dankBarLeftWidgets", "dankBarCenterWidgets", "dankBarRightWidgets",
"dankBarWidgetOrder", "dankBarAutoHide", "dankBarAutoHideDelay",
"dankBarOpenOnOverview", "dankBarVisible", "dankBarSpacing",
"dankBarBottomGap", "dankBarInnerPadding", "dankBarPosition",
"dankBarSquareCorners", "dankBarNoBackground", "dankBarGothCornersEnabled",
"dankBarGothCornerRadiusOverride", "dankBarGothCornerRadiusValue",
"dankBarBorderEnabled", "dankBarBorderColor", "dankBarBorderOpacity",
"dankBarBorderThickness", "popupGapsAuto", "popupGapsManual",
"dankBarAtBottom", "topBarAtBottom", "dankBarTransparency", "dankBarWidgetTransparency"
];
for (var i = 0; i < legacyKeys.length; i++) {
delete settings[legacyKeys[i]];
}
console.info("Migrated single bar settings to barConfigs");
} }
settings.configVersion = 2;
} }
if (jsonObj.dankBarAtBottom !== undefined || jsonObj.topBarAtBottom !== undefined) { return settings;
var atBottom = jsonObj.dankBarAtBottom !== undefined ? jsonObj.dankBarAtBottom : jsonObj.topBarAtBottom;
root.dankBarPosition = atBottom ? 1 : 0;
}
if (jsonObj.pluginSettings !== undefined) {
root.pluginSettings = jsonObj.pluginSettings;
return true;
}
return false;
} }
function cleanup(fileText) { function cleanup(fileText) {
@@ -104,7 +127,6 @@ function cleanup(fileText) {
for (var key in settings) { for (var key in settings) {
if (validKeys.indexOf(key) < 0) { if (validKeys.indexOf(key) < 0) {
console.log("SettingsData: Removing unused key:", key);
delete settings[key]; delete settings[key];
needsSave = true; needsSave = true;
} }
+39 -29
View File
@@ -22,7 +22,7 @@ import qs.Modules.ProcessList
import qs.Modules.Settings import qs.Modules.Settings
import qs.Modules.DankBar import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts import qs.Modules.DankBar.Popouts
import qs.Modules.HyprWorkspaces import qs.Modules.WorkspaceOverlays
import qs.Modules.Plugins import qs.Modules.Plugins
import qs.Services import qs.Services
@@ -66,38 +66,32 @@ Item {
id: lock id: lock
} }
Loader { Repeater {
id: dankBarLoader id: dankBarRepeater
asynchronous: false model: ScriptModel {
values: SettingsData.barConfigs
}
property var currentPosition: SettingsData.dankBarPosition
property bool initialized: false
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
sourceComponent: DankBar { delegate: Loader {
hyprlandOverviewLoader: dankBarLoader.hyprlandOverviewLoaderRef id: barLoader
active: modelData.enabled
asynchronous: false
onColorPickerRequested: { sourceComponent: DankBar {
if (colorPickerModal.shouldBeVisible) { barConfig: modelData
colorPickerModal.close() hyprlandOverviewLoader: dankBarRepeater.hyprlandOverviewLoaderRef
} else {
colorPickerModal.show() onColorPickerRequested: {
if (colorPickerModal.shouldBeVisible) {
colorPickerModal.close()
} else {
colorPickerModal.show()
}
} }
} }
} }
Component.onCompleted: {
initialized = true
}
onCurrentPositionChanged: {
if (!initialized)
return
const component = sourceComponent
sourceComponent = null
sourceComponent = component
}
} }
Loader { Loader {
@@ -126,7 +120,6 @@ Item {
if (!initialized) if (!initialized)
return return
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
const comp = sourceComponent const comp = sourceComponent
sourceComponent = null sourceComponent = null
sourceComponent = comp sourceComponent = comp
@@ -137,7 +130,7 @@ Item {
id: dankDashPopoutLoader id: dankDashPopoutLoader
active: false active: false
asynchronous: true asynchronous: false
sourceComponent: Component { sourceComponent: Component {
DankDashPopout { DankDashPopout {
@@ -513,8 +506,9 @@ Item {
dankDashPopoutLoader: dankDashPopoutLoader dankDashPopoutLoader: dankDashPopoutLoader
notepadSlideoutVariants: notepadSlideoutVariants notepadSlideoutVariants: notepadSlideoutVariants
hyprKeybindsModalLoader: hyprKeybindsModalLoader hyprKeybindsModalLoader: hyprKeybindsModalLoader
dankBarLoader: dankBarLoader dankBarRepeater: dankBarRepeater
hyprlandOverviewLoader: hyprlandOverviewLoader hyprlandOverviewLoader: hyprlandOverviewLoader
settingsModal: settingsModal
} }
Variants { Variants {
@@ -534,6 +528,14 @@ Item {
} }
} }
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MediaVolumeOSD {
modelData: item
}
}
Variants { Variants {
model: SettingsData.getFilteredScreens("osd") model: SettingsData.getFilteredScreens("osd")
@@ -581,4 +583,12 @@ Item {
id: hyprlandOverview id: hyprlandOverview
} }
} }
LazyLoader {
id: niriOverviewOverlayLoader
active: CompositorService.isNiri
component: NiriOverviewOverlay {
id: niriOverviewOverlay
}
}
} }
+253 -155
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland import Quickshell.Hyprland
import qs.Common import qs.Common
@@ -14,36 +13,44 @@ Item {
required property var dankDashPopoutLoader required property var dankDashPopoutLoader
required property var notepadSlideoutVariants required property var notepadSlideoutVariants
required property var hyprKeybindsModalLoader required property var hyprKeybindsModalLoader
required property var dankBarLoader required property var dankBarRepeater
required property var hyprlandOverviewLoader required property var hyprlandOverviewLoader
required property var settingsModal
function getFirstBar() {
if (!root.dankBarRepeater || root.dankBarRepeater.count === 0)
return null;
const firstLoader = root.dankBarRepeater.itemAt(0);
return firstLoader ? firstLoader.item : null;
}
IpcHandler { IpcHandler {
function open() { function open() {
root.powerMenuModalLoader.active = true root.powerMenuModalLoader.active = true;
if (root.powerMenuModalLoader.item) if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.openCentered() root.powerMenuModalLoader.item.openCentered();
return "POWERMENU_OPEN_SUCCESS" return "POWERMENU_OPEN_SUCCESS";
} }
function close() { function close() {
if (root.powerMenuModalLoader.item) if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.close() root.powerMenuModalLoader.item.close();
return "POWERMENU_CLOSE_SUCCESS" return "POWERMENU_CLOSE_SUCCESS";
} }
function toggle() { function toggle() {
root.powerMenuModalLoader.active = true root.powerMenuModalLoader.active = true;
if (root.powerMenuModalLoader.item) { if (root.powerMenuModalLoader.item) {
if (root.powerMenuModalLoader.item.shouldBeVisible) { if (root.powerMenuModalLoader.item.shouldBeVisible) {
root.powerMenuModalLoader.item.close() root.powerMenuModalLoader.item.close();
} else { } else {
root.powerMenuModalLoader.item.openCentered() root.powerMenuModalLoader.item.openCentered();
} }
} }
return "POWERMENU_TOGGLE_SUCCESS" return "POWERMENU_TOGGLE_SUCCESS";
} }
target: "powermenu" target: "powermenu"
@@ -51,26 +58,26 @@ Item {
IpcHandler { IpcHandler {
function open(): string { function open(): string {
root.processListModalLoader.active = true root.processListModalLoader.active = true;
if (root.processListModalLoader.item) if (root.processListModalLoader.item)
root.processListModalLoader.item.show() root.processListModalLoader.item.show();
return "PROCESSLIST_OPEN_SUCCESS" return "PROCESSLIST_OPEN_SUCCESS";
} }
function close(): string { function close(): string {
if (root.processListModalLoader.item) if (root.processListModalLoader.item)
root.processListModalLoader.item.hide() root.processListModalLoader.item.hide();
return "PROCESSLIST_CLOSE_SUCCESS" return "PROCESSLIST_CLOSE_SUCCESS";
} }
function toggle(): string { function toggle(): string {
root.processListModalLoader.active = true root.processListModalLoader.active = true;
if (root.processListModalLoader.item) if (root.processListModalLoader.item)
root.processListModalLoader.item.toggle() root.processListModalLoader.item.toggle();
return "PROCESSLIST_TOGGLE_SUCCESS" return "PROCESSLIST_TOGGLE_SUCCESS";
} }
target: "processlist" target: "processlist"
@@ -78,27 +85,33 @@ Item {
IpcHandler { IpcHandler {
function open(): string { function open(): string {
if (root.dankBarLoader.item) { const bar = root.getFirstBar();
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen() if (bar) {
return "CONTROL_CENTER_OPEN_SUCCESS" bar.triggerControlCenterOnFocusedScreen();
return "CONTROL_CENTER_OPEN_SUCCESS";
} }
return "CONTROL_CENTER_OPEN_FAILED" return "CONTROL_CENTER_OPEN_FAILED";
} }
function close(): string { function hide(): string {
if (root.controlCenterLoader.item) { if (root.controlCenterLoader.item && root.controlCenterLoader.item.shouldBeVisible) {
root.controlCenterLoader.item.close() root.controlCenterLoader.item.close();
return "CONTROL_CENTER_CLOSE_SUCCESS" return "CONTROL_CENTER_HIDE_SUCCESS";
} }
return "CONTROL_CENTER_CLOSE_FAILED" return "CONTROL_CENTER_HIDE_FAILED";
} }
function toggle(): string { function toggle(): string {
if (root.dankBarLoader.item) { const bar = root.getFirstBar();
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen() if (bar) {
return "CONTROL_CENTER_TOGGLE_SUCCESS" bar.triggerControlCenterOnFocusedScreen();
return "CONTROL_CENTER_TOGGLE_SUCCESS";
} }
return "CONTROL_CENTER_TOGGLE_FAILED" return "CONTROL_CENTER_TOGGLE_FAILED";
}
function status(): string {
return (root.controlCenterLoader.item && root.controlCenterLoader.item.shouldBeVisible) ? "visible" : "hidden";
} }
target: "control-center" target: "control-center"
@@ -106,55 +119,56 @@ Item {
IpcHandler { IpcHandler {
function open(tab: string): string { function open(tab: string): string {
root.dankDashPopoutLoader.active = true root.dankDashPopoutLoader.active = true;
if (root.dankDashPopoutLoader.item) { if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) { switch (tab.toLowerCase()) {
case "media": case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1 root.dankDashPopoutLoader.item.currentTabIndex = 1;
break break;
case "weather": case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0 root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0;
break break;
default: default:
root.dankDashPopoutLoader.item.currentTabIndex = 0 root.dankDashPopoutLoader.item.currentTabIndex = 0;
break break;
} }
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen) root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
root.dankDashPopoutLoader.item.dashVisible = true root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS" return "DASH_OPEN_SUCCESS";
} }
return "DASH_OPEN_FAILED" return "DASH_OPEN_FAILED";
} }
function close(): string { function close(): string {
if (root.dankDashPopoutLoader.item) { if (root.dankDashPopoutLoader.item) {
root.dankDashPopoutLoader.item.dashVisible = false root.dankDashPopoutLoader.item.dashVisible = false;
return "DASH_CLOSE_SUCCESS" return "DASH_CLOSE_SUCCESS";
} }
return "DASH_CLOSE_FAILED" return "DASH_CLOSE_FAILED";
} }
function toggle(tab: string): string { function toggle(tab: string): string {
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) { const bar = root.getFirstBar();
if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
if (root.dankDashPopoutLoader.item) { if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) { switch (tab.toLowerCase()) {
case "media": case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1 root.dankDashPopoutLoader.item.currentTabIndex = 1;
break break;
case "wallpaper": case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2 root.dankDashPopoutLoader.item.currentTabIndex = 2;
break break;
case "weather": case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0 root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break break;
default: default:
root.dankDashPopoutLoader.item.currentTabIndex = 0 root.dankDashPopoutLoader.item.currentTabIndex = 0;
break break;
} }
} }
return "DASH_TOGGLE_SUCCESS" return "DASH_TOGGLE_SUCCESS";
} }
return "DASH_TOGGLE_FAILED" return "DASH_TOGGLE_FAILED";
} }
target: "dash" target: "dash"
@@ -163,68 +177,68 @@ Item {
IpcHandler { IpcHandler {
function getFocusedScreenName() { function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) { if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name return Hyprland.focusedWorkspace.monitor.name;
} }
if (CompositorService.isNiri && NiriService.currentOutput) { if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput return NiriService.currentOutput;
} }
return "" return "";
} }
function getActiveNotepadInstance() { function getActiveNotepadInstance() {
if (root.notepadSlideoutVariants.instances.length === 0) { if (root.notepadSlideoutVariants.instances.length === 0) {
return null return null;
} }
if (root.notepadSlideoutVariants.instances.length === 1) { if (root.notepadSlideoutVariants.instances.length === 1) {
return root.notepadSlideoutVariants.instances[0] return root.notepadSlideoutVariants.instances[0];
} }
var focusedScreen = getFocusedScreenName() var focusedScreen = getFocusedScreenName();
if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) { if (focusedScreen && root.notepadSlideoutVariants.instances.length > 0) {
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) { for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i] var slideout = root.notepadSlideoutVariants.instances[i];
if (slideout.modelData && slideout.modelData.name === focusedScreen) { if (slideout.modelData && slideout.modelData.name === focusedScreen) {
return slideout return slideout;
} }
} }
} }
for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) { for (var i = 0; i < root.notepadSlideoutVariants.instances.length; i++) {
var slideout = root.notepadSlideoutVariants.instances[i] var slideout = root.notepadSlideoutVariants.instances[i];
if (slideout.isVisible) { if (slideout.isVisible) {
return slideout return slideout;
} }
} }
return root.notepadSlideoutVariants.instances[0] return root.notepadSlideoutVariants.instances[0];
} }
function open(): string { function open(): string {
var instance = getActiveNotepadInstance() var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.show() instance.show();
return "NOTEPAD_OPEN_SUCCESS" return "NOTEPAD_OPEN_SUCCESS";
} }
return "NOTEPAD_OPEN_FAILED" return "NOTEPAD_OPEN_FAILED";
} }
function close(): string { function close(): string {
var instance = getActiveNotepadInstance() var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.hide() instance.hide();
return "NOTEPAD_CLOSE_SUCCESS" return "NOTEPAD_CLOSE_SUCCESS";
} }
return "NOTEPAD_CLOSE_FAILED" return "NOTEPAD_CLOSE_FAILED";
} }
function toggle(): string { function toggle(): string {
var instance = getActiveNotepadInstance() var instance = getActiveNotepadInstance();
if (instance) { if (instance) {
instance.toggle() instance.toggle();
return "NOTEPAD_TOGGLE_SUCCESS" return "NOTEPAD_TOGGLE_SUCCESS";
} }
return "NOTEPAD_TOGGLE_FAILED" return "NOTEPAD_TOGGLE_FAILED";
} }
target: "notepad" target: "notepad"
@@ -232,31 +246,31 @@ Item {
IpcHandler { IpcHandler {
function toggle(): string { function toggle(): string {
SessionService.toggleIdleInhibit() SessionService.toggleIdleInhibit();
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled" return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled";
} }
function enable(): string { function enable(): string {
SessionService.enableIdleInhibit() SessionService.enableIdleInhibit();
return "Idle inhibit enabled" return "Idle inhibit enabled";
} }
function disable(): string { function disable(): string {
SessionService.disableIdleInhibit() SessionService.disableIdleInhibit();
return "Idle inhibit disabled" return "Idle inhibit disabled";
} }
function status(): string { function status(): string {
return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled" return SessionService.idleInhibited ? "Idle inhibit is enabled" : "Idle inhibit is disabled";
} }
function reason(newReason: string): string { function reason(newReason: string): string {
if (!newReason) { if (!newReason) {
return `Current reason: ${SessionService.inhibitReason}` return `Current reason: ${SessionService.inhibitReason}`;
} }
SessionService.setInhibitReason(newReason) SessionService.setInhibitReason(newReason);
return `Inhibit reason set to: ${newReason}` return `Inhibit reason set to: ${newReason}`;
} }
target: "inhibit" target: "inhibit"
@@ -264,42 +278,42 @@ Item {
IpcHandler { IpcHandler {
function list(): string { function list(): string {
return MprisController.availablePlayers.map(p => p.identity).join("\n") return MprisController.availablePlayers.map(p => p.identity).join("\n");
} }
function play(): void { function play(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) { if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
MprisController.activePlayer.play() MprisController.activePlayer.play();
} }
} }
function pause(): void { function pause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPause) { if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
MprisController.activePlayer.pause() MprisController.activePlayer.pause();
} }
} }
function playPause(): void { function playPause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) { if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
MprisController.activePlayer.togglePlaying() MprisController.activePlayer.togglePlaying();
} }
} }
function previous(): void { function previous(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) { if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous() MprisController.activePlayer.previous();
} }
} }
function next(): void { function next(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) { if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
MprisController.activePlayer.next() MprisController.activePlayer.next();
} }
} }
function stop(): void { function stop(): void {
if (MprisController.activePlayer) { if (MprisController.activePlayer) {
MprisController.activePlayer.stop() MprisController.activePlayer.stop();
} }
} }
@@ -309,78 +323,78 @@ Item {
IpcHandler { IpcHandler {
function toggle(provider: string): string { function toggle(provider: string): string {
if (!provider) { if (!provider) {
return "ERROR: No provider specified" return "ERROR: No provider specified";
} }
KeybindsService.loadProvider(provider) KeybindsService.loadProvider(provider);
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) { if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close() root.hyprKeybindsModalLoader.item.close();
} else { } else {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
} }
return `KEYBINDS_TOGGLE_SUCCESS: ${provider}` return `KEYBINDS_TOGGLE_SUCCESS: ${provider}`;
} }
return `KEYBINDS_TOGGLE_FAILED: ${provider}` return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
} }
function toggleWithPath(provider: string, path: string): string { function toggleWithPath(provider: string, path: string): string {
if (!provider) { if (!provider) {
return "ERROR: No provider specified" return "ERROR: No provider specified";
} }
KeybindsService.loadProviderWithPath(provider, path) KeybindsService.loadProviderWithPath(provider, path);
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) { if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close() root.hyprKeybindsModalLoader.item.close();
} else { } else {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
} }
return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})` return `KEYBINDS_TOGGLE_SUCCESS: ${provider} (${path})`;
} }
return `KEYBINDS_TOGGLE_FAILED: ${provider}` return `KEYBINDS_TOGGLE_FAILED: ${provider}`;
} }
function open(provider: string): string { function open(provider: string): string {
if (!provider) { if (!provider) {
return "ERROR: No provider specified" return "ERROR: No provider specified";
} }
KeybindsService.loadProvider(provider) KeybindsService.loadProvider(provider);
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider}` return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
} }
return `KEYBINDS_OPEN_FAILED: ${provider}` return `KEYBINDS_OPEN_FAILED: ${provider}`;
} }
function openWithPath(provider: string, path: string): string { function openWithPath(provider: string, path: string): string {
if (!provider) { if (!provider) {
return "ERROR: No provider specified" return "ERROR: No provider specified";
} }
KeybindsService.loadProviderWithPath(provider, path) KeybindsService.loadProviderWithPath(provider, path);
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})` return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
} }
return `KEYBINDS_OPEN_FAILED: ${provider}` return `KEYBINDS_OPEN_FAILED: ${provider}`;
} }
function close(): string { function close(): string {
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close() root.hyprKeybindsModalLoader.item.close();
return "KEYBINDS_CLOSE_SUCCESS" return "KEYBINDS_CLOSE_SUCCESS";
} }
return "KEYBINDS_CLOSE_FAILED" return "KEYBINDS_CLOSE_FAILED";
} }
target: "keybinds" target: "keybinds"
@@ -389,67 +403,67 @@ Item {
IpcHandler { IpcHandler {
function openBinds(): string { function openBinds(): string {
if (!CompositorService.isHyprland) { if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
KeybindsService.loadProvider("hyprland") KeybindsService.loadProvider("hyprland");
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
return "HYPR_KEYBINDS_OPEN_SUCCESS" return "HYPR_KEYBINDS_OPEN_SUCCESS";
} }
return "HYPR_KEYBINDS_OPEN_FAILED" return "HYPR_KEYBINDS_OPEN_FAILED";
} }
function closeBinds(): string { function closeBinds(): string {
if (!CompositorService.isHyprland) { if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close() root.hyprKeybindsModalLoader.item.close();
return "HYPR_KEYBINDS_CLOSE_SUCCESS" return "HYPR_KEYBINDS_CLOSE_SUCCESS";
} }
return "HYPR_KEYBINDS_CLOSE_FAILED" return "HYPR_KEYBINDS_CLOSE_FAILED";
} }
function toggleBinds(): string { function toggleBinds(): string {
if (!CompositorService.isHyprland) { if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
KeybindsService.loadProvider("hyprland") KeybindsService.loadProvider("hyprland");
root.hyprKeybindsModalLoader.active = true root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) { if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) { if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close() root.hyprKeybindsModalLoader.item.close();
} else { } else {
root.hyprKeybindsModalLoader.item.open() root.hyprKeybindsModalLoader.item.open();
} }
return "HYPR_KEYBINDS_TOGGLE_SUCCESS" return "HYPR_KEYBINDS_TOGGLE_SUCCESS";
} }
return "HYPR_KEYBINDS_TOGGLE_FAILED" return "HYPR_KEYBINDS_TOGGLE_FAILED";
} }
function toggleOverview(): string { function toggleOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) { if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS" return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS";
} }
function closeOverview(): string { function closeOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) { if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
root.hyprlandOverviewLoader.item.overviewOpen = false root.hyprlandOverviewLoader.item.overviewOpen = false;
return "OVERVIEW_CLOSE_SUCCESS" return "OVERVIEW_CLOSE_SUCCESS";
} }
function openOverview(): string { function openOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) { if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE" return "HYPR_NOT_AVAILABLE";
} }
root.hyprlandOverviewLoader.item.overviewOpen = true root.hyprlandOverviewLoader.item.overviewOpen = true;
return "OVERVIEW_OPEN_SUCCESS" return "OVERVIEW_OPEN_SUCCESS";
} }
target: "hypr" target: "hypr"
@@ -457,12 +471,96 @@ Item {
IpcHandler { IpcHandler {
function wallpaper(): string { function wallpaper(): string {
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) { const bar = root.getFirstBar();
return "SUCCESS: Toggled wallpaper browser" if (bar && bar.triggerWallpaperBrowserOnFocusedScreen()) {
return "SUCCESS: Toggled wallpaper browser";
} }
return "ERROR: Failed to toggle wallpaper browser" return "ERROR: Failed to toggle wallpaper browser";
} }
target: "dankdash" target: "dankdash"
} }
IpcHandler {
function reveal(index: int): string {
const idx = index - 1;
if (idx < 0 || idx >= SettingsData.barConfigs.length) {
return `BAR_${index}_NOT_FOUND`;
}
const bar = SettingsData.barConfigs[idx];
SettingsData.updateBarConfig(bar.id, {
visible: true
});
return `BAR_${index}_SHOW_SUCCESS`;
}
function hide(index: int): string {
const idx = index - 1;
if (idx < 0 || idx >= SettingsData.barConfigs.length) {
return `BAR_${index}_NOT_FOUND`;
}
const bar = SettingsData.barConfigs[idx];
SettingsData.updateBarConfig(bar.id, {
visible: false
});
return `BAR_${index}_HIDE_SUCCESS`;
}
function toggle(index: int): string {
const idx = index - 1;
if (idx < 0 || idx >= SettingsData.barConfigs.length) {
return `BAR_${index}_NOT_FOUND`;
}
const bar = SettingsData.barConfigs[idx];
const newVisible = !(bar.visible ?? true);
SettingsData.updateBarConfig(bar.id, {
visible: newVisible
});
return newVisible ? `BAR_${index}_SHOW_SUCCESS` : `BAR_${index}_HIDE_SUCCESS`;
}
function status(index: int): string {
const idx = index - 1;
if (idx < 0 || idx >= SettingsData.barConfigs.length) {
return `BAR_${index}_NOT_FOUND`;
}
const bar = SettingsData.barConfigs[idx];
return (bar.visible ?? true) ? "visible" : "hidden";
}
target: "bar"
}
IpcHandler {
function open(): string {
root.settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close(): string {
root.settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle(): string {
root.settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
root.settingsModal.wallpaperBrowser.allowStacking = false;
root.settingsModal.wallpaperBrowser.open();
} else if (type === "profile") {
root.settingsModal.profileBrowser.allowStacking = false;
root.settingsModal.profileBrowser.open();
}
}
target: "file"
}
} }
+83 -77
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -9,6 +10,11 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing" layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string deviceName: "" property string deviceName: ""
property string deviceAddress: "" property string deviceAddress: ""
property string requestType: "" property string requestType: ""
@@ -18,38 +24,38 @@ DankModal {
property string passkeyInput: "" property string passkeyInput: ""
function show(pairingData) { function show(pairingData) {
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData)) console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
token = pairingData.token || "" token = pairingData.token || "";
deviceName = pairingData.deviceName || "" deviceName = pairingData.deviceName || "";
deviceAddress = pairingData.deviceAddr || "" deviceAddress = pairingData.deviceAddr || "";
requestType = pairingData.requestType || "" requestType = pairingData.requestType || "";
passkey = pairingData.passkey || 0 passkey = pairingData.passkey || 0;
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
console.log("BluetoothPairingModal: Calling open()") console.log("BluetoothPairingModal: Calling open()");
open() open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
if (requestType === "pin" && contentLoader.item.pinInputField) { if (requestType === "pin" && contentLoader.item.pinInputField) {
contentLoader.item.pinInputField.forceActiveFocus() contentLoader.item.pinInputField.forceActiveFocus();
} else if (requestType === "passkey" && contentLoader.item.passkeyInputField) { } else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
contentLoader.item.passkeyInputField.forceActiveFocus() contentLoader.item.passkeyInputField.forceActiveFocus();
} }
} }
}) });
} }
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
keepPopoutsOpen: true keepPopoutsOpen: true
width: 420 modalWidth: 420
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240
onShouldBeVisibleChanged: () => { onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) { if (!shouldBeVisible) {
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
} }
} }
@@ -57,22 +63,22 @@ DankModal {
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
if (requestType === "pin" && contentLoader.item.pinInputField) { if (requestType === "pin" && contentLoader.item.pinInputField) {
contentLoader.item.pinInputField.forceActiveFocus() contentLoader.item.pinInputField.forceActiveFocus();
} else if (requestType === "passkey" && contentLoader.item.passkeyInputField) { } else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
contentLoader.item.passkeyInputField.forceActiveFocus() contentLoader.item.passkeyInputField.forceActiveFocus();
} }
} }
}) });
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
if (token) { if (token) {
DMSService.bluetoothCancelPairing(token) DMSService.bluetoothCancelPairing(token);
} }
close() close();
token = "" token = "";
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
} }
content: Component { content: Component {
@@ -88,13 +94,13 @@ DankModal {
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
if (token) { if (token) {
DMSService.bluetoothCancelPairing(token) DMSService.bluetoothCancelPairing(token);
} }
close() close();
token = "" token = "";
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
event.accepted = true event.accepted = true;
} }
Column { Column {
@@ -122,19 +128,19 @@ DankModal {
text: { text: {
switch (requestType) { switch (requestType) {
case "confirm": case "confirm":
return I18n.tr("Confirm passkey for ") + deviceName return I18n.tr("Confirm passkey for ") + deviceName;
case "display-passkey": case "display-passkey":
return I18n.tr("Enter this passkey on ") + deviceName return I18n.tr("Enter this passkey on ") + deviceName;
case "authorize": case "authorize":
return I18n.tr("Authorize pairing with ") + deviceName return I18n.tr("Authorize pairing with ") + deviceName;
case "pin": case "pin":
return I18n.tr("Enter PIN for ") + deviceName return I18n.tr("Enter PIN for ") + deviceName;
case "passkey": case "passkey":
return I18n.tr("Enter passkey for ") + deviceName return I18n.tr("Enter passkey for ") + deviceName;
default: default:
if (requestType.startsWith("authorize-service")) if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize service for ") + deviceName return I18n.tr("Authorize service for ") + deviceName;
return deviceName return deviceName;
} }
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -156,7 +162,7 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
pinInputField.forceActiveFocus() pinInputField.forceActiveFocus();
} }
} }
@@ -171,10 +177,10 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
pinInput = text pinInput = text;
} }
onAccepted: () => { onAccepted: () => {
submitPairing() submitPairing();
} }
} }
} }
@@ -191,7 +197,7 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
passkeyInputField.forceActiveFocus() passkeyInputField.forceActiveFocus();
} }
} }
@@ -206,10 +212,10 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
passkeyInput = text passkeyInput = text;
} }
onAccepted: () => { onAccepted: () => {
submitPairing() submitPairing();
} }
} }
} }
@@ -277,12 +283,12 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
if (token) { if (token) {
DMSService.bluetoothCancelPairing(token) DMSService.bluetoothCancelPairing(token);
} }
close() close();
token = "" token = "";
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
} }
} }
} }
@@ -294,10 +300,10 @@ DankModal {
color: pairArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: pairArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: { enabled: {
if (requestType === "pin") if (requestType === "pin")
return pinInput.length > 0 return pinInput.length > 0;
if (requestType === "passkey") if (requestType === "passkey")
return passkeyInput.length === 6 return passkeyInput.length === 6;
return true return true;
} }
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
@@ -309,13 +315,13 @@ DankModal {
switch (requestType) { switch (requestType) {
case "confirm": case "confirm":
case "display-passkey": case "display-passkey":
return I18n.tr("Confirm") return I18n.tr("Confirm");
case "authorize": case "authorize":
return I18n.tr("Authorize") return I18n.tr("Authorize");
default: default:
if (requestType.startsWith("authorize-service")) if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize") return I18n.tr("Authorize");
return I18n.tr("Pair") return I18n.tr("Pair");
} }
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
@@ -331,7 +337,7 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: parent.enabled enabled: parent.enabled
onClicked: () => { onClicked: () => {
submitPairing() submitPairing();
} }
} }
@@ -356,48 +362,48 @@ DankModal {
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: () => { onClicked: () => {
if (token) { if (token) {
DMSService.bluetoothCancelPairing(token) DMSService.bluetoothCancelPairing(token);
} }
close() close();
token = "" token = "";
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
} }
} }
} }
} }
function submitPairing() { function submitPairing() {
const secrets = {} const secrets = {};
switch (requestType) { switch (requestType) {
case "pin": case "pin":
secrets["pin"] = pinInput secrets["pin"] = pinInput;
break break;
case "passkey": case "passkey":
secrets["passkey"] = passkeyInput secrets["passkey"] = passkeyInput;
break break;
case "confirm": case "confirm":
case "display-passkey": case "display-passkey":
case "authorize": case "authorize":
secrets["decision"] = "yes" secrets["decision"] = "yes";
break break;
default: default:
if (requestType.startsWith("authorize-service")) { if (requestType.startsWith("authorize-service")) {
secrets["decision"] = "yes" secrets["decision"] = "yes";
} }
break break;
} }
DMSService.bluetoothSubmitPairing(token, secrets, true, response => { DMSService.bluetoothSubmitPairing(token, secrets, true, response => {
if (response.error) { if (response.error) {
ToastService.showError(I18n.tr("Pairing failed"), response.error) ToastService.showError(I18n.tr("Pairing failed"), response.error);
} }
}) });
close() close();
token = "" token = "";
pinInput = "" pinInput = "";
passkeyInput = "" passkeyInput = "";
} }
} }
@@ -1,19 +1,23 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
import qs.Widgets
DankModal { DankModal {
id: clipboardHistoryModal id: clipboardHistoryModal
layerNamespace: "dms:clipboard" layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal]
active: CompositorService.isHyprland && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0 property int totalCount: 0
property var clipboardEntries: [] property var clipboardEntries: []
property string searchText: "" property string searchText: ""
@@ -25,119 +29,119 @@ DankModal {
readonly property int maxConcurrentLoads: 3 readonly property int maxConcurrentLoads: 3
function updateFilteredModel() { function updateFilteredModel() {
filteredClipboardModel.clear() filteredClipboardModel.clear();
for (var i = 0; i < clipboardModel.count; i++) { for (var i = 0; i < clipboardModel.count; i++) {
const entry = clipboardModel.get(i).entry const entry = clipboardModel.get(i).entry;
if (searchText.trim().length === 0) { if (searchText.trim().length === 0) {
filteredClipboardModel.append({ filteredClipboardModel.append({
"entry": entry "entry": entry
}) });
} else { } else {
const content = getEntryPreview(entry).toLowerCase() const content = getEntryPreview(entry).toLowerCase();
if (content.includes(searchText.toLowerCase())) { if (content.includes(searchText.toLowerCase())) {
filteredClipboardModel.append({ filteredClipboardModel.append({
"entry": entry "entry": entry
}) });
} }
} }
} }
clipboardHistoryModal.totalCount = filteredClipboardModel.count clipboardHistoryModal.totalCount = filteredClipboardModel.count;
if (filteredClipboardModel.count === 0) { if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false keyboardNavigationActive = false;
selectedIndex = 0 selectedIndex = 0;
} else if (selectedIndex >= filteredClipboardModel.count) { } else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1 selectedIndex = filteredClipboardModel.count - 1;
} }
} }
function toggle() { function toggle() {
if (shouldBeVisible) { if (shouldBeVisible) {
hide() hide();
} else { } else {
show() show();
} }
} }
function show() { function show() {
open() open();
clipboardHistoryModal.searchText = "" clipboardHistoryModal.searchText = "";
clipboardHistoryModal.activeImageLoads = 0 clipboardHistoryModal.activeImageLoads = 0;
clipboardHistoryModal.shouldHaveFocus = true clipboardHistoryModal.shouldHaveFocus = true;
refreshClipboard() refreshClipboard();
keyboardController.reset() keyboardController.reset();
Qt.callLater(function () { Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) { if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = "" contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus() contentLoader.item.searchField.forceActiveFocus();
} }
}) });
} }
function hide() { function hide() {
close() close();
clipboardHistoryModal.searchText = "" clipboardHistoryModal.searchText = "";
clipboardHistoryModal.activeImageLoads = 0 clipboardHistoryModal.activeImageLoads = 0;
updateFilteredModel() updateFilteredModel();
keyboardController.reset() keyboardController.reset();
cleanupTempFiles() cleanupTempFiles();
} }
function cleanupTempFiles() { function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]) Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]);
} }
function refreshClipboard() { function refreshClipboard() {
clipboardProcesses.refresh() clipboardProcesses.refresh();
} }
function copyEntry(entry) { function copyEntry(entry) {
const entryId = entry.split('\t')[0] const entryId = entry.split('\t')[0];
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]) Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]);
ToastService.showInfo(I18n.tr("Copied to clipboard")) ToastService.showInfo(I18n.tr("Copied to clipboard"));
hide() hide();
} }
function deleteEntry(entry) { function deleteEntry(entry) {
clipboardProcesses.deleteEntry(entry) clipboardProcesses.deleteEntry(entry);
} }
function clearAll() { function clearAll() {
clipboardProcesses.clearAll() clipboardProcesses.clearAll();
} }
function getEntryPreview(entry) { function getEntryPreview(entry) {
let content = entry.replace(/^\s*\d+\s+/, "") let content = entry.replace(/^\s*\d+\s+/, "");
if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) { if (content.includes("image/") || content.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(content)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/) const dimensionMatch = content.match(/(\d+)x(\d+)/);
if (dimensionMatch) { if (dimensionMatch) {
return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}` return `Image ${dimensionMatch[1]}×${dimensionMatch[2]}`;
} }
const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i) const typeMatch = content.match(/\b(png|jpg|jpeg|gif|bmp|webp)\b/i);
if (typeMatch) { if (typeMatch) {
return `Image (${typeMatch[1].toUpperCase()})` return `Image (${typeMatch[1].toUpperCase()})`;
} }
return "Image" return "Image";
} }
if (content.length > ClipboardConstants.previewLength) { if (content.length > ClipboardConstants.previewLength) {
return content.substring(0, ClipboardConstants.previewLength) + "..." return content.substring(0, ClipboardConstants.previewLength) + "...";
} }
return content return content;
} }
function getEntryType(entry) { function getEntryType(entry) {
if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) { if (entry.includes("image/") || entry.includes("binary data") || /\.(png|jpg|jpeg|gif|bmp|webp)/i.test(entry) || /\b(png|jpg|jpeg|gif|bmp|webp)\b/i.test(entry)) {
return "image" return "image";
} }
if (entry.length > ClipboardConstants.longTextThreshold) { if (entry.length > ClipboardConstants.longTextThreshold) {
return "long_text" return "long_text";
} }
return "text" return "text";
} }
visible: false visible: false
width: ClipboardConstants.modalWidth modalWidth: ClipboardConstants.modalWidth
height: ClipboardConstants.modalHeight modalHeight: ClipboardConstants.modalHeight
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
@@ -145,7 +149,7 @@ DankModal {
enableShadow: true enableShadow: true
onBackgroundClicked: hide() onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) { modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event) keyboardController.handleKey(event);
} }
content: clipboardContent content: clipboardContent
@@ -160,12 +164,12 @@ DankModal {
confirmButtonColor: Theme.primary confirmButtonColor: Theme.primary
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
clipboardHistoryModal.shouldHaveFocus = false clipboardHistoryModal.shouldHaveFocus = false;
} else if (clipboardHistoryModal.shouldBeVisible) { } else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.modalFocusScope.forceActiveFocus() clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) { if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus() clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
} }
} }
} }
@@ -192,18 +196,18 @@ DankModal {
IpcHandler { IpcHandler {
function open(): string { function open(): string {
clipboardHistoryModal.show() clipboardHistoryModal.show();
return "CLIPBOARD_OPEN_SUCCESS" return "CLIPBOARD_OPEN_SUCCESS";
} }
function close(): string { function close(): string {
clipboardHistoryModal.hide() clipboardHistoryModal.hide();
return "CLIPBOARD_CLOSE_SUCCESS" return "CLIPBOARD_CLOSE_SUCCESS";
} }
function toggle(): string { function toggle(): string {
clipboardHistoryModal.toggle() clipboardHistoryModal.toggle();
return "CLIPBOARD_TOGGLE_SUCCESS" return "CLIPBOARD_TOGGLE_SUCCESS";
} }
target: "clipboard" target: "clipboard"
+87 -87
View File
@@ -19,141 +19,141 @@ DankModal {
property bool keyboardNavigation: false property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) { function show(title, message, onConfirmCallback, onCancelCallback) {
confirmTitle = title || "" confirmTitle = title || "";
confirmMessage = message || "" confirmMessage = message || "";
confirmButtonText = "Confirm" confirmButtonText = "Confirm";
cancelButtonText = "Cancel" cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || (() => {}) onConfirm = onConfirmCallback || (() => {});
onCancel = onCancelCallback || (() => {}) onCancel = onCancelCallback || (() => {});
selectedButton = -1 selectedButton = -1;
keyboardNavigation = false keyboardNavigation = false;
open() open();
} }
function showWithOptions(options) { function showWithOptions(options) {
confirmTitle = options.title || "" confirmTitle = options.title || "";
confirmMessage = options.message || "" confirmMessage = options.message || "";
confirmButtonText = options.confirmText || "Confirm" confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel" cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || (() => {}) onConfirm = options.onConfirm || (() => {});
onCancel = options.onCancel || (() => {}) onCancel = options.onCancel || (() => {});
selectedButton = -1 selectedButton = -1;
keyboardNavigation = false keyboardNavigation = false;
open() open();
} }
function selectButton() { function selectButton() {
close() close();
if (selectedButton === 0) { if (selectedButton === 0) {
if (onCancel) { if (onCancel) {
onCancel() onCancel();
} }
} else { } else {
if (onConfirm) { if (onConfirm) {
onConfirm() onConfirm();
} }
} }
} }
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
width: 350 modalWidth: 350
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
enableShadow: true enableShadow: true
shouldHaveFocus: true shouldHaveFocus: true
onBackgroundClicked: { onBackgroundClicked: {
close() close();
if (onCancel) { if (onCancel) {
onCancel() onCancel();
} }
} }
onOpened: { onOpened: {
Qt.callLater(function () { Qt.callLater(function () {
modalFocusScope.forceActiveFocus() modalFocusScope.forceActiveFocus();
modalFocusScope.focus = true modalFocusScope.focus = true;
shouldHaveFocus = true shouldHaveFocus = true;
}) });
} }
modalFocusScope.Keys.onPressed: function (event) { modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Escape: case Qt.Key_Escape:
close() close();
if (onCancel) { if (onCancel) {
onCancel() onCancel();
} }
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Left: case Qt.Key_Left:
case Qt.Key_Up: case Qt.Key_Up:
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 0 selectedButton = 0;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Right: case Qt.Key_Right:
case Qt.Key_Down: case Qt.Key_Down:
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 1 selectedButton = 1;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_N: case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2 selectedButton = (selectedButton + 1) % 2;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_P: case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2 selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_J: case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 1 selectedButton = 1;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_K: case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 0 selectedButton = 0;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_H: case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 0 selectedButton = 0;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_L: case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true keyboardNavigation = true;
selectedButton = 1 selectedButton = 1;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_Tab: case Qt.Key_Tab:
keyboardNavigation = true keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2 selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
if (selectedButton !== -1) { if (selectedButton !== -1) {
selectButton() selectButton();
} else { } else {
selectedButton = 1 selectedButton = 1;
selectButton() selectButton();
} }
event.accepted = true event.accepted = true;
break break;
} }
} }
@@ -210,11 +210,11 @@ DankModal {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (keyboardNavigation && selectedButton === 0) { if (keyboardNavigation && selectedButton === 0) {
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
} else if (cancelButton.containsMouse) { } else if (cancelButton.containsMouse) {
return Theme.surfacePressed return Theme.surfacePressed;
} else { } else {
return Theme.surfaceVariantAlpha return Theme.surfaceVariantAlpha;
} }
} }
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent" border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
@@ -235,8 +235,8 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
selectedButton = 0 selectedButton = 0;
selectButton() selectButton();
} }
} }
} }
@@ -246,13 +246,13 @@ DankModal {
height: 40 height: 40
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
const baseColor = confirmButtonColor const baseColor = confirmButtonColor;
if (keyboardNavigation && selectedButton === 1) { if (keyboardNavigation && selectedButton === 1) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1) return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 1);
} else if (confirmButton.containsMouse) { } else if (confirmButton.containsMouse) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9) return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else { } else {
return baseColor return baseColor;
} }
} }
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent" border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
@@ -273,8 +273,8 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
selectedButton = 1 selectedButton = 1;
selectButton() selectButton();
} }
} }
} }
+265 -221
View File
@@ -1,25 +1,24 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland import Quickshell.Wayland
import qs.Common import qs.Common
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
PanelWindow { Item {
id: root id: root
property string layerNamespace: "dms:modal" property string layerNamespace: "dms:modal"
WlrLayershell.namespace: layerNamespace
property alias content: contentLoader.sourceComponent property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader property alias contentLoader: contentLoader
property Item directContent: null property Item directContent: null
property real width: 400 property real modalWidth: 400
property real height: 300 property real modalHeight: 300
readonly property real screenWidth: screen ? screen.width : 1920 property var targetScreen
readonly property real screenHeight: screen ? screen.height : 1080 readonly property var effectiveScreen: targetScreen || contentWindow.screen
readonly property real dpr: CompositorService.getScreenScale(screen) readonly property real screenWidth: effectiveScreen?.width
readonly property real screenHeight: effectiveScreen?.height
readonly property real dpr: effectiveScreen ? CompositorService.getScreenScale(effectiveScreen) : 1
property bool showBackground: true property bool showBackground: true
property real backgroundOpacity: 0.5 property real backgroundOpacity: 0.5
property string positioning: "center" property string positioning: "center"
@@ -44,295 +43,340 @@ PanelWindow {
property bool allowStacking: false property bool allowStacking: false
property bool keepContentLoaded: false property bool keepContentLoaded: false
property bool keepPopoutsOpen: false property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
signal opened signal opened
signal dialogClosed signal dialogClosed
signal backgroundClicked signal backgroundClicked
readonly property bool useBackgroundWindow: {
const layerOverride = Quickshell.env("DMS_MODAL_LAYER");
return !layerOverride || layerOverride === "overlay";
}
function open() { function open() {
ModalManager.openModal(root) ModalManager.openModal(root);
closeTimer.stop() closeTimer.stop();
shouldBeVisible = true shouldBeVisible = true;
visible = true if (useBackgroundWindow)
shouldHaveFocus = false backgroundWindow.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => { Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible) shouldHaveFocus = Qt.binding(() => shouldBeVisible);
}) });
} }
function close() { function close() {
shouldBeVisible = false shouldBeVisible = false;
shouldHaveFocus = false shouldHaveFocus = false;
closeTimer.restart() closeTimer.restart();
} }
function toggle() { function toggle() {
if (shouldBeVisible) { shouldBeVisible ? close() : open();
close()
} else {
open()
}
}
visible: shouldBeVisible
color: "transparent"
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
return WlrLayershell.Bottom
case "overlay":
return WlrLayershell.Overlay
case "background":
return WlrLayershell.Background
default:
return WlrLayershell.Top
}
}
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (!shouldHaveFocus) return WlrKeyboardFocus.None
if (CompositorService.isHyprland) return WlrKeyboardFocus.OnDemand
return WlrKeyboardFocus.Exclusive
}
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && shouldHaveFocus
}
onVisibleChanged: {
if (root.visible) {
opened()
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide()
Qt.inputMethod.reset()
}
dialogClosed()
}
} }
Connections { Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) { function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) { if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close() close();
} }
} }
target: ModalManager
} }
Timer { Timer {
id: closeTimer id: closeTimer
interval: animationDuration + 120 interval: animationDuration + 120
onTriggered: { onTriggered: {
visible = false if (!shouldBeVisible) {
} contentWindow.visible = false;
} if (useBackgroundWindow)
backgroundWindow.visible = false;
anchors { dialogClosed();
top: true
left: true
right: true
bottom: true
}
MouseArea {
anchors.fill: parent
enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const localPos = mapToItem(contentContainer, mouse.x, mouse.y)
if (localPos.x < 0 || localPos.x > contentContainer.width || localPos.y < 0 || localPos.y > contentContainer.height) {
root.backgroundClicked()
}
}
}
Rectangle {
id: background
anchors.fill: parent
color: "black"
opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
} }
Item { readonly property real shadowBuffer: 5
id: modalContainer readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
width: Theme.px(root.width, dpr) readonly property real alignedX: Theme.snap((() => {
height: Theme.px(root.height, dpr) switch (positioning) {
x: { case "center":
if (positioning === "center") { return (screenWidth - alignedWidth) / 2;
return Theme.snap((root.screenWidth - width) / 2, dpr) case "top-right":
} else if (positioning === "top-right") { return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr) case "custom":
} else if (positioning === "custom") { return customPosition.x;
return Theme.snap(root.customPosition.x, dpr) default:
return 0;
} }
return 0 })(), dpr)
}
y: { readonly property real alignedY: Theme.snap((() => {
if (positioning === "center") { switch (positioning) {
return Theme.snap((root.screenHeight - height) / 2, dpr) case "center":
} else if (positioning === "top-right") { return (screenHeight - alignedHeight) / 2;
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr) case "top-right":
} else if (positioning === "custom") { return Theme.barHeight + Theme.spacingXS;
return Theme.snap(root.customPosition.y, dpr) case "custom":
return customPosition.y;
default:
return 0;
} }
return 0 })(), dpr)
PanelWindow {
id: backgroundWindow
visible: false
color: "transparent"
WlrLayershell.namespace: root.layerNamespace + ":background"
WlrLayershell.layer: WlrLayershell.Top
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
anchors {
top: true
left: true
right: true
bottom: true
} }
readonly property bool slide: root.animationType === "slide" MouseArea {
readonly property real offsetX: slide ? 15 : 0 anchors.fill: parent
readonly property real offsetY: slide ? -30 : root.animationOffset enabled: root.closeOnBackgroundClick && root.shouldBeVisible
onClicked: mouse => {
const clickX = mouse.x;
const clickY = mouse.y;
const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
property real animX: 0 if (!outsideContent)
property real animY: 0 return;
property real scaleValue: root.animationScaleCollapsed root.backgroundClicked();
onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
Connections {
target: root
function onShouldBeVisibleChanged() {
modalContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetX, root.dpr)
modalContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : modalContainer.offsetY, root.dpr)
modalContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed
} }
} }
Behavior on animX { Rectangle {
NumberAnimation { id: background
duration: root.animationDuration anchors.fill: parent
easing.type: Easing.BezierSpline color: "black"
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve opacity: root.showBackground && SettingsData.modalDarkenBackground ? (root.shouldBeVisible ? root.backgroundOpacity : 0) : 0
visible: root.showBackground && SettingsData.modalDarkenBackground
Behavior on opacity {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
} }
} }
}
Behavior on animY { PanelWindow {
NumberAnimation { id: contentWindow
duration: root.animationDuration visible: false
easing.type: Easing.BezierSpline color: "transparent"
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
WlrLayershell.namespace: root.layerNamespace
WlrLayershell.layer: {
switch (Quickshell.env("DMS_MODAL_LAYER")) {
case "bottom":
return WlrLayershell.Bottom;
case "top":
return WlrLayershell.Top;
case "background":
return WlrLayershell.Background;
default:
return WlrLayershell.Overlay;
} }
} }
WlrLayershell.exclusiveZone: -1
WlrLayershell.keyboardFocus: {
if (customKeyboardFocus !== null)
return customKeyboardFocus;
if (!shouldHaveFocus)
return WlrKeyboardFocus.None;
if (CompositorService.isHyprland)
return WlrKeyboardFocus.OnDemand;
return WlrKeyboardFocus.Exclusive;
}
Behavior on scaleValue { anchors {
NumberAnimation { left: true
duration: root.animationDuration top: true
easing.type: Easing.BezierSpline }
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
WlrLayershell.margins {
left: Math.max(0, Theme.snap(root.alignedX - shadowBuffer, dpr))
top: Math.max(0, Theme.snap(root.alignedY - shadowBuffer, dpr))
}
implicitWidth: root.alignedWidth + (shadowBuffer * 2)
implicitHeight: root.alignedHeight + (shadowBuffer * 2)
onVisibleChanged: {
if (visible) {
opened();
} else {
if (Qt.inputMethod) {
Qt.inputMethod.hide();
Qt.inputMethod.reset();
}
} }
} }
Item { Item {
id: contentContainer id: modalContainer
x: shadowBuffer
y: shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
anchors.centerIn: parent readonly property bool slide: root.animationType === "slide"
width: parent.width readonly property real offsetX: slide ? 15 : 0
height: parent.height readonly property real offsetY: slide ? -30 : root.animationOffset
clip: false
layer.enabled: true
layer.smooth: false
layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
opacity: root.shouldBeVisible ? 1 : 0
scale: modalContainer.scaleValue
x: Theme.snap(modalContainer.animX + (parent.width - width) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
y: Theme.snap(modalContainer.animY + (parent.height - height) * (1 - modalContainer.scaleValue) * 0.5, root.dpr)
Behavior on opacity { 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;
}
}
Behavior on animX {
NumberAnimation { NumberAnimation {
duration: animationDuration duration: root.animationDuration
easing.type: Easing.BezierSpline easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
DankRectangle { Behavior on animY {
anchors.fill: parent NumberAnimation {
color: root.backgroundColor duration: root.animationDuration
borderColor: root.borderColor easing.type: Easing.BezierSpline
borderWidth: root.borderWidth easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
radius: root.cornerRadius }
} }
FocusScope { Behavior on scaleValue {
anchors.fill: parent NumberAnimation {
focus: root.shouldBeVisible duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Item {
id: contentContainer
anchors.centerIn: parent
width: parent.width
height: parent.height
clip: false clip: false
Item { Item {
id: directContentWrapper id: animatedContent
anchors.fill: parent anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false clip: false
opacity: 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
Component.onCompleted: { Behavior on opacity {
if (root.directContent) { NumberAnimation {
root.directContent.parent = directContentWrapper duration: animationDuration
root.directContent.anchors.fill = directContentWrapper easing.type: Easing.BezierSpline
Qt.callLater(() => root.directContent.forceActiveFocus()) easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
} }
} }
Connections { DankRectangle {
function onDirectContentChanged() { anchors.fill: parent
if (root.directContent) { color: root.backgroundColor
root.directContent.parent = directContentWrapper borderColor: root.borderColor
root.directContent.anchors.fill = directContentWrapper borderWidth: root.borderWidth
Qt.callLater(() => root.directContent.forceActiveFocus()) radius: root.cornerRadius
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
clip: false
Item {
id: directContentWrapper
anchors.fill: parent
visible: root.directContent !== null
focus: true
clip: false
Component.onCompleted: {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
Connections {
target: root
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper;
root.directContent.anchors.fill = directContentWrapper;
Qt.callLater(() => root.directContent.forceActiveFocus());
}
}
} }
} }
target: root Loader {
} id: contentLoader
} anchors.fill: parent
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || contentWindow.visible)
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
Loader { onLoaded: {
id: contentLoader if (item) {
Qt.callLater(() => item.forceActiveFocus());
anchors.fill: parent }
active: root.directContent === null && (root.keepContentLoaded || root.shouldBeVisible || root.visible) }
asynchronous: false
focus: true
clip: false
visible: root.directContent === null
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus())
} }
} }
} }
} }
} }
}
FocusScope { FocusScope {
id: focusScope id: focusScope
objectName: "modalFocusScope"
objectName: "modalFocusScope" anchors.fill: parent
anchors.fill: parent visible: root.shouldBeVisible || contentWindow.visible
visible: root.shouldBeVisible || root.visible focus: root.shouldBeVisible
focus: root.shouldBeVisible Keys.onEscapePressed: event => {
Keys.onEscapePressed: event => { if (root.closeOnEscapeKey && shouldHaveFocus) {
if (root.closeOnEscapeKey && shouldHaveFocus) { root.close();
root.close() event.accepted = true;
event.accepted = true }
} }
} }
} }
} }
+171 -140
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -12,7 +11,12 @@ DankModal {
layerNamespace: "dms:color-picker" layerNamespace: "dms:color-picker"
property string pickerTitle: "Choose Color" HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string pickerTitle: I18n.tr("Choose Color")
property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary property color selectedColor: SessionData.recentColors.length > 0 ? SessionData.recentColors[0] : Theme.primary
property var onColorSelectedCallback: null property var onColorSelectedCallback: null
@@ -26,87 +30,79 @@ DankModal {
property real gradientX: 0 property real gradientX: 0
property real gradientY: 0 property real gradientY: 0
readonly property var standardColors: [ readonly property var standardColors: ["#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4", "#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722", "#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7", "#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19", "#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f", "#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315", "#ffffff", "#9e9e9e", "#212121"]
"#f44336", "#e91e63", "#9c27b0", "#673ab7", "#3f51b5", "#2196f3", "#03a9f4", "#00bcd4",
"#009688", "#4caf50", "#8bc34a", "#cddc39", "#ffeb3b", "#ffc107", "#ff9800", "#ff5722",
"#d32f2f", "#c2185b", "#7b1fa2", "#512da8", "#303f9f", "#1976d2", "#0288d1", "#0097a7",
"#00796b", "#388e3c", "#689f38", "#afb42b", "#fbc02d", "#ffa000", "#f57c00", "#e64a19",
"#c62828", "#ad1457", "#6a1b9a", "#4527a0", "#283593", "#1565c0", "#0277bd", "#00838f",
"#00695c", "#2e7d32", "#558b2f", "#9e9d24", "#f9a825", "#ff8f00", "#ef6c00", "#d84315",
"#ffffff", "#9e9e9e", "#212121"
]
function show() { function show() {
currentColor = selectedColor currentColor = selectedColor;
updateFromColor(currentColor) updateFromColor(currentColor);
open() open();
} }
function hide() { function hide() {
onColorSelectedCallback = null onColorSelectedCallback = null;
close() close();
} }
function hideInstant() { function hideInstant() {
onColorSelectedCallback = null onColorSelectedCallback = null;
shouldBeVisible = false shouldBeVisible = false;
visible = false visible = false;
} }
onColorSelected: (color) => { onColorSelected: color => {
if (onColorSelectedCallback) { if (onColorSelectedCallback) {
onColorSelectedCallback(color) onColorSelectedCallback(color);
} }
} }
function copyColorToClipboard(colorValue) { function copyColorToClipboard(colorValue) {
Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`]);
ToastService.showInfo(`Color ${colorValue} copied`) ToastService.showInfo(`Color ${colorValue} copied`);
SessionData.addRecentColor(currentColor) SessionData.addRecentColor(currentColor);
} }
function updateFromColor(color) { function updateFromColor(color) {
hue = color.hsvHue hue = color.hsvHue;
saturation = color.hsvSaturation saturation = color.hsvSaturation;
value = color.hsvValue value = color.hsvValue;
alpha = color.a alpha = color.a;
gradientX = saturation gradientX = saturation;
gradientY = 1 - value gradientY = 1 - value;
} }
function updateColor() { function updateColor() {
currentColor = Qt.hsva(hue, saturation, value, alpha) currentColor = Qt.hsva(hue, saturation, value, alpha);
} }
function updateColorFromGradient(x, y) { function updateColorFromGradient(x, y) {
saturation = Math.max(0, Math.min(1, x)) saturation = Math.max(0, Math.min(1, x));
value = Math.max(0, Math.min(1, 1 - y)) value = Math.max(0, Math.min(1, 1 - y));
updateColor() updateColor();
selectedColor = currentColor selectedColor = currentColor;
} }
function pickColorFromScreen() { function pickColorFromScreen() {
hideInstant() hideInstant();
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => { Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
if (errorCode !== 0) { if (errorCode !== 0) {
console.warn("hyprpicker exited with code:", errorCode) console.warn("hyprpicker exited with code:", errorCode);
root.show() root.show();
return return;
} }
const colorStr = output.trim() const colorStr = output.trim();
if (colorStr.length >= 7 && colorStr.startsWith('#')) { if (colorStr.length >= 7 && colorStr.startsWith('#')) {
const pickedColor = Qt.color(colorStr) const pickedColor = Qt.color(colorStr);
root.selectedColor = pickedColor root.selectedColor = pickedColor;
root.currentColor = pickedColor root.currentColor = pickedColor;
root.updateFromColor(pickedColor) root.updateFromColor(pickedColor);
copyColorToClipboard(colorStr) copyColorToClipboard(colorStr);
root.show() root.show();
} }
}) });
} }
width: 680 modalWidth: 680
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
backgroundColor: Theme.surfaceContainer backgroundColor: Theme.surfaceContainer
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
@@ -127,8 +123,8 @@ DankModal {
focus: true focus: true
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
root.hide() root.hide();
event.accepted = true event.accepted = true;
} }
Column { Column {
@@ -166,7 +162,7 @@ DankModal {
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: () => { onClicked: () => {
root.pickColorFromScreen() root.pickColorFromScreen();
} }
} }
@@ -175,7 +171,7 @@ DankModal {
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: () => { onClicked: () => {
root.hide() root.hide();
} }
} }
} }
@@ -201,8 +197,14 @@ DankModal {
anchors.fill: parent anchors.fill: parent
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Horizontal orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#ffffff" } GradientStop {
GradientStop { position: 1.0; color: "transparent" } position: 0.0
color: "#ffffff"
}
GradientStop {
position: 1.0
color: "transparent"
}
} }
} }
@@ -210,8 +212,14 @@ DankModal {
anchors.fill: parent anchors.fill: parent
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Vertical orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "transparent" } GradientStop {
GradientStop { position: 1.0; color: "#000000" } position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: "#000000"
}
} }
} }
} }
@@ -242,19 +250,19 @@ DankModal {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.CrossCursor cursorShape: Qt.CrossCursor
onPressed: mouse => { onPressed: mouse => {
const x = Math.max(0, Math.min(1, mouse.x / width)) const x = Math.max(0, Math.min(1, mouse.x / width));
const y = Math.max(0, Math.min(1, mouse.y / height)) const y = Math.max(0, Math.min(1, mouse.y / height));
root.gradientX = x root.gradientX = x;
root.gradientY = y root.gradientY = y;
root.updateColorFromGradient(x, y) root.updateColorFromGradient(x, y);
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
const x = Math.max(0, Math.min(1, mouse.x / width)) const x = Math.max(0, Math.min(1, mouse.x / width));
const y = Math.max(0, Math.min(1, mouse.y / height)) const y = Math.max(0, Math.min(1, mouse.y / height));
root.gradientX = x root.gradientX = x;
root.gradientY = y root.gradientY = y;
root.updateColorFromGradient(x, y) root.updateColorFromGradient(x, y);
} }
} }
} }
@@ -270,13 +278,34 @@ DankModal {
gradient: Gradient { gradient: Gradient {
orientation: Gradient.Vertical orientation: Gradient.Vertical
GradientStop { position: 0.00; color: "#ff0000" } GradientStop {
GradientStop { position: 0.17; color: "#ffff00" } position: 0.00
GradientStop { position: 0.33; color: "#00ff00" } color: "#ff0000"
GradientStop { position: 0.50; color: "#00ffff" } }
GradientStop { position: 0.67; color: "#0000ff" } GradientStop {
GradientStop { position: 0.83; color: "#ff00ff" } position: 0.17
GradientStop { position: 1.00; color: "#ff0000" } color: "#ffff00"
}
GradientStop {
position: 0.33
color: "#00ff00"
}
GradientStop {
position: 0.50
color: "#00ffff"
}
GradientStop {
position: 0.67
color: "#0000ff"
}
GradientStop {
position: 0.83
color: "#ff00ff"
}
GradientStop {
position: 1.00
color: "#ff0000"
}
} }
Rectangle { Rectangle {
@@ -293,17 +322,17 @@ DankModal {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.SizeVerCursor cursorShape: Qt.SizeVerCursor
onPressed: mouse => { onPressed: mouse => {
const h = Math.max(0, Math.min(1, mouse.y / height)) const h = Math.max(0, Math.min(1, mouse.y / height));
root.hue = h root.hue = h;
root.updateColor() root.updateColor();
root.selectedColor = root.currentColor root.selectedColor = root.currentColor;
} }
onPositionChanged: mouse => { onPositionChanged: mouse => {
if (pressed) { if (pressed) {
const h = Math.max(0, Math.min(1, mouse.y / height)) const h = Math.max(0, Math.min(1, mouse.y / height));
root.hue = h root.hue = h;
root.updateColor() root.updateColor();
root.selectedColor = root.currentColor root.selectedColor = root.currentColor;
} }
} }
} }
@@ -342,10 +371,10 @@ DankModal {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
const pickedColor = Qt.color(modelData) const pickedColor = Qt.color(modelData);
root.selectedColor = pickedColor root.selectedColor = pickedColor;
root.currentColor = pickedColor root.currentColor = pickedColor;
root.updateFromColor(pickedColor) root.updateFromColor(pickedColor);
} }
} }
} }
@@ -387,9 +416,9 @@ DankModal {
color: { color: {
if (index < SessionData.recentColors.length) { if (index < SessionData.recentColors.length) {
return SessionData.recentColors[index] return SessionData.recentColors[index];
} }
return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) return Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency);
} }
opacity: index < SessionData.recentColors.length ? 1.0 : 0.3 opacity: index < SessionData.recentColors.length ? 1.0 : 0.3
@@ -400,10 +429,10 @@ DankModal {
enabled: index < SessionData.recentColors.length enabled: index < SessionData.recentColors.length
onClicked: () => { onClicked: () => {
if (index < SessionData.recentColors.length) { if (index < SessionData.recentColors.length) {
const pickedColor = SessionData.recentColors[index] const pickedColor = SessionData.recentColors[index];
root.selectedColor = pickedColor root.selectedColor = pickedColor;
root.currentColor = pickedColor root.currentColor = pickedColor;
root.updateFromColor(pickedColor) root.updateFromColor(pickedColor);
} }
} }
} }
@@ -429,10 +458,10 @@ DankModal {
minimum: 0 minimum: 0
maximum: 100 maximum: 100
showValue: false showValue: false
onSliderValueChanged: (newValue) => { onSliderValueChanged: newValue => {
root.alpha = newValue / 100 root.alpha = newValue / 100;
root.updateColor() root.updateColor();
root.selectedColor = root.currentColor root.selectedColor = root.currentColor;
} }
} }
} }
@@ -479,9 +508,10 @@ DankModal {
text: root.currentColor.toString() text: root.currentColor.toString()
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
textColor: { textColor: {
if (text.length === 0) return Theme.surfaceText if (text.length === 0)
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/ return Theme.surfaceText;
return hexPattern.test(text) ? Theme.surfaceText : Theme.error const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
return hexPattern.test(text) ? Theme.surfaceText : Theme.error;
} }
placeholderText: "#000000" placeholderText: "#000000"
backgroundColor: Theme.surfaceHover backgroundColor: Theme.surfaceHover
@@ -490,13 +520,14 @@ DankModal {
topPadding: Theme.spacingS topPadding: Theme.spacingS
bottomPadding: Theme.spacingS bottomPadding: Theme.spacingS
onAccepted: () => { onAccepted: () => {
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/ const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
if (!hexPattern.test(text)) return if (!hexPattern.test(text))
const color = Qt.color(text) return;
const color = Qt.color(text);
if (color) { if (color) {
root.selectedColor = color root.selectedColor = color;
root.currentColor = color root.currentColor = color;
root.updateFromColor(color) root.updateFromColor(color);
} }
} }
} }
@@ -508,7 +539,7 @@ DankModal {
buttonSize: 36 buttonSize: 36
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: () => { onClicked: () => {
root.copyColorToClipboard(hexInput.text) root.copyColorToClipboard(hexInput.text);
} }
} }
} }
@@ -540,14 +571,14 @@ DankModal {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
const r = Math.round(root.currentColor.r * 255) const r = Math.round(root.currentColor.r * 255);
const g = Math.round(root.currentColor.g * 255) const g = Math.round(root.currentColor.g * 255);
const b = Math.round(root.currentColor.b * 255) const b = Math.round(root.currentColor.b * 255);
if (root.alpha < 1) { if (root.alpha < 1) {
const a = Math.round(root.alpha * 255) const a = Math.round(root.alpha * 255);
return `${r}, ${g}, ${b}, ${a}` return `${r}, ${g}, ${b}, ${a}`;
} }
return `${r}, ${g}, ${b}` return `${r}, ${g}, ${b}`;
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
@@ -561,18 +592,18 @@ DankModal {
buttonSize: 36 buttonSize: 36
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: () => { onClicked: () => {
const r = Math.round(root.currentColor.r * 255) const r = Math.round(root.currentColor.r * 255);
const g = Math.round(root.currentColor.g * 255) const g = Math.round(root.currentColor.g * 255);
const b = Math.round(root.currentColor.b * 255) const b = Math.round(root.currentColor.b * 255);
let rgbString let rgbString;
if (root.alpha < 1) { if (root.alpha < 1) {
const a = Math.round(root.alpha * 255) const a = Math.round(root.alpha * 255);
rgbString = `rgba(${r}, ${g}, ${b}, ${a})` rgbString = `rgba(${r}, ${g}, ${b}, ${a})`;
} else { } else {
rgbString = `rgb(${r}, ${g}, ${b})` rgbString = `rgb(${r}, ${g}, ${b})`;
} }
Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`]);
ToastService.showInfo(`${rgbString} copied`) ToastService.showInfo(`${rgbString} copied`);
} }
} }
} }
@@ -604,14 +635,14 @@ DankModal {
StyledText { StyledText {
anchors.centerIn: parent anchors.centerIn: parent
text: { text: {
const h = Math.round(root.hue * 360) const h = Math.round(root.hue * 360);
const s = Math.round(root.saturation * 100) const s = Math.round(root.saturation * 100);
const v = Math.round(root.value * 100) const v = Math.round(root.value * 100);
if (root.alpha < 1) { if (root.alpha < 1) {
const a = Math.round(root.alpha * 100) const a = Math.round(root.alpha * 100);
return `${h}°, ${s}%, ${v}%, ${a}%` return `${h}°, ${s}%, ${v}%, ${a}%`;
} }
return `${h}°, ${s}%, ${v}%` return `${h}°, ${s}%, ${v}%`;
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText color: Theme.surfaceText
@@ -625,18 +656,18 @@ DankModal {
buttonSize: 36 buttonSize: 36
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: () => { onClicked: () => {
const h = Math.round(root.hue * 360) const h = Math.round(root.hue * 360);
const s = Math.round(root.saturation * 100) const s = Math.round(root.saturation * 100);
const v = Math.round(root.value * 100) const v = Math.round(root.value * 100);
let hsvString let hsvString;
if (root.alpha < 1) { if (root.alpha < 1) {
const a = Math.round(root.alpha * 100) const a = Math.round(root.alpha * 100);
hsvString = `${h}, ${s}, ${v}, ${a}` hsvString = `${h}, ${s}, ${v}, ${a}`;
} else { } else {
hsvString = `${h}, ${s}, ${v}` hsvString = `${h}, ${s}, ${v}`;
} }
Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`]) Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`]);
ToastService.showInfo(`HSV ${hsvString} copied`) ToastService.showInfo(`HSV ${hsvString} copied`);
} }
} }
} }
@@ -652,9 +683,9 @@ DankModal {
textColor: Theme.background textColor: Theme.background
anchors.right: parent.right anchors.right: parent.right
onClicked: { onClicked: {
SessionData.addRecentColor(root.currentColor) SessionData.addRecentColor(root.currentColor);
root.colorSelected(root.currentColor) root.colorSelected(root.currentColor);
root.hide() root.hide();
} }
} }
} }
+15 -16
View File
@@ -1,7 +1,6 @@
import QtQuick import QtQuick
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services
import qs.Widgets import qs.Widgets
DankModal { DankModal {
@@ -15,8 +14,8 @@ DankModal {
shouldBeVisible: false shouldBeVisible: false
allowStacking: true allowStacking: true
width: 420 modalWidth: 420
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200 modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
Timer { Timer {
id: countdownTimer id: countdownTimer
@@ -24,20 +23,20 @@ DankModal {
repeat: true repeat: true
running: root.shouldBeVisible running: root.shouldBeVisible
onTriggered: { onTriggered: {
countdown-- countdown--;
if (countdown <= 0) { if (countdown <= 0) {
revert() revert();
} }
} }
} }
onOpened: { onOpened: {
countdown = 15 countdown = 15;
countdownTimer.start() countdownTimer.start();
} }
onClosed: { onClosed: {
countdownTimer.stop() countdownTimer.stop();
} }
onBackgroundClicked: revert onBackgroundClicked: revert
@@ -51,13 +50,13 @@ DankModal {
implicitHeight: mainColumn.implicitHeight implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
revert() revert();
event.accepted = true event.accepted = true;
} }
Keys.onReturnPressed: event => { Keys.onReturnPressed: event => {
confirm() confirm();
event.accepted = true event.accepted = true;
} }
Column { Column {
@@ -235,12 +234,12 @@ DankModal {
} }
function confirm() { function confirm() {
displaysTab.confirmChanges() displaysTab.confirmChanges();
close() close();
} }
function revert() { function revert() {
displaysTab.revertChanges() displaysTab.revertChanges();
close() close();
} }
} }
File diff suppressed because it is too large Load Diff
+15 -24
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -10,34 +9,36 @@ DankModal {
layerNamespace: "dms:network-info" layerNamespace: "dms:network-info"
keepPopoutsOpen: true
property bool networkInfoModalVisible: false property bool networkInfoModalVisible: false
property string networkSSID: "" property string networkSSID: ""
property var networkData: null property var networkData: null
function showNetworkInfo(ssid, data) { function showNetworkInfo(ssid, data) {
networkSSID = ssid networkSSID = ssid;
networkData = data networkData = data;
networkInfoModalVisible = true networkInfoModalVisible = true;
open() open();
NetworkService.fetchNetworkInfo(ssid) NetworkService.fetchNetworkInfo(ssid);
} }
function hideDialog() { function hideDialog() {
networkInfoModalVisible = false networkInfoModalVisible = false;
close() close();
networkSSID = "" networkSSID = "";
networkData = null networkData = null;
} }
visible: networkInfoModalVisible visible: networkInfoModalVisible
width: 600 modalWidth: 600
height: 500 modalHeight: 500
enableShadow: true enableShadow: true
onBackgroundClicked: hideDialog() onBackgroundClicked: hideDialog()
onVisibleChanged: { onVisibleChanged: {
if (!visible) { if (!visible) {
networkSSID = "" networkSSID = "";
networkData = null networkData = null;
} }
} }
@@ -71,7 +72,6 @@ DankModal {
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
DankActionButton { DankActionButton {
@@ -80,7 +80,6 @@ DankModal {
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: root.hideDialog() onClicked: root.hideDialog()
} }
} }
Rectangle { Rectangle {
@@ -109,7 +108,6 @@ DankModal {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
} }
Item { Item {
@@ -148,17 +146,10 @@ DankModal {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
} }
} }
+15 -24
View File
@@ -1,5 +1,4 @@
import QtQuick import QtQuick
import QtQuick.Controls
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -10,34 +9,36 @@ DankModal {
layerNamespace: "dms:network-info-wired" layerNamespace: "dms:network-info-wired"
keepPopoutsOpen: true
property bool networkWiredInfoModalVisible: false property bool networkWiredInfoModalVisible: false
property string networkID: "" property string networkID: ""
property var networkData: null property var networkData: null
function showNetworkInfo(id, data) { function showNetworkInfo(id, data) {
networkID = id networkID = id;
networkData = data networkData = data;
networkWiredInfoModalVisible = true networkWiredInfoModalVisible = true;
open() open();
NetworkService.fetchWiredNetworkInfo(data.uuid) NetworkService.fetchWiredNetworkInfo(data.uuid);
} }
function hideDialog() { function hideDialog() {
networkWiredInfoModalVisible = false networkWiredInfoModalVisible = false;
close() close();
networkID = "" networkID = "";
networkData = null networkData = null;
} }
visible: networkWiredInfoModalVisible visible: networkWiredInfoModalVisible
width: 600 modalWidth: 600
height: 500 modalHeight: 500
enableShadow: true enableShadow: true
onBackgroundClicked: hideDialog() onBackgroundClicked: hideDialog()
onVisibleChanged: { onVisibleChanged: {
if (!visible) { if (!visible) {
networkID = "" networkID = "";
networkData = null networkData = null;
} }
} }
@@ -71,7 +72,6 @@ DankModal {
width: parent.width width: parent.width
elide: Text.ElideRight elide: Text.ElideRight
} }
} }
DankActionButton { DankActionButton {
@@ -80,7 +80,6 @@ DankModal {
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: root.hideDialog() onClicked: root.hideDialog()
} }
} }
Rectangle { Rectangle {
@@ -109,7 +108,6 @@ DankModal {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
} }
} }
} }
Item { Item {
@@ -148,17 +146,10 @@ DankModal {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
} }
} }
+36 -35
View File
@@ -1,72 +1,77 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import Quickshell.Io import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Modules.Notifications.Center import qs.Modules.Notifications.Center
import qs.Services import qs.Services
import qs.Widgets
DankModal { DankModal {
id: notificationModal id: notificationModal
layerNamespace: "dms:notification-center-modal" layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal]
active: CompositorService.isHyprland && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false property bool notificationModalOpen: false
property var notificationListRef: null property var notificationListRef: null
function show() { function show() {
notificationModalOpen = true notificationModalOpen = true;
NotificationService.onOverlayOpen() NotificationService.onOverlayOpen();
open() open();
modalKeyboardController.reset() modalKeyboardController.reset();
if (modalKeyboardController && notificationListRef) { if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef modalKeyboardController.listView = notificationListRef;
modalKeyboardController.rebuildFlatNavigation() modalKeyboardController.rebuildFlatNavigation();
Qt.callLater(() => { Qt.callLater(() => {
modalKeyboardController.keyboardNavigationActive = true modalKeyboardController.keyboardNavigationActive = true;
modalKeyboardController.selectedFlatIndex = 0 modalKeyboardController.selectedFlatIndex = 0;
modalKeyboardController.updateSelectedIdFromIndex() modalKeyboardController.updateSelectedIdFromIndex();
if (notificationListRef) { if (notificationListRef) {
notificationListRef.keyboardActive = true notificationListRef.keyboardActive = true;
notificationListRef.currentIndex = 0 notificationListRef.currentIndex = 0;
} }
modalKeyboardController.selectionVersion++ modalKeyboardController.selectionVersion++;
modalKeyboardController.ensureVisible() modalKeyboardController.ensureVisible();
}) });
} }
} }
function hide() { function hide() {
notificationModalOpen = false notificationModalOpen = false;
NotificationService.onOverlayClose() NotificationService.onOverlayClose();
close() close();
modalKeyboardController.reset() modalKeyboardController.reset();
} }
function toggle() { function toggle() {
if (shouldBeVisible) { if (shouldBeVisible) {
hide() hide();
} else { } else {
show() show();
} }
} }
width: 500 modalWidth: 500
height: 700 modalHeight: 700
visible: false visible: false
onBackgroundClicked: hide() onBackgroundClicked: hide()
onOpened: () => { onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus()); Qt.callLater(() => modalFocusScope.forceActiveFocus());
} }
onShouldBeVisibleChanged: (shouldBeVisible) => { onShouldBeVisibleChanged: shouldBeVisible => {
if (!shouldBeVisible) { if (!shouldBeVisible) {
notificationModalOpen = false notificationModalOpen = false;
modalKeyboardController.reset() modalKeyboardController.reset();
NotificationService.onOverlayClose() NotificationService.onOverlayClose();
} }
} }
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event) modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event)
NotificationKeyboardController { NotificationKeyboardController {
id: modalKeyboardController id: modalKeyboardController
@@ -125,14 +130,13 @@ DankModal {
height: parent.height - y height: parent.height - y
keyboardController: modalKeyboardController keyboardController: modalKeyboardController
Component.onCompleted: { Component.onCompleted: {
notificationModal.notificationListRef = notificationList notificationModal.notificationListRef = notificationList;
if (modalKeyboardController) { if (modalKeyboardController) {
modalKeyboardController.listView = notificationList modalKeyboardController.listView = notificationList;
modalKeyboardController.rebuildFlatNavigation() modalKeyboardController.rebuildFlatNavigation();
} }
} }
} }
} }
NotificationKeyboardHints { NotificationKeyboardHints {
@@ -144,9 +148,6 @@ DankModal {
anchors.margins: Theme.spacingL anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints showHints: modalKeyboardController.showKeyboardHints
} }
} }
} }
} }
+41 -35
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -9,33 +10,38 @@ DankModal {
layerNamespace: "dms:polkit" layerNamespace: "dms:polkit"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string passwordInput: "" property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false property bool isLoading: false
property real minHeight: 240 property real minHeight: 240
function show() { function show() {
passwordInput = "" passwordInput = "";
isLoading = false isLoading = false;
open() open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) { if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus() contentLoader.item.passwordField.forceActiveFocus();
} }
}) });
} }
shouldBeVisible: false shouldBeVisible: false
width: 420 modalWidth: 420
height: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240) modalHeight: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
Connections { Connections {
target: contentLoader.item target: contentLoader.item
function onImplicitHeightChanged() { function onImplicitHeightChanged() {
if (shouldBeVisible && contentLoader.item) { if (shouldBeVisible && contentLoader.item) {
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2 const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2;
if (newHeight > minHeight) { if (newHeight > minHeight) {
minHeight = newHeight minHeight = newHeight;
} }
} }
} }
@@ -44,19 +50,19 @@ DankModal {
onOpened: { onOpened: {
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) { if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus() contentLoader.item.passwordField.forceActiveFocus();
} }
}) });
} }
onClosed: { onDialogClosed: {
passwordInput = "" passwordInput = "";
isLoading = false isLoading = false;
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
if (currentFlow && !isLoading) { if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest() currentFlow.cancelAuthenticationRequest();
} }
} }
@@ -65,12 +71,12 @@ DankModal {
enabled: PolkitService.polkitAvailable enabled: PolkitService.polkitAvailable
function onAuthenticationRequestStarted() { function onAuthenticationRequestStarted() {
show() show();
} }
function onIsActiveChanged() { function onIsActiveChanged() {
if (!(PolkitService.agent?.isActive ?? false)) { if (!(PolkitService.agent?.isActive ?? false)) {
close() close();
} }
} }
} }
@@ -81,24 +87,24 @@ DankModal {
function onIsResponseRequiredChanged() { function onIsResponseRequiredChanged() {
if (currentFlow.isResponseRequired) { if (currentFlow.isResponseRequired) {
isLoading = false isLoading = false;
passwordInput = "" passwordInput = "";
if (contentLoader.item && contentLoader.item.passwordField) { if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus() contentLoader.item.passwordField.forceActiveFocus();
} }
} }
} }
function onAuthenticationSucceeded() { function onAuthenticationSucceeded() {
close() close();
} }
function onAuthenticationFailed() { function onAuthenticationFailed() {
isLoading = false isLoading = false;
} }
function onAuthenticationRequestCancelled() { function onAuthenticationRequestCancelled() {
close() close();
} }
} }
@@ -114,9 +120,9 @@ DankModal {
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
if (currentFlow && !isLoading) { if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest() currentFlow.cancelAuthenticationRequest();
} }
event.accepted = true event.accepted = true;
} }
Row { Row {
@@ -171,7 +177,7 @@ DankModal {
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
onClicked: () => { onClicked: () => {
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest() currentFlow.cancelAuthenticationRequest();
} }
} }
} }
@@ -208,7 +214,7 @@ DankModal {
anchors.fill: parent anchors.fill: parent
enabled: !isLoading enabled: !isLoading
onClicked: () => { onClicked: () => {
passwordField.forceActiveFocus() passwordField.forceActiveFocus();
} }
} }
@@ -224,13 +230,13 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: !isLoading enabled: !isLoading
onTextEdited: () => { onTextEdited: () => {
passwordInput = text passwordInput = text;
} }
onAccepted: () => { onAccepted: () => {
if (passwordInput.length > 0 && currentFlow && !isLoading) { if (passwordInput.length > 0 && currentFlow && !isLoading) {
isLoading = true isLoading = true;
currentFlow.submit(passwordInput) currentFlow.submit(passwordInput);
passwordInput = "" passwordInput = "";
} }
} }
} }
@@ -303,7 +309,7 @@ DankModal {
enabled: parent.enabled enabled: parent.enabled
onClicked: () => { onClicked: () => {
if (currentFlow) { if (currentFlow) {
currentFlow.cancelAuthenticationRequest() currentFlow.cancelAuthenticationRequest();
} }
} }
} }
@@ -336,9 +342,9 @@ DankModal {
enabled: parent.enabled enabled: parent.enabled
onClicked: () => { onClicked: () => {
if (currentFlow && !isLoading) { if (currentFlow && !isLoading) {
isLoading = true isLoading = true;
currentFlow.submit(passwordInput) currentFlow.submit(passwordInput);
passwordInput = "" passwordInput = "";
} }
} }
} }
+241 -239
View File
@@ -1,5 +1,6 @@
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:power-menu" layerNamespace: "dms:power-menu"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property int selectedIndex: 0 property int selectedIndex: 0
property int selectedRow: 0 property int selectedRow: 0
property int selectedCol: 0 property int selectedCol: 0
@@ -23,64 +29,64 @@ DankModal {
signal lockRequested signal lockRequested
function openCentered() { function openCentered() {
parentBounds = Qt.rect(0, 0, 0, 0) parentBounds = Qt.rect(0, 0, 0, 0);
parentScreen = null parentScreen = null;
backgroundOpacity = 0.5 backgroundOpacity = 0.5;
open() open();
} }
function openFromControlCenter(bounds, targetScreen) { function openFromControlCenter(bounds, targetScreen) {
parentBounds = bounds parentBounds = bounds;
parentScreen = targetScreen parentScreen = targetScreen;
backgroundOpacity = 0 backgroundOpacity = 0;
keepPopoutsOpen = true keepPopoutsOpen = true;
open() open();
keepPopoutsOpen = false keepPopoutsOpen = false;
} }
function updateVisibleActions() { function updateVisibleActions() {
const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"] const allActions = SettingsData.powerMenuActions || ["reboot", "logout", "poweroff", "lock", "suspend", "restart"];
visibleActions = allActions.filter(action => { visibleActions = allActions.filter(action => {
if (action === "hibernate" && !SessionService.hibernateSupported) if (action === "hibernate" && !SessionService.hibernateSupported)
return false return false;
return true return true;
}) });
if (!SettingsData.powerMenuGridLayout) return if (!SettingsData.powerMenuGridLayout)
return;
const count = visibleActions.length const count = visibleActions.length;
if (count === 0) { if (count === 0) {
gridColumns = 1 gridColumns = 1;
gridRows = 1 gridRows = 1;
return return;
} }
if (count <= 3) { if (count <= 3) {
gridColumns = 1 gridColumns = 1;
gridRows = count gridRows = count;
return return;
} }
if (count === 4) { if (count === 4) {
gridColumns = 2 gridColumns = 2;
gridRows = 2 gridRows = 2;
return return;
} }
gridColumns = 3 gridColumns = 3;
gridRows = Math.ceil(count / 3) gridRows = Math.ceil(count / 3);
} }
function getDefaultActionIndex() { function getDefaultActionIndex() {
const defaultAction = SettingsData.powerMenuDefaultAction || "logout" const defaultAction = SettingsData.powerMenuDefaultAction || "logout";
const index = visibleActions.indexOf(defaultAction) const index = visibleActions.indexOf(defaultAction);
return index >= 0 ? index : 0 return index >= 0 ? index : 0;
} }
function getActionAtIndex(index) { function getActionAtIndex(index) {
if (index < 0 || index >= visibleActions.length) if (index < 0 || index >= visibleActions.length)
return "" return "";
return visibleActions[index] return visibleActions[index];
} }
function getActionData(action) { function getActionData(action) {
@@ -90,64 +96,64 @@ DankModal {
"icon": "restart_alt", "icon": "restart_alt",
"label": I18n.tr("Reboot"), "label": I18n.tr("Reboot"),
"key": "R" "key": "R"
} };
case "logout": case "logout":
return { return {
"icon": "logout", "icon": "logout",
"label": I18n.tr("Log Out"), "label": I18n.tr("Log Out"),
"key": "X" "key": "X"
} };
case "poweroff": case "poweroff":
return { return {
"icon": "power_settings_new", "icon": "power_settings_new",
"label": I18n.tr("Power Off"), "label": I18n.tr("Power Off"),
"key": "P" "key": "P"
} };
case "lock": case "lock":
return { return {
"icon": "lock", "icon": "lock",
"label": I18n.tr("Lock"), "label": I18n.tr("Lock"),
"key": "L" "key": "L"
} };
case "suspend": case "suspend":
return { return {
"icon": "bedtime", "icon": "bedtime",
"label": I18n.tr("Suspend"), "label": I18n.tr("Suspend"),
"key": "S" "key": "S"
} };
case "hibernate": case "hibernate":
return { return {
"icon": "ac_unit", "icon": "ac_unit",
"label": I18n.tr("Hibernate"), "label": I18n.tr("Hibernate"),
"key": "H" "key": "H"
} };
case "restart": case "restart":
return { return {
"icon": "refresh", "icon": "refresh",
"label": I18n.tr("Restart DMS"), "label": I18n.tr("Restart DMS"),
"key": "D" "key": "D"
} };
default: default:
return { return {
"icon": "help", "icon": "help",
"label": action, "label": action,
"key": "?" "key": "?"
} };
} }
} }
function selectOption(action) { function selectOption(action) {
if (action === "lock") { if (action === "lock") {
close() close();
lockRequested() lockRequested();
return return;
} }
if (action === "restart") { if (action === "restart") {
close() close();
Quickshell.execDetached(["dms", "restart"]) Quickshell.execDetached(["dms", "restart"]);
return return;
} }
close() close();
const actions = { const actions = {
"logout": { "logout": {
"title": I18n.tr("Log Out"), "title": I18n.tr("Log Out"),
@@ -169,227 +175,223 @@ DankModal {
"title": I18n.tr("Power Off"), "title": I18n.tr("Power Off"),
"message": I18n.tr("Are you sure you want to power off the system?") "message": I18n.tr("Are you sure you want to power off the system?")
} }
} };
const selected = actions[action] const selected = actions[action];
if (selected) { if (selected) {
root.powerActionRequested(action, selected.title, selected.message) root.powerActionRequested(action, selected.title, selected.message);
} }
} }
shouldBeVisible: false shouldBeVisible: false
width: SettingsData.powerMenuGridLayout modalWidth: SettingsData.powerMenuGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 400
? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) modalHeight: contentLoader.item ? contentLoader.item.implicitHeight : 300
: 400
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
enableShadow: true enableShadow: true
screen: parentScreen targetScreen: parentScreen
positioning: parentBounds.width > 0 ? "custom" : "center" positioning: parentBounds.width > 0 ? "custom" : "center"
customPosition: { customPosition: {
if (parentBounds.width > 0) { if (parentBounds.width > 0) {
const effectiveBarThickness = Math.max(26 + SettingsData.dankBarInnerPadding * 0.6 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding)) const effectiveBarThickness = Math.max(26 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) * 0.6 + (SettingsData.barConfigs[0]?.innerPadding ?? 4) + 4, Theme.barHeight - 4 - (8 - (SettingsData.barConfigs[0]?.innerPadding ?? 4)));
const barExclusionZone = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap const barExclusionZone = effectiveBarThickness + (SettingsData.barConfigs[0]?.spacing ?? 4) + (SettingsData.barConfigs[0]?.bottomGap ?? 0);
const screenW = parentScreen?.width ?? 1920 const screenW = parentScreen?.width ?? 1920;
const screenH = parentScreen?.height ?? 1080 const screenH = parentScreen?.height ?? 1080;
const margin = Theme.spacingL const margin = Theme.spacingL;
let targetX = parentBounds.x + (parentBounds.width - width) / 2 let targetX = parentBounds.x + (parentBounds.width - modalWidth) / 2;
let targetY = parentBounds.y + (parentBounds.height - height) / 2 let targetY = parentBounds.y + (parentBounds.height - modalHeight) / 2;
const minY = SettingsData.dankBarPosition === SettingsData.Position.Top ? barExclusionZone + margin : margin const minY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Top ? barExclusionZone + margin : margin;
const maxY = SettingsData.dankBarPosition === SettingsData.Position.Bottom ? screenH - height - barExclusionZone - margin : screenH - height - margin const maxY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Bottom ? screenH - modalHeight - barExclusionZone - margin : screenH - modalHeight - margin;
targetY = Math.max(minY, Math.min(maxY, targetY)) targetY = Math.max(minY, Math.min(maxY, targetY));
return Qt.point(targetX, targetY) return Qt.point(targetX, targetY);
} }
return Qt.point(0, 0) return Qt.point(0, 0);
} }
onBackgroundClicked: () => close() onBackgroundClicked: () => close()
onOpened: () => { onOpened: () => {
updateVisibleActions() updateVisibleActions();
const defaultIndex = getDefaultActionIndex() const defaultIndex = getDefaultActionIndex();
if (SettingsData.powerMenuGridLayout) { if (SettingsData.powerMenuGridLayout) {
selectedRow = Math.floor(defaultIndex / gridColumns) selectedRow = Math.floor(defaultIndex / gridColumns);
selectedCol = defaultIndex % gridColumns selectedCol = defaultIndex % gridColumns;
selectedIndex = defaultIndex selectedIndex = defaultIndex;
} else { } else {
selectedIndex = defaultIndex selectedIndex = defaultIndex;
} }
Qt.callLater(() => modalFocusScope.forceActiveFocus()) Qt.callLater(() => modalFocusScope.forceActiveFocus());
} }
Component.onCompleted: updateVisibleActions() Component.onCompleted: updateVisibleActions()
modalFocusScope.Keys.onPressed: event => { modalFocusScope.Keys.onPressed: event => {
if (SettingsData.powerMenuGridLayout) { if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event) handleGridNavigation(event);
} else { } else {
handleListNavigation(event) handleListNavigation(event);
} }
} }
function handleListNavigation(event) { function handleListNavigation(event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Up: case Qt.Key_Up:
case Qt.Key_Backtab: case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Down: case Qt.Key_Down:
case Qt.Key_Tab: case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % visibleActions.length selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex)) selectOption(getActionAtIndex(selectedIndex));
event.accepted = true event.accepted = true;
break break;
case Qt.Key_N: case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_P: case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) { if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff") selectOption("poweroff");
event.accepted = true event.accepted = true;
} else { } else {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_J: case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_K: case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_R: case Qt.Key_R:
selectOption("reboot") selectOption("reboot");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_X: case Qt.Key_X:
selectOption("logout") selectOption("logout");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_L: case Qt.Key_L:
selectOption("lock") selectOption("lock");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_S: case Qt.Key_S:
selectOption("suspend") selectOption("suspend");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_H: case Qt.Key_H:
selectOption("hibernate") selectOption("hibernate");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_D: case Qt.Key_D:
selectOption("restart") selectOption("restart");
event.accepted = true event.accepted = true;
break break;
} }
} }
function handleGridNavigation(event) { function handleGridNavigation(event) {
switch (event.key) { switch (event.key) {
case Qt.Key_Left: case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Right: case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Up: case Qt.Key_Up:
case Qt.Key_Backtab: case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Down: case Qt.Key_Down:
case Qt.Key_Tab: case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
break break;
case Qt.Key_Return: case Qt.Key_Return:
case Qt.Key_Enter: case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex)) selectOption(getActionAtIndex(selectedIndex));
event.accepted = true event.accepted = true;
break break;
case Qt.Key_N: case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedCol = (selectedCol + 1) % gridColumns selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_P: case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) { if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff") selectOption("poweroff");
event.accepted = true event.accepted = true;
} else { } else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_J: case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow + 1) % gridRows selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_K: case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) { if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow - 1 + gridRows) % gridRows selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true event.accepted = true;
} }
break break;
case Qt.Key_R: case Qt.Key_R:
selectOption("reboot") selectOption("reboot");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_X: case Qt.Key_X:
selectOption("logout") selectOption("logout");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_L: case Qt.Key_L:
selectOption("lock") selectOption("lock");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_S: case Qt.Key_S:
selectOption("suspend") selectOption("suspend");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_H: case Qt.Key_H:
selectOption("hibernate") selectOption("hibernate");
event.accepted = true event.accepted = true;
break break;
case Qt.Key_D: case Qt.Key_D:
selectOption("restart") selectOption("restart");
event.accepted = true event.accepted = true;
break break;
} }
} }
content: Component { content: Component {
Item { Item {
anchors.fill: parent anchors.fill: parent
implicitHeight: SettingsData.powerMenuGridLayout implicitHeight: SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight + Theme.spacingL * 2 : buttonColumn.implicitHeight + Theme.spacingL * 2
? buttonGrid.implicitHeight + Theme.spacingL * 2
: buttonColumn.implicitHeight + Theme.spacingL * 2
Grid { Grid {
id: buttonGrid id: buttonGrid
@@ -410,15 +412,15 @@ DankModal {
readonly property bool isSelected: root.selectedIndex === index readonly property bool isSelected: root.selectedIndex === index
readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff" readonly property bool showWarning: modelData === "reboot" || modelData === "poweroff"
width: (root.width - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns width: (root.modalWidth - Theme.spacingL * 2 - Theme.spacingS * (root.gridColumns - 1)) / root.gridColumns
height: 100 height: 100
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isSelected) if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
if (mouseArea.containsMouse) if (mouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
} }
border.color: isSelected ? Theme.primary : "transparent" border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
@@ -432,9 +434,9 @@ DankModal {
size: Theme.iconSize + 8 size: Theme.iconSize + 8
color: { color: {
if (parent.parent.showWarning && mouseArea.containsMouse) { if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
} }
@@ -444,9 +446,9 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (parent.parent.showWarning && mouseArea.containsMouse) { if (parent.parent.showWarning && mouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
@@ -475,9 +477,9 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.selectedRow = Math.floor(index / root.gridColumns) root.selectedRow = Math.floor(index / root.gridColumns);
root.selectedCol = index % root.gridColumns root.selectedCol = index % root.gridColumns;
root.selectOption(modelData) root.selectOption(modelData);
} }
} }
} }
@@ -512,10 +514,10 @@ DankModal {
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: { color: {
if (isSelected) if (isSelected)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.12);
if (listMouseArea.containsMouse) if (listMouseArea.containsMouse)
return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08) return Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08) return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
} }
border.color: isSelected ? Theme.primary : "transparent" border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0 border.width: isSelected ? 2 : 0
@@ -535,9 +537,9 @@ DankModal {
size: Theme.iconSize + 4 size: Theme.iconSize + 4
color: { color: {
if (parent.parent.showWarning && listMouseArea.containsMouse) { if (parent.parent.showWarning && listMouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
@@ -547,9 +549,9 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: { color: {
if (parent.parent.showWarning && listMouseArea.containsMouse) { if (parent.parent.showWarning && listMouseArea.containsMouse) {
return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning return parent.parent.modelData === "poweroff" ? Theme.error : Theme.warning;
} }
return Theme.surfaceText return Theme.surfaceText;
} }
font.weight: Font.Medium font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
@@ -582,8 +584,8 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: { onClicked: {
root.selectedIndex = index root.selectedIndex = index;
root.selectOption(modelData) root.selectOption(modelData);
} }
} }
} }
+278 -295
View File
@@ -1,56 +1,62 @@
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import qs.Common import qs.Common
import qs.Modals.Common
import qs.Modules.ProcessList import qs.Modules.ProcessList
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
DankModal { FloatingWindow {
id: processListModal id: processListModal
layerNamespace: "dms:process-list-modal"
property int currentTab: 0 property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"] property var tabNames: ["Processes", "Performance", "System"]
property bool shouldHaveFocus: visible
property alias shouldBeVisible: processListModal.visible
signal closingModal
function show() { function show() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); console.warn("ProcessListModal: dgop is not available");
return ; return;
} }
open(); visible = true;
UserInfoService.getUptime(); UserInfoService.getUptime();
} }
function hide() { function hide() {
close(); visible = false;
if (processContextMenu.visible) { if (processContextMenu.visible) {
processContextMenu.close(); processContextMenu.close();
} }
} }
function toggle() { function toggle() {
if (!DgopService.dgopAvailable) { if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available"); console.warn("ProcessListModal: dgop is not available");
return ; return;
}
if (shouldBeVisible) {
hide();
} else {
show();
} }
visible = !visible;
} }
width: 900 objectName: "processListModal"
height: 680 title: I18n.tr("System Monitor", "sysmon window title")
implicitWidth: 900
implicitHeight: 680
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius onVisibleChanged: {
enableShadow: true if (!visible) {
onBackgroundClicked: () => { closingModal();
return hide(); } else {
Qt.callLater(() => {
if (contentFocusScope) {
contentFocusScope.forceActiveFocus();
}
});
}
} }
Component { Component {
@@ -59,300 +65,277 @@ DankModal {
ProcessesTab { ProcessesTab {
contextMenu: processContextMenu contextMenu: processContextMenu
} }
} }
Component { Component {
id: performanceTabComponent id: performanceTabComponent
PerformanceTab { PerformanceTab {}
}
} }
Component { Component {
id: systemTabComponent id: systemTabComponent
SystemTab { SystemTab {}
}
} }
ProcessContextMenu { ProcessContextMenu {
id: processContextMenu id: processContextMenu
} }
content: Component { FocusScope {
Item { id: contentFocusScope
anchors.fill: parent
focus: true anchors.fill: parent
Keys.onPressed: (event) => { focus: true
if (event.key === Qt.Key_Escape) {
processListModal.hide(); Keys.onPressed: event => {
event.accepted = true; if (event.key === Qt.Key_Escape) {
} else if (event.key === Qt.Key_1) { hide();
currentTab = 0; event.accepted = true;
event.accepted = true; return;
} else if (event.key === Qt.Key_2) {
currentTab = 1;
event.accepted = true;
} else if (event.key === Qt.Key_3) {
currentTab = 2;
event.accepted = true;
}
} }
// Show error message when dgop is not available switch (event.key) {
Rectangle { case Qt.Key_1:
anchors.centerIn: parent currentTab = 0;
width: 400 event.accepted = true;
height: 200 return;
radius: Theme.cornerRadius case Qt.Key_2:
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1) currentTab = 1;
border.color: Theme.error event.accepted = true;
border.width: 2 return;
visible: !DgopService.dgopAvailable case Qt.Key_3:
currentTab = 2;
Column { event.accepted = true;
anchors.centerIn: parent return;
spacing: Theme.spacingL
DankIcon {
name: "error"
size: 48
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("System Monitor Unavailable")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
} }
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
visible: DgopService.dgopAvailable
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: I18n.tr("System Monitor")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return processListModal.hide();
}
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
const tabIcons = ["list_alt", "analytics", "settings"];
return tabIcons[index] || "tab";
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
} }
} Rectangle {
anchors.centerIn: parent
width: 400
height: 200
radius: Theme.cornerRadius
color: Qt.rgba(Theme.error.r, Theme.error.g, Theme.error.b, 0.1)
border.color: Theme.error
border.width: 2
visible: !DgopService.dgopAvailable
Column {
anchors.centerIn: parent
spacing: Theme.spacingL
DankIcon {
name: "error"
size: 48
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("System Monitor Unavailable")
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Bold
color: Theme.error
anchors.horizontalCenter: parent.horizontalCenter
}
StyledText {
text: I18n.tr("The 'dgop' tool is required for system monitoring.\nPlease install dgop to use this feature.")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
wrapMode: Text.WordWrap
}
}
}
ColumnLayout {
anchors.fill: parent
anchors.margins: Theme.spacingL
spacing: Theme.spacingL
visible: DgopService.dgopAvailable
RowLayout {
Layout.fillWidth: true
height: 40
StyledText {
text: I18n.tr("System Monitor")
font.pixelSize: Theme.fontSizeLarge + 4
font.weight: Font.Bold
color: Theme.surfaceText
Layout.alignment: Qt.AlignVCenter
}
Item {
Layout.fillWidth: true
}
DankActionButton {
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
processListModal.hide();
}
Layout.alignment: Qt.AlignVCenter
}
}
Rectangle {
Layout.fillWidth: true
height: 52
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
radius: Theme.cornerRadius
border.color: Theme.outlineLight
border.width: 1
Row {
anchors.fill: parent
anchors.margins: 4
spacing: 2
Repeater {
model: tabNames
Rectangle {
width: (parent.width - (tabNames.length - 1) * 2) / tabNames.length
height: 44
radius: Theme.cornerRadius
color: currentTab === index ? Theme.primaryPressed : (tabMouseArea.containsMouse ? Theme.primaryHoverLight : "transparent")
border.color: currentTab === index ? Theme.primary : "transparent"
border.width: currentTab === index ? 1 : 0
Row {
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: {
const tabIcons = ["list_alt", "analytics", "settings"];
return tabIcons[index] || "tab";
}
size: Theme.iconSize - 2
color: currentTab === index ? Theme.primary : Theme.surfaceText
opacity: currentTab === index ? 1 : 0.7
anchors.verticalCenter: parent.verticalCenter
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
StyledText {
text: modelData
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
color: currentTab === index ? Theme.primary : Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
anchors.verticalCenterOffset: -1
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
MouseArea {
id: tabMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
currentTab = index;
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
}
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
}
}
}
}
}
}
Rectangle {
Layout.fillWidth: true
Layout.fillHeight: true
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
border.color: Theme.outlineLight
border.width: 1
Loader {
id: processesTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 0
visible: currentTab === 0
opacity: currentTab === 0 ? 1 : 0
sourceComponent: processesTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: performanceTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 1
visible: currentTab === 1
opacity: currentTab === 1 ? 1 : 0
sourceComponent: performanceTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
Loader {
id: systemTab
anchors.fill: parent
anchors.margins: Theme.spacingS
active: processListModal.visible && currentTab === 2
visible: currentTab === 2
opacity: currentTab === 2 ? 1 : 0
sourceComponent: systemTabComponent
Behavior on opacity {
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.emphasizedEasing
}
}
}
}
}
}
} }
+5 -5
View File
@@ -22,7 +22,7 @@ Item {
width: parent.width width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2 height: lockScreenSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0 border.width: 0
@@ -106,7 +106,7 @@ Item {
width: parent.width width: parent.width
height: timeoutSection.implicitHeight + Theme.spacingL * 2 height: timeoutSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0 border.width: 0
@@ -329,7 +329,7 @@ Item {
width: parent.width width: parent.width
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2 height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0 border.width: 0
@@ -516,7 +516,7 @@ Item {
width: parent.width width: parent.width
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2 height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0 border.width: 0
@@ -560,7 +560,7 @@ Item {
width: parent.width width: parent.width
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2 height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.3) color: Theme.surfaceContainerHigh
border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2) border.color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
border.width: 0 border.width: 0
+164 -169
View File
@@ -1,142 +1,87 @@
import QtQuick import QtQuick
import QtQuick.Effects import Quickshell
import Quickshell.Io
import qs.Common import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser import qs.Modals.FileBrowser
import qs.Modules.Settings
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
DankModal { FloatingWindow {
id: settingsModal id: settingsModal
layerNamespace: "dms:settings"
property Component settingsContent
property alias profileBrowser: profileBrowser property alias profileBrowser: profileBrowser
property alias wallpaperBrowser: wallpaperBrowser
property int currentTabIndex: 0 property int currentTabIndex: 0
property bool shouldHaveFocus: visible
property bool allowFocusOverride: false
property alias shouldBeVisible: settingsModal.visible
property bool isCompactMode: width < 700
property bool menuVisible: !isCompactMode
property bool enableAnimations: true
signal closingModal() signal closingModal
function show() { function show() {
open(); visible = true;
} }
function hide() { function hide() {
close(); visible = false;
} }
function toggle() { function toggle() {
if (shouldBeVisible) { visible = !visible;
hide(); }
} else {
show(); function toggleMenu() {
} enableAnimations = true;
menuVisible = !menuVisible;
} }
objectName: "settingsModal" objectName: "settingsModal"
width: Math.min(800, screenWidth * 0.9) title: I18n.tr("Settings", "settings window title")
height: Math.min(800, screenHeight * 0.85) implicitWidth: 800
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) implicitHeight: 800
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false visible: false
onBackgroundClicked: () => {
return hide(); onIsCompactModeChanged: {
} enableAnimations = false;
content: settingsContent if (!isCompactMode) {
onOpened: () => { menuVisible = true;
}
Qt.callLater(() => { Qt.callLater(() => {
modalFocusScope.forceActiveFocus() enableAnimations = true;
if (contentLoader.item) { });
contentLoader.item.forceActiveFocus()
}
})
} }
onVisibleChanged: { onVisibleChanged: {
if (visible && shouldBeVisible) { if (!visible) {
closingModal();
} else {
Qt.callLater(() => { Qt.callLater(() => {
modalFocusScope.forceActiveFocus() if (contentFocusScope) {
if (contentLoader.item) { contentFocusScope.forceActiveFocus();
contentLoader.item.forceActiveFocus()
} }
}) });
} }
} }
modalFocusScope.Keys.onPressed: event => {
const tabCount = 11
if (event.key === Qt.Key_Down) {
currentTabIndex = (currentTabIndex + 1) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Up) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Tab && !event.modifiers) {
currentTabIndex = (currentTabIndex + 1) % tabCount
event.accepted = true
} else if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount
event.accepted = true
}
}
IpcHandler {
function open(): string {
settingsModal.show();
return "SETTINGS_OPEN_SUCCESS";
}
function close(): string {
settingsModal.hide();
return "SETTINGS_CLOSE_SUCCESS";
}
function toggle(): string {
settingsModal.toggle();
return "SETTINGS_TOGGLE_SUCCESS";
}
target: "settings"
}
IpcHandler {
function browse(type: string) {
if (type === "wallpaper") {
wallpaperBrowser.allowStacking = false;
wallpaperBrowser.open();
} else if (type === "profile") {
profileBrowser.allowStacking = false;
profileBrowser.open();
}
}
target: "file"
}
FileBrowserModal { FileBrowserModal {
id: profileBrowser id: profileBrowser
allowStacking: true allowStacking: true
parentModal: settingsModal parentModal: settingsModal
browserTitle: "Select Profile Image" browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
browserIcon: "person" browserIcon: "person"
browserType: "profile" browserType: "profile"
showHiddenFiles: true showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => { onFileSelected: path => {
PortalService.setProfileImage(path); PortalService.setProfileImage(path);
close(); close();
} }
onDialogClosed: () => { onDialogClosed: () => {
allowStacking = true; allowStacking = true;
if (settingsModal.shouldBeVisible) {
Qt.callLater(() => {
settingsModal.modalFocusScope.forceActiveFocus()
if (settingsModal.contentLoader.item) {
settingsModal.contentLoader.item.forceActiveFocus()
}
})
}
} }
} }
@@ -145,116 +90,166 @@ DankModal {
allowStacking: true allowStacking: true
parentModal: settingsModal parentModal: settingsModal
browserTitle: "Select Wallpaper" browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper" browserIcon: "wallpaper"
browserType: "wallpaper" browserType: "wallpaper"
showHiddenFiles: true showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"] fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => { onFileSelected: path => {
SessionData.setWallpaper(path); SessionData.setWallpaper(path);
close(); close();
} }
onDialogClosed: () => { onDialogClosed: () => {
allowStacking = true; allowStacking = true;
if (settingsModal.shouldBeVisible) {
Qt.callLater(() => {
settingsModal.modalFocusScope.forceActiveFocus()
if (settingsModal.contentLoader.item) {
settingsModal.contentLoader.item.forceActiveFocus()
}
})
}
} }
} }
settingsContent: Component { FocusScope {
Item { id: contentFocusScope
id: rootScope
anchors.fill: parent
Keys.onEscapePressed: event => { anchors.fill: parent
settingsModal.hide() focus: true
event.accepted = true
Keys.onPressed: event => {
const tabCount = 11;
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
} }
if (event.key === Qt.Key_Down) {
currentTabIndex = (currentTabIndex + 1) % tabCount;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Up) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Tab && !event.modifiers) {
currentTabIndex = (currentTabIndex + 1) % tabCount;
event.accepted = true;
return;
}
if (event.key === Qt.Key_Backtab || (event.key === Qt.Key_Tab && event.modifiers & Qt.ShiftModifier)) {
currentTabIndex = (currentTabIndex - 1 + tabCount) % tabCount;
event.accepted = true;
return;
}
}
Column { Column {
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: Theme.spacingL spacing: 0
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
spacing: 0
Item { Item {
width: parent.width width: parent.width
height: 35 height: 48
z: 10
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Settings")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
return settingsModal.hide();
}
}
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainer
opacity: 0.5
} }
Row { Row {
width: parent.width anchors.left: parent.left
height: parent.height - 35 anchors.leftMargin: Theme.spacingL
spacing: 0 anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
SettingsSidebar { DankActionButton {
id: sidebar visible: settingsModal.isCompactMode
circular: false
parentModal: settingsModal iconName: "menu"
currentIndex: settingsModal.currentTabIndex iconSize: Theme.iconSize - 4
onCurrentIndexChanged: { iconColor: Theme.surfaceText
settingsModal.currentTabIndex = currentIndex anchors.verticalCenter: parent.verticalCenter
onClicked: () => {
settingsModal.toggleMenu();
} }
} }
DankIcon {
name: "settings"
size: Theme.iconSize
color: Theme.primary
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: I18n.tr("Settings")
font.pixelSize: Theme.fontSizeXLarge
color: Theme.surfaceText
font.weight: Font.Medium
anchors.verticalCenter: parent.verticalCenter
}
}
DankActionButton {
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.top: parent.top
anchors.topMargin: Theme.spacingM
circular: false
iconName: "close"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
settingsModal.hide();
}
}
}
Item {
width: parent.width
height: parent.height - 48
clip: true
SettingsSidebar {
id: sidebar
x: 0
width: settingsModal.isCompactMode ? parent.width : 270
visible: settingsModal.isCompactMode ? settingsModal.menuVisible : true
parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex
onCurrentIndexChanged: {
settingsModal.currentTabIndex = currentIndex;
if (settingsModal.isCompactMode) {
settingsModal.enableAnimations = true;
settingsModal.menuVisible = false;
}
}
}
Item {
x: settingsModal.isCompactMode ? (settingsModal.menuVisible ? parent.width : 0) : sidebar.width
width: settingsModal.isCompactMode ? parent.width : parent.width - sidebar.width
height: parent.height
clip: true
SettingsContent { SettingsContent {
id: content id: content
width: parent.width - sidebar.width width: Math.min(550, parent.width - Theme.spacingL * 2)
height: parent.height height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
parentModal: settingsModal parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex currentIndex: settingsModal.currentTabIndex
} }
Behavior on x {
enabled: settingsModal.enableAnimations
NumberAnimation {
duration: Theme.mediumDuration
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
} }
} }
} }
} }
} }
+58 -52
View File
@@ -10,47 +10,59 @@ Rectangle {
property int currentIndex: 0 property int currentIndex: 0
property var parentModal: null property var parentModal: null
readonly property var sidebarItems: [{ readonly property var sidebarItems: [
"text": I18n.tr("Personalization"), {
"icon": "person" "text": I18n.tr("Personalization"),
}, { "icon": "person"
"text": I18n.tr("Time & Weather"), },
"icon": "schedule" {
}, { "text": I18n.tr("Time & Weather"),
"text": I18n.tr("Dank Bar"), "icon": "schedule"
"icon": "toolbar" },
}, { {
"text": I18n.tr("Widgets"), "text": I18n.tr("Dank Bar"),
"icon": "widgets" "icon": "toolbar"
}, { },
"text": I18n.tr("Dock"), {
"icon": "dock_to_bottom" "text": I18n.tr("Widgets"),
}, { "icon": "widgets"
"text": I18n.tr("Displays"), },
"icon": "monitor" {
}, { "text": I18n.tr("Dock"),
"text": I18n.tr("Launcher"), "icon": "dock_to_bottom"
"icon": "apps" },
}, { {
"text": I18n.tr("Theme & Colors"), "text": I18n.tr("Displays"),
"icon": "palette" "icon": "monitor"
}, { },
"text": I18n.tr("Power & Security"), {
"icon": "power" "text": I18n.tr("Launcher"),
}, { "icon": "apps"
"text": I18n.tr("Plugins"), },
"icon": "extension" {
}, { "text": I18n.tr("Theme & Colors"),
"text": I18n.tr("About"), "icon": "palette"
"icon": "info" },
}] {
"text": I18n.tr("Power & Security"),
"icon": "power"
},
{
"text": I18n.tr("Plugins"),
"icon": "extension"
},
{
"text": I18n.tr("About"),
"icon": "info"
}
]
function navigateNext() { function navigateNext() {
currentIndex = (currentIndex + 1) % sidebarItems.length currentIndex = (currentIndex + 1) % sidebarItems.length;
} }
function navigatePrevious() { function navigatePrevious() {
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length;
} }
width: 270 width: 270
@@ -61,31 +73,32 @@ Rectangle {
DankFlickable { DankFlickable {
anchors.fill: parent anchors.fill: parent
clip: true clip: true
contentHeight: sidebarColumn.implicitHeight contentHeight: sidebarColumn.height
Column { Column {
id: sidebarColumn id: sidebarColumn
anchors.fill: parent width: parent.width
anchors.leftMargin: Theme.spacingS leftPadding: Theme.spacingS
anchors.rightMargin: Theme.spacingS rightPadding: Theme.spacingS
anchors.bottomMargin: Theme.spacingS bottomPadding: Theme.spacingL
anchors.topMargin: Theme.spacingM + 2 topPadding: Theme.spacingM + 2
spacing: Theme.spacingXS spacing: Theme.spacingXS
ProfileSection { ProfileSection {
width: parent.width - parent.leftPadding - parent.rightPadding
parentModal: sidebarContainer.parentModal parentModal: sidebarContainer.parentModal
} }
Rectangle { Rectangle {
width: parent.width - Theme.spacingS * 2 width: parent.width - parent.leftPadding - parent.rightPadding
height: 1 height: 1
color: Theme.outline color: Theme.outline
opacity: 0.2 opacity: 0.2
} }
Item { Item {
width: parent.width width: parent.width - parent.leftPadding - parent.rightPadding
height: Theme.spacingL height: Theme.spacingL
} }
@@ -100,7 +113,7 @@ Rectangle {
property bool isActive: sidebarContainer.currentIndex === index property bool isActive: sidebarContainer.currentIndex === index
width: parent.width width: parent.width - parent.leftPadding - parent.rightPadding
height: 44 height: 44
radius: Theme.cornerRadius radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent" color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
@@ -125,7 +138,6 @@ Rectangle {
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
} }
MouseArea { MouseArea {
@@ -144,15 +156,9 @@ Rectangle {
duration: Theme.shortDuration duration: Theme.shortDuration
easing.type: Theme.standardEasing easing.type: Theme.standardEasing
} }
} }
} }
} }
} }
} }
} }
@@ -158,6 +158,10 @@ Item {
onAppLaunched: () => { onAppLaunched: () => {
if (parentModal) if (parentModal)
parentModal.hide() parentModal.hide()
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview()
}
} }
onViewModeSelected: mode => { onViewModeSelected: mode => {
SettingsData.set("spotlightModalViewMode", mode) SettingsData.set("spotlightModalViewMode", mode)
@@ -170,6 +174,10 @@ Item {
onFileOpened: () => { onFileOpened: () => {
if (parentModal) if (parentModal)
parentModal.hide() parentModal.hide()
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview()
}
} }
} }
+60 -56
View File
@@ -1,88 +1,92 @@
import QtQuick import QtQuick
import QtQuick.Controls import Quickshell.Hyprland
import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Widgets
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Modules.AppDrawer
import qs.Services import qs.Services
import qs.Widgets
DankModal { DankModal {
id: spotlightModal id: spotlightModal
layerNamespace: "dms:spotlight" layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal]
active: CompositorService.isHyprland && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance property alias spotlightContent: spotlightContentInstance
property bool openedFromOverview: false
function show() { function show() {
spotlightOpen = true openedFromOverview = false;
open() spotlightOpen = true;
open();
Qt.callLater(() => { Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) { if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus() spotlightContent.searchField.forceActiveFocus();
} }
}) });
} }
function showWithQuery(query) { function showWithQuery(query) {
if (spotlightContent) { if (spotlightContent) {
if (spotlightContent.appLauncher) { if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = query spotlightContent.appLauncher.searchQuery = query;
} }
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = query spotlightContent.searchField.text = query;
} }
} }
spotlightOpen = true spotlightOpen = true;
open() open();
Qt.callLater(() => { Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) { if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus() spotlightContent.searchField.forceActiveFocus();
} }
}) });
} }
function hide() { function hide() {
spotlightOpen = false openedFromOverview = false;
close() spotlightOpen = false;
close();
} }
onDialogClosed: { onDialogClosed: {
if (spotlightContent) { if (spotlightContent) {
if (spotlightContent.appLauncher) { if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = "" spotlightContent.appLauncher.searchQuery = "";
spotlightContent.appLauncher.selectedIndex = 0 spotlightContent.appLauncher.selectedIndex = 0;
spotlightContent.appLauncher.setCategory(I18n.tr("All")) spotlightContent.appLauncher.setCategory(I18n.tr("All"));
} }
if (spotlightContent.fileSearchController) { if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset() spotlightContent.fileSearchController.reset();
} }
if (spotlightContent.resetScroll) { if (spotlightContent.resetScroll) {
spotlightContent.resetScroll() spotlightContent.resetScroll();
} }
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.text = "" spotlightContent.searchField.text = "";
} }
} }
} }
function toggle() { function toggle() {
if (spotlightOpen) { if (spotlightOpen) {
hide() hide();
} else { } else {
show() show();
} }
} }
shouldBeVisible: spotlightOpen shouldBeVisible: spotlightOpen
width: 500 modalWidth: 500
height: 600 modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium borderColor: Theme.outlineMedium
@@ -90,25 +94,25 @@ DankModal {
enableShadow: true enableShadow: true
keepContentLoaded: true keepContentLoaded: true
onVisibleChanged: () => { onVisibleChanged: () => {
if (visible && !spotlightOpen) { if (visible && !spotlightOpen) {
show() show();
} }
if (visible && spotlightContent) { if (visible && spotlightContent) {
Qt.callLater(() => { Qt.callLater(() => {
if (spotlightContent.searchField) { if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus() spotlightContent.searchField.forceActiveFocus();
} }
}) });
} }
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
return hide() return hide();
} }
Connections { Connections {
function onCloseAllModalsExcept(excludedModal) { function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) { if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
spotlightOpen = false spotlightOpen = false;
} }
} }
@@ -117,32 +121,32 @@ DankModal {
IpcHandler { IpcHandler {
function open(): string { function open(): string {
spotlightModal.show() spotlightModal.show();
return "SPOTLIGHT_OPEN_SUCCESS" return "SPOTLIGHT_OPEN_SUCCESS";
} }
function close(): string { function close(): string {
spotlightModal.hide() spotlightModal.hide();
return "SPOTLIGHT_CLOSE_SUCCESS" return "SPOTLIGHT_CLOSE_SUCCESS";
} }
function toggle(): string { function toggle(): string {
spotlightModal.toggle() spotlightModal.toggle();
return "SPOTLIGHT_TOGGLE_SUCCESS" return "SPOTLIGHT_TOGGLE_SUCCESS";
} }
function openQuery(query: string): string { function openQuery(query: string): string {
spotlightModal.showWithQuery(query) spotlightModal.showWithQuery(query);
return "SPOTLIGHT_OPEN_QUERY_SUCCESS" return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
} }
function toggleQuery(query: string): string { function toggleQuery(query: string): string {
if (spotlightModal.spotlightOpen) { if (spotlightModal.spotlightOpen) {
spotlightModal.hide() spotlightModal.hide();
} else { } else {
spotlightModal.showWithQuery(query) spotlightModal.showWithQuery(query);
} }
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS" return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
} }
target: "spotlight" target: "spotlight"
+297 -239
View File
@@ -1,4 +1,5 @@
import QtQuick import QtQuick
import Quickshell.Hyprland
import qs.Common import qs.Common
import qs.Modals.Common import qs.Modals.Common
import qs.Services import qs.Services
@@ -8,6 +9,12 @@ DankModal {
id: root id: root
layerNamespace: "dms:wifi-password" layerNamespace: "dms:wifi-password"
keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string wifiPasswordSSID: "" property string wifiPasswordSSID: ""
property string wifiPasswordInput: "" property string wifiPasswordInput: ""
@@ -29,118 +36,124 @@ DankModal {
property string connectionType: "" property string connectionType: ""
function show(ssid) { function show(ssid) {
wifiPasswordSSID = ssid wifiPasswordSSID = ssid;
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
isPromptMode = false isPromptMode = false;
promptToken = "" promptToken = "";
promptReason = "" promptReason = "";
promptFields = [] promptFields = [];
promptSetting = "" promptSetting = "";
isVpnPrompt = false isVpnPrompt = false;
connectionName = "" connectionName = "";
vpnServiceType = "" vpnServiceType = "";
connectionType = "" connectionType = "";
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid) const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid);
requiresEnterprise = network?.enterprise || false requiresEnterprise = network?.enterprise || false;
open() open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) { if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus() contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) { } else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus() contentLoader.item.passwordInput.forceActiveFocus();
} }
} }
}) });
} }
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) { function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
isPromptMode = true isPromptMode = true;
promptToken = token promptToken = token;
promptReason = reason promptReason = reason;
promptFields = fields || [] promptFields = fields || [];
promptSetting = setting || "802-11-wireless-security" promptSetting = setting || "802-11-wireless-security";
connectionType = connType || "802-11-wireless" connectionType = connType || "802-11-wireless";
connectionName = connName || ssid || "" connectionName = connName || ssid || "";
vpnServiceType = vpnService || "" vpnServiceType = vpnService || "";
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard") isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard");
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid wifiPasswordSSID = isVpnPrompt ? connectionName : ssid;
requiresEnterprise = setting === "802-1x" requiresEnterprise = setting === "802-1x";
if (reason === "wrong-password") { if (reason === "wrong-password") {
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
} else { } else {
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
} }
open() open();
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
if (reason === "wrong-password" && contentLoader.item.passwordInput) { if (reason === "wrong-password" && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.text = "" contentLoader.item.passwordInput.text = "";
contentLoader.item.passwordInput.forceActiveFocus() contentLoader.item.passwordInput.forceActiveFocus();
} else if (requiresEnterprise && contentLoader.item.usernameInput) { } else if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus() contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) { } else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus() contentLoader.item.passwordInput.forceActiveFocus();
} }
} }
}) });
} }
shouldBeVisible: false shouldBeVisible: false
width: 420 modalWidth: 420
height: requiresEnterprise ? 430 : 230 modalHeight: {
if (requiresEnterprise)
return 430;
if (isVpnPrompt)
return 260;
return 230;
}
onShouldBeVisibleChanged: () => { onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) { if (!shouldBeVisible) {
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
} }
} }
onOpened: { onOpened: {
Qt.callLater(() => { Qt.callLater(() => {
if (contentLoader.item) { if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) { if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus() contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) { } else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus() contentLoader.item.passwordInput.forceActiveFocus();
} }
} }
}) });
} }
onBackgroundClicked: () => { onBackgroundClicked: () => {
if (isPromptMode) { if (isPromptMode) {
NetworkService.cancelCredentials(promptToken) NetworkService.cancelCredentials(promptToken);
} }
close() close();
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
} }
Connections { Connections {
target: NetworkService target: NetworkService
function onPasswordDialogShouldReopenChanged() { function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") { if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID wifiPasswordSSID = NetworkService.connectingSSID;
wifiPasswordInput = "" wifiPasswordInput = "";
open() open();
NetworkService.passwordDialogShouldReopen = false NetworkService.passwordDialogShouldReopen = false;
} }
} }
} }
@@ -155,16 +168,16 @@ DankModal {
anchors.fill: parent anchors.fill: parent
focus: true focus: true
Keys.onEscapePressed: event => { Keys.onEscapePressed: event => {
if (isPromptMode) { if (isPromptMode) {
NetworkService.cancelCredentials(promptToken) NetworkService.cancelCredentials(promptToken);
} }
close() close();
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
event.accepted = true event.accepted = true;
} }
Column { Column {
anchors.centerIn: parent anchors.centerIn: parent
@@ -181,9 +194,9 @@ DankModal {
StyledText { StyledText {
text: { text: {
if (isVpnPrompt) { if (isVpnPrompt) {
return I18n.tr("Connect to VPN") return I18n.tr("Connect to VPN");
} }
return I18n.tr("Connect to Wi-Fi") return I18n.tr("Connect to Wi-Fi");
} }
font.pixelSize: Theme.fontSizeLarge font.pixelSize: Theme.fontSizeLarge
color: Theme.surfaceText color: Theme.surfaceText
@@ -197,10 +210,10 @@ DankModal {
StyledText { StyledText {
text: { text: {
if (isVpnPrompt) { if (isVpnPrompt) {
return I18n.tr("Enter password for ") + wifiPasswordSSID return I18n.tr("Enter password for ") + wifiPasswordSSID;
} }
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ") const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
return prefix + wifiPasswordSSID return prefix + wifiPasswordSSID;
} }
font.pixelSize: Theme.fontSizeMedium font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium color: Theme.surfaceTextMedium
@@ -223,15 +236,15 @@ DankModal {
iconSize: Theme.iconSize - 4 iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText iconColor: Theme.surfaceText
onClicked: () => { onClicked: () => {
if (isPromptMode) { if (isPromptMode) {
NetworkService.cancelCredentials(promptToken) NetworkService.cancelCredentials(promptToken);
} }
close() close();
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
} }
} }
} }
@@ -247,8 +260,8 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
usernameInput.forceActiveFocus() usernameInput.forceActiveFocus();
} }
} }
DankTextField { DankTextField {
@@ -262,13 +275,13 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
wifiUsernameInput = text wifiUsernameInput = text;
} }
onAccepted: () => { onAccepted: () => {
if (passwordInput) { if (passwordInput) {
passwordInput.forceActiveFocus() passwordInput.forceActiveFocus();
} }
} }
} }
} }
@@ -283,8 +296,8 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
passwordInput.forceActiveFocus() passwordInput.forceActiveFocus();
} }
} }
DankTextField { DankTextField {
@@ -300,43 +313,42 @@ DankModal {
focus: !requiresEnterprise focus: !requiresEnterprise
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
wifiPasswordInput = text wifiPasswordInput = text;
} }
onAccepted: () => { onAccepted: () => {
if (isPromptMode) { if (isPromptMode) {
const secrets = {} const secrets = {};
if (isVpnPrompt) { if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text if (passwordInput.text)
} else if (promptSetting === "802-11-wireless-security") { secrets["password"] = passwordInput.text;
secrets["psk"] = passwordInput.text } else if (promptSetting === "802-11-wireless-security") {
} else if (promptSetting === "802-1x") { secrets["psk"] = passwordInput.text;
if (usernameInput.text) secrets["identity"] = usernameInput.text } else if (promptSetting === "802-1x") {
if (passwordInput.text) secrets["password"] = passwordInput.text if (usernameInput.text)
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput secrets["identity"] = usernameInput.text;
} if (passwordInput.text)
NetworkService.submitCredentials(promptToken, secrets, true) secrets["password"] = passwordInput.text;
} else { if (wifiAnonymousIdentityInput)
const username = requiresEnterprise ? usernameInput.text : "" secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
NetworkService.connectToWifi( }
wifiPasswordSSID, NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
passwordInput.text, } else {
username, const username = requiresEnterprise ? usernameInput.text : "";
wifiAnonymousIdentityInput, NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
wifiDomainInput }
) close();
} wifiPasswordInput = "";
close() wifiUsernameInput = "";
wifiPasswordInput = "" wifiAnonymousIdentityInput = "";
wifiUsernameInput = "" wifiDomainInput = "";
wifiAnonymousIdentityInput = "" passwordInput.text = "";
wifiDomainInput = "" if (requiresEnterprise)
passwordInput.text = "" usernameInput.text = "";
if (requiresEnterprise) usernameInput.text = "" }
}
Component.onCompleted: () => { Component.onCompleted: () => {
if (root.shouldBeVisible && !requiresEnterprise) if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start() focusDelayTimer.start();
} }
Timer { Timer {
id: focusDelayTimer id: focusDelayTimer
@@ -344,14 +356,14 @@ DankModal {
interval: 100 interval: 100
repeat: false repeat: false
onTriggered: () => { onTriggered: () => {
if (root.shouldBeVisible) { if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) { if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus() usernameInput.forceActiveFocus();
} else { } else {
passwordInput.forceActiveFocus() passwordInput.forceActiveFocus();
} }
} }
} }
} }
Connections { Connections {
@@ -359,7 +371,7 @@ DankModal {
function onShouldBeVisibleChanged() { function onShouldBeVisibleChanged() {
if (root.shouldBeVisible) if (root.shouldBeVisible)
focusDelayTimer.start() focusDelayTimer.start();
} }
} }
} }
@@ -377,8 +389,8 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
anonInput.forceActiveFocus() anonInput.forceActiveFocus();
} }
} }
DankTextField { DankTextField {
@@ -392,8 +404,8 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
wifiAnonymousIdentityInput = text wifiAnonymousIdentityInput = text;
} }
} }
} }
@@ -409,8 +421,8 @@ DankModal {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
onClicked: () => { onClicked: () => {
domainMatchInput.forceActiveFocus() domainMatchInput.forceActiveFocus();
} }
} }
DankTextField { DankTextField {
@@ -424,49 +436,96 @@ DankModal {
backgroundColor: "transparent" backgroundColor: "transparent"
enabled: root.shouldBeVisible enabled: root.shouldBeVisible
onTextEdited: () => { onTextEdited: () => {
wifiDomainInput = text wifiDomainInput = text;
} }
} }
} }
Row { Column {
spacing: Theme.spacingS spacing: Theme.spacingS
width: parent.width
Rectangle { Row {
id: showPasswordCheckbox spacing: Theme.spacingS
property bool checked: false Rectangle {
id: showPasswordCheckbox
width: 20 property bool checked: false
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon { width: 20
anchors.centerIn: parent height: 20
name: "check" radius: 4
size: 12 color: checked ? Theme.primary : "transparent"
color: Theme.background border.color: checked ? Theme.primary : Theme.outlineButton
visible: parent.checked border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
} }
MouseArea { StyledText {
anchors.fill: parent text: I18n.tr("Show password")
hoverEnabled: true font.pixelSize: Theme.fontSizeMedium
cursorShape: Qt.PointingHandCursor color: Theme.surfaceText
onClicked: () => { anchors.verticalCenter: parent.verticalCenter
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
} }
} }
StyledText { Row {
text: I18n.tr("Show password") spacing: Theme.spacingS
font.pixelSize: Theme.fontSizeMedium visible: isVpnPrompt
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter Rectangle {
id: savePasswordCheckbox
property bool checked: false
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
savePasswordCheckbox.checked = !savePasswordCheckbox.checked;
}
}
}
StyledText {
text: I18n.tr("Save password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
} }
} }
@@ -504,15 +563,15 @@ DankModal {
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onClicked: () => { onClicked: () => {
if (isPromptMode) { if (isPromptMode) {
NetworkService.cancelCredentials(promptToken) NetworkService.cancelCredentials(promptToken);
} }
close() close();
wifiPasswordInput = "" wifiPasswordInput = "";
wifiUsernameInput = "" wifiUsernameInput = "";
wifiAnonymousIdentityInput = "" wifiAnonymousIdentityInput = "";
wifiDomainInput = "" wifiDomainInput = "";
} }
} }
} }
@@ -523,9 +582,9 @@ DankModal {
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: { enabled: {
if (isVpnPrompt) { if (isVpnPrompt) {
return passwordInput.text.length > 0 return passwordInput.text.length > 0;
} }
return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0 return requiresEnterprise ? (usernameInput.text.length > 0 && passwordInput.text.length > 0) : passwordInput.text.length > 0;
} }
opacity: enabled ? 1 : 0.5 opacity: enabled ? 1 : 0.5
@@ -547,36 +606,35 @@ DankModal {
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
enabled: parent.enabled enabled: parent.enabled
onClicked: () => { onClicked: () => {
if (isPromptMode) { if (isPromptMode) {
const secrets = {} const secrets = {};
if (isVpnPrompt) { if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text if (passwordInput.text)
} else if (promptSetting === "802-11-wireless-security") { secrets["password"] = passwordInput.text;
secrets["psk"] = passwordInput.text } else if (promptSetting === "802-11-wireless-security") {
} else if (promptSetting === "802-1x") { secrets["psk"] = passwordInput.text;
if (usernameInput.text) secrets["identity"] = usernameInput.text } else if (promptSetting === "802-1x") {
if (passwordInput.text) secrets["password"] = passwordInput.text if (usernameInput.text)
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput secrets["identity"] = usernameInput.text;
} if (passwordInput.text)
NetworkService.submitCredentials(promptToken, secrets, true) secrets["password"] = passwordInput.text;
} else { if (wifiAnonymousIdentityInput)
const username = requiresEnterprise ? usernameInput.text : "" secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
NetworkService.connectToWifi( }
wifiPasswordSSID, NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
passwordInput.text, } else {
username, const username = requiresEnterprise ? usernameInput.text : "";
wifiAnonymousIdentityInput, NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
wifiDomainInput }
) close();
} wifiPasswordInput = "";
close() wifiUsernameInput = "";
wifiPasswordInput = "" wifiAnonymousIdentityInput = "";
wifiUsernameInput = "" wifiDomainInput = "";
wifiAnonymousIdentityInput = "" passwordInput.text = "";
wifiDomainInput = "" if (requiresEnterprise)
passwordInput.text = "" usernameInput.text = "";
if (requiresEnterprise) usernameInput.text = "" }
}
} }
Behavior on color { Behavior on color {

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