1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-05-04 11:32:12 -04:00

Compare commits

..

49 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
github-actions[bot]
dfdaa82245 chore: bump version to v0.6.1 2025-11-19 03:16:35 +00:00
bbedward
99a307e0ad dankbar: hot fix color moda & systm tray item positions 2025-11-18 22:13:06 -05:00
204 changed files with 15854 additions and 8724 deletions

View File

@@ -386,6 +386,68 @@ jobs:
env:
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:
runs-on: ubuntu-latest
needs: release

View File

@@ -1,4 +1,4 @@
name: DMS Copr Stable Release (Manual)
name: DMS Copr Stable Release
on:
workflow_dispatch:

114
.github/workflows/run-obs.yml vendored Normal file
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
.github/workflows/run-ppa.yml vendored Normal file
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
.gitignore vendored
View File

@@ -136,3 +136,9 @@ go.work.sum
# .vscode/
bin/
# Extracted source trees in Ubuntu package directories
distro/ubuntu/*/dms-git-repo/
distro/ubuntu/*/DankMaterialShell-*/
distro/ubuntu/danklinux/*/dsearch-*/
distro/ubuntu/danklinux/*/dgop-*/

View File

@@ -2,28 +2,42 @@
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
"customLocalFormatters.formatters": [
{
"command": "sh -c \"qmlfmt -t 4 -i 4 -b 250 | sed 's/pragma ComponentBehavior$/pragma ComponentBehavior: Bound/g'\"",
"languages": ["qml"]
}
],
"[qml]": {
"editor.defaultFormatter": "jkillian.custom-local-formatters",
"editor.formatOnSave": true
},
{
"qt-qml.doNotAskForQmllsDownload": true,
"qt-qml.qmlls.customExePath": "/usr/lib/qt6/bin/qmlls"
}
```
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

View File

@@ -57,6 +57,11 @@ func getRuntimeDir() string {
return os.TempDir()
}
func hasSystemdRun() bool {
_, err := exec.LookPath("systemd-run")
return err == nil
}
func getPIDFilePath() string {
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)
}
if isSessionManaged && hasSystemdRun() {
cmd.Env = append(cmd.Env, "DMS_DEFAULT_LAUNCH_PREFIX=systemd-run --user --scope")
}
homeDir, err := os.UserHomeDir()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {
@@ -387,6 +396,10 @@ func runShellDaemon(session bool) {
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()
if err == nil && os.Getenv("DMS_DISABLE_HOT_RELOAD") == "" {
if !strings.HasPrefix(configPath, homeDir) {

View File

@@ -125,6 +125,8 @@ windowrulev2 = noborder, class:^(kitty)$
windowrulev2 = float, class:^(firefox)$, title:^(Picture-in-Picture)$
windowrulev2 = float, class:^(zoom)$
# DMS windows floating by default
windowrulev2 = float, class:^(org.quickshell)$
windowrulev2 = opacity 0.9 0.9, floating:0, focus:0
layerrule = noanim, ^(quickshell)$

View File

@@ -218,6 +218,11 @@ window-rule {
geometry-corner-radius 12
clip-to-geometry true
}
// Open dms windows as floating by default
window-rule {
match app-id=r#"org.quickshell$"#
open-floating true
}
binds {
// === System & Overview ===
Mod+D { spawn "niri" "msg" "action" "toggle-overview"; }

View File

@@ -19,10 +19,12 @@ func init() {
Register("fedora-asahi-remix", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("bluefin", "#0B57A4", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
Register("ultramarine", "#00078b", FamilyFedora, func(config DistroConfig, logChan chan<- string) Distribution {
return NewFedoraDistribution(config, logChan)
})
}
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, ", ")))
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...)
progressChan <- InstallProgressMsg{

View File

@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"time"
"github.com/AvengeMedia/DankMaterialShell/core/internal/errdefs"
@@ -125,8 +126,9 @@ func (a *SecretAgent) GetSecrets(
connType, displayName, vpnSvc := readConnTypeAndName(conn)
ssid := readSSID(conn)
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 {
a.backend.stateMutex.RLock()
@@ -163,57 +165,70 @@ func (a *SecretAgent) GetSecrets(
}
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" {
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
if a.backend != nil {
a.backend.stateMutex.RLock()
isConnectingVPN := a.backend.state.IsConnectingVPN
a.backend.stateMutex.RUnlock()
const (
NM_SETTING_SECRET_FLAG_NONE = 0
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
)
if !isConnectingVPN {
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
var passwordFlags uint32 = 0xFFFF
switch settingName {
case "802-11-wireless-security":
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
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
}
}
log.Infof("[SecretAgent] VPN with empty hints but we're connecting - prompting for password")
fields = []string{"password"}
} else {
log.Infof("[SecretAgent] VPN with empty hints - deferring to other agents for %s", vpnSvc)
return nil, dbus.NewError("org.freedesktop.NetworkManager.SecretAgent.Error.NoSecrets", nil)
}
}
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
if len(fields) == 0 {
const (
NM_SETTING_SECRET_FLAG_NONE = 0
NM_SETTING_SECRET_FLAG_AGENT_OWNED = 1
NM_SETTING_SECRET_FLAG_NOT_SAVED = 2
NM_SETTING_SECRET_FLAG_NOT_REQUIRED = 4
)
var passwordFlags uint32 = 0xFFFF
switch settingName {
case "802-11-wireless-security":
if wifiSecSettings, ok := conn["802-11-wireless-security"]; ok {
if flagsVariant, ok := wifiSecSettings["psk-flags"]; ok {
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 {
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
switch settingName {
case "vpn":
// Set password-flags=0 and add secrets to vpn section
vpn, ok := existingSettings["vpn"]
if !ok {
vpn = make(map[string]dbus.Variant)
}
// Get existing data map (vpn.data is string->string)
var data map[string]string
if dataVariant, ok := vpn["data"]; ok {
if dm, ok := dataVariant.Value().(map[string]string); ok {
@@ -364,11 +377,9 @@ func (a *SecretAgent) GetSecrets(
data = make(map[string]string)
}
// Update password-flags to 0 (system-stored)
data["password-flags"] = "0"
vpn["data"] = dbus.MakeVariant(data)
// Add secrets (vpn.secrets is string->string)
secs := make(map[string]string)
for k, v := range reply.Secrets {
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))
case "802-11-wireless-security":
// Set psk-flags=0 for WiFi
wifiSec, ok := existingSettings["802-11-wireless-security"]
if !ok {
wifiSec = make(map[string]dbus.Variant)
}
wifiSec["psk-flags"] = dbus.MakeVariant(uint32(0))
// Add PSK secret
if psk, ok := reply.Secrets["psk"]; ok {
wifiSec["psk"] = dbus.MakeVariant(psk)
log.Infof("[SecretAgent] Updated WiFi settings: psk-flags=0")
@@ -394,14 +403,12 @@ func (a *SecretAgent) GetSecrets(
settings["802-11-wireless-security"] = wifiSec
case "802-1x":
// Set password-flags=0 for 802.1x
dot1x, ok := existingSettings["802-1x"]
if !ok {
dot1x = make(map[string]dbus.Variant)
}
dot1x["password-flags"] = dbus.MakeVariant(uint32(0))
// Add password secret
if password, ok := reply.Secrets["password"]; ok {
dot1x["password"] = dbus.MakeVariant(password)
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 {
const (
NM_SECRET_AGENT_GET_SECRETS_FLAG_NONE = 0x0

View File

@@ -235,7 +235,7 @@ func (b *NetworkManagerBackend) ConnectVPN(uuidOrName string, singleActive bool)
}
nm := b.nmConn.(gonetworkmanager.NetworkManager)
activeConn, err := nm.ActivateConnection(targetConn, nil, nil)
_, err = nm.ActivateConnection(targetConn, nil, nil)
if err != nil {
b.stateMutex.Lock()
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)
}
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
}

View File

@@ -607,41 +607,6 @@ func (m *Manager) transitionWorker() {
if finalTarget == 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 {
if m.controlsInitialized {
m.configMutex.RLock()
identityTemp := m.config.HighTemp
m.configMutex.RUnlock()
m.transitionMutex.RLock()
currentTemp := m.currentTemp
m.transitionMutex.RUnlock()
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)
m.post(func() {
log.Info("Disabling gamma, destroying controls immediately")
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)
}
log.Info("All gamma controls destroyed")
return true
})
} else {
log.Infof("Disabling: transitioning to %dK before destroying controls", identityTemp)
m.startTransition(identityTemp)
}
m.outputs.Range(func(key uint32, value *outputState) bool {
m.outputs.Delete(key)
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")
})
}
}
}

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>

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

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

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.

View File

@@ -0,0 +1 @@
dms-git_0.6.0+git2061.5ddea836ppa1_source.buildinfo x11 optional

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

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1 @@
dms-distropkg-amd64.gz

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-git-repo

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>

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

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

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.

View File

@@ -0,0 +1 @@
dms_0.6.0ppa2_source.buildinfo x11 optional

64
distro/debian/dms/debian/rules Executable file
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

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1,2 @@
dms-distropkg-amd64.gz
dms-source.tar.gz

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
distro/opensuse/_service Normal file
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>

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
distro/opensuse/dms.spec Normal file
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)

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!"

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
distro/scripts/test-packaging.sh Executable file
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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

View File

@@ -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

View File

@@ -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

View File

@@ -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".

View File

@@ -0,0 +1 @@
danksearch_0.0.7ppa3_source.buildinfo utils optional

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

View File

@@ -0,0 +1 @@
3.0 (native)

Binary file not shown.

Binary file not shown.

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -0,0 +1 @@
dgop_0.1.11ppa2_source.buildinfo utils optional

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

View File

@@ -0,0 +1 @@
3.0 (native)

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

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

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.

View File

@@ -0,0 +1 @@
dms-git_0.6.2+git2094.6cc6e7c8ppa1_source.buildinfo x11 optional

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

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1 @@
dms-distropkg-amd64.gz

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-git-repo

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

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

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.

View File

@@ -0,0 +1 @@
dms_0.6.2ppa3_source.buildinfo x11 optional

63
distro/ubuntu/dms/debian/rules Executable file
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

View File

@@ -0,0 +1 @@
3.0 (native)

View File

@@ -0,0 +1,2 @@
dms-distropkg-amd64.gz
dms-source.tar.gz

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

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

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!"

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
distro/ubuntu/ppa/upload-ppa.sh Executable file
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

View File

@@ -9,155 +9,167 @@ Singleton {
property var currentPopoutsByScreen: ({})
property var currentPopoutTriggers: ({})
function showPopout(popout) {
if (!popout || !popout.screen) return
signal popoutOpening
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) {
const otherPopout = currentPopoutsByScreen[otherScreenName]
if (!otherPopout || otherPopout === popout) continue
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout || otherPopout === popout)
continue;
if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false
otherPopout.dashVisible = false;
} else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false
otherPopout.notificationHistoryVisible = false;
} else {
otherPopout.close()
otherPopout.close();
}
}
currentPopoutsByScreen[screenName] = popout
ModalManager.closeAllModalsExcept(null)
TrayMenuManager.closeAllMenus()
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
ModalManager.closeAllModalsExcept(null);
}
function hidePopout(popout) {
if (!popout || !popout.screen) return
const screenName = popout.screen.name
if (!popout || !popout.screen)
return;
const screenName = popout.screen.name;
if (currentPopoutsByScreen[screenName] === popout) {
currentPopoutsByScreen[screenName] = null
currentPopoutTriggers[screenName] = null
currentPopoutsByScreen[screenName] = null;
currentPopoutTriggers[screenName] = null;
popoutChanged();
}
}
function closeAllPopouts() {
for (const screenName in currentPopoutsByScreen) {
const popout = currentPopoutsByScreen[screenName]
if (!popout) continue
const popout = currentPopoutsByScreen[screenName];
if (!popout)
continue;
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
popout.notificationHistoryVisible = false;
} else {
popout.close()
popout.close();
}
}
currentPopoutsByScreen = {}
currentPopoutsByScreen = {};
}
function getActivePopout(screen) {
if (!screen) return null
return currentPopoutsByScreen[screen.name] || null
if (!screen)
return null;
return currentPopoutsByScreen[screen.name] || null;
}
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 currentPopout = currentPopoutsByScreen[screenName]
const triggerId = triggerSource !== undefined ? triggerSource : tabIndex
const willOpen = !(currentPopout === popout && popout.shouldBeVisible && triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId);
if (willOpen) {
popoutOpening();
}
let justClosedSamePopout = false
let justClosedSamePopout = false;
for (const otherScreenName in currentPopoutsByScreen) {
if (otherScreenName === screenName) continue
const otherPopout = currentPopoutsByScreen[otherScreenName]
if (!otherPopout) continue
if (otherScreenName === screenName)
continue;
const otherPopout = currentPopoutsByScreen[otherScreenName];
if (!otherPopout)
continue;
if (otherPopout === popout) {
justClosedSamePopout = true
justClosedSamePopout = true;
}
if (otherPopout.dashVisible !== undefined) {
otherPopout.dashVisible = false
otherPopout.dashVisible = false;
} else if (otherPopout.notificationHistoryVisible !== undefined) {
otherPopout.notificationHistoryVisible = false
otherPopout.notificationHistoryVisible = false;
} else {
otherPopout.close()
otherPopout.close();
}
}
if (currentPopout && currentPopout !== popout) {
if (currentPopout.dashVisible !== undefined) {
currentPopout.dashVisible = false
currentPopout.dashVisible = false;
} else if (currentPopout.notificationHistoryVisible !== undefined) {
currentPopout.notificationHistoryVisible = false
currentPopout.notificationHistoryVisible = false;
} else {
currentPopout.close()
currentPopout.close();
}
}
if (currentPopout === popout && popout.shouldBeVisible) {
if (triggerId !== undefined && currentPopoutTriggers[screenName] === triggerId) {
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
popout.notificationHistoryVisible = false;
} else {
popout.close()
popout.close();
}
return
return;
}
if (triggerId === undefined) {
if (popout.dashVisible !== undefined) {
popout.dashVisible = false
popout.dashVisible = false;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = false
popout.notificationHistoryVisible = false;
} else {
popout.close()
popout.close();
}
return
return;
}
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex
popout.currentTabIndex = tabIndex;
}
currentPopoutTriggers[screenName] = triggerId
return
currentPopoutTriggers[screenName] = triggerId;
}
currentPopoutTriggers[screenName] = triggerId
currentPopoutsByScreen[screenName] = popout
currentPopoutTriggers[screenName] = triggerId;
currentPopoutsByScreen[screenName] = popout;
popoutChanged();
if (tabIndex !== undefined && popout.currentTabIndex !== undefined) {
popout.currentTabIndex = tabIndex
popout.currentTabIndex = tabIndex;
}
if (currentPopout !== popout) {
ModalManager.closeAllModalsExcept(null)
ModalManager.closeAllModalsExcept(null);
}
TrayMenuManager.closeAllMenus()
if (justClosedSamePopout) {
Qt.callLater(() => {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true
popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true
popout.notificationHistoryVisible = true;
} else {
popout.open()
popout.open();
}
})
});
} else {
if (popout.dashVisible !== undefined) {
popout.dashVisible = true
popout.dashVisible = true;
} else if (popout.notificationHistoryVisible !== undefined) {
popout.notificationHistoryVisible = true
popout.notificationHistoryVisible = true;
} else {
popout.open()
popout.open();
}
}
}

View File

@@ -67,13 +67,21 @@ Singleton {
})
out.streamFinished.connect(function() {
capturedOut = out.text || ""
try {
capturedOut = out.text || ""
} catch (e) {
capturedOut = ""
}
outSeen = true
maybeComplete()
})
err.streamFinished.connect(function() {
capturedErr = err.text || ""
try {
capturedErr = err.text || ""
} catch (e) {
capturedErr = ""
}
errSeen = true
maybeComplete()
})
@@ -88,8 +96,14 @@ Singleton {
function maybeComplete() {
if (!exitSeen || !outSeen || !errSeen) return
timeoutTimer.stop()
if (typeof entry.callback === "function") {
try { entry.callback(capturedOut, exitCodeValue) } catch (e) { console.warn("runCommand callback error:", e) }
if (entry && entry.callback && typeof entry.callback === "function") {
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 { timeoutTimer.destroy() } catch (_) {}

File diff suppressed because it is too large Load Diff

View File

@@ -2,16 +2,16 @@
// Separated from Theme.qml to keep that file clean
const CatppuccinMocha = {
surface: "#313244",
surface: "#181825",
surfaceText: "#cdd6f4",
surfaceVariant: "#313244",
surfaceVariant: "#1e1e2e",
surfaceVariantText: "#a6adc8",
background: "#1e1e2e",
background: "#181825",
backgroundText: "#cdd6f4",
outline: "#6c7086",
surfaceContainer: "#45475a",
surfaceContainerHigh: "#585b70",
surfaceContainerHighest: "#6c7086"
surfaceContainer: "#1e1e2e",
surfaceContainerHigh: "#313244",
surfaceContainerHighest: "#45475a"
}
const CatppuccinLatte = {

File diff suppressed because it is too large Load Diff

View File

@@ -6,26 +6,30 @@ import QtQuick
Singleton {
id: root
property var activeTrayBars: ({})
property var activeTrayMenus: ({})
function register(screenName, trayBar) {
if (!screenName || !trayBar) return
activeTrayBars[screenName] = trayBar
function registerMenu(screenName, menu) {
if (!screenName || !menu) return
const newMenus = Object.assign({}, activeTrayMenus)
newMenus[screenName] = menu
activeTrayMenus = newMenus
}
function unregister(screenName) {
function unregisterMenu(screenName) {
if (!screenName) return
delete activeTrayBars[screenName]
const newMenus = Object.assign({}, activeTrayMenus)
delete newMenus[screenName]
activeTrayMenus = newMenus
}
function closeAllMenus() {
for (const screenName in activeTrayBars) {
const trayBar = activeTrayBars[screenName]
if (!trayBar) continue
trayBar.menuOpen = false
if (trayBar.currentTrayMenu) {
trayBar.currentTrayMenu.showMenu = false
for (const screenName in activeTrayMenus) {
const menu = activeTrayMenus[screenName]
if (!menu) continue
if (typeof menu.close === "function") {
menu.close()
} else if (menu.showMenu !== undefined) {
menu.showMenu = false
}
}
}

View File

@@ -12,12 +12,11 @@ var SPEC = {
runUserMatugenTemplates: { def: true, 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 },
dockTransparency: { def: 1.0, coerce: percentToUnit },
widgetBackgroundColor: { def: "sch" },
widgetColorMode: { def: "default" },
cornerRadius: { def: 12, onChange: "updateNiriLayout" },
use24HourClock: { def: true },
@@ -88,15 +87,11 @@ var SPEC = {
lockDateFormat: { def: "" },
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" },
spotlightModalViewMode: { def: "list" },
sortAppsAlphabetically: { def: false },
appLauncherGridColumns: { def: 4 },
spotlightCloseNiriOverview: { def: true },
weatherLocation: { def: "New York, NY" },
weatherCoordinates: { def: "40.7128,-74.0060" },
@@ -125,7 +120,6 @@ var SPEC = {
monoFontFamily: { def: "Fira Code" },
fontWeight: { def: 400 },
fontScale: { def: 1.0 },
dankBarFontScale: { def: 1.0 },
notepadUseMonospace: { def: true },
notepadFontFamily: { def: "" },
@@ -175,31 +169,9 @@ var SPEC = {
dockIndicatorStyle: { def: "circle" },
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 },
overviewColumns: { def: 5, 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 },
@@ -217,6 +189,7 @@ var SPEC = {
osdAlwaysShowValue: { def: false },
osdPosition: { def: 5 },
osdVolumeEnabled: { def: true },
osdMediaVolumeEnabled : { def: true },
osdBrightnessEnabled: { def: true },
osdIdleInhibitorEnabled: { def: true },
osdMicMuteEnabled: { def: true },
@@ -240,7 +213,40 @@ var SPEC = {
displayNameMode: { def: "system" },
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() {

View File

@@ -5,6 +5,7 @@
function parse(root, jsonObj) {
var SPEC = SpecModule.SPEC;
for (var k in SPEC) {
if (k === "pluginSettings") continue;
var spec = SPEC[k];
root[k] = spec.def;
}
@@ -13,6 +14,7 @@ function parse(root, jsonObj) {
for (var k in jsonObj) {
if (!SPEC[k]) continue;
if (k === "pluginSettings") continue;
var raw = jsonObj[k];
var spec = SPEC[k];
var coerce = spec.coerce;
@@ -25,72 +27,93 @@ function toJson(root) {
var out = {};
for (var k in SPEC) {
if (SPEC[k].persist === false) continue;
if (k === "pluginSettings") continue;
out[k] = root[k];
}
out.configVersion = root.settingsConfigVersion;
return out;
}
function migrate(root, jsonObj) {
var SPEC = SpecModule.SPEC;
if (!jsonObj) return;
function migrateToVersion(obj, targetVersion) {
if (!obj) return null;
if (jsonObj.themeIndex !== undefined || jsonObj.themeIsDynamic !== undefined) {
var themeNames = ["blue", "deepBlue", "purple", "green", "orange", "red", "cyan", "pink", "amber", "coral"];
if (jsonObj.themeIsDynamic) {
root.currentThemeName = "dynamic";
} else if (jsonObj.themeIndex >= 0 && jsonObj.themeIndex < themeNames.length) {
root.currentThemeName = themeNames[jsonObj.themeIndex];
}
console.info("Auto-migrated theme from index", jsonObj.themeIndex, "to", root.currentThemeName);
var settings = JSON.parse(JSON.stringify(obj));
var currentVersion = settings.configVersion || 0;
if (currentVersion >= targetVersion) {
return null;
}
if ((jsonObj.dankBarWidgetOrder && jsonObj.dankBarWidgetOrder.length > 0) ||
(jsonObj.topBarWidgetOrder && jsonObj.topBarWidgetOrder.length > 0)) {
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 (currentVersion < 2) {
console.info("Migrating settings from version", currentVersion, "to version 2");
if (jsonObj.useOSLogo !== undefined) {
root.launcherLogoMode = jsonObj.useOSLogo ? "os" : "apps";
root.launcherLogoColorOverride = jsonObj.osLogoColorOverride !== undefined ? jsonObj.osLogoColorOverride : "";
root.launcherLogoBrightness = jsonObj.osLogoBrightness !== undefined ? jsonObj.osLogoBrightness : 0.5;
root.launcherLogoContrast = jsonObj.osLogoContrast !== undefined ? jsonObj.osLogoContrast : 1;
}
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;
if (settings.barConfigs === undefined) {
var position = 0;
if (settings.dankBarAtBottom !== undefined || settings.topBarAtBottom !== undefined) {
var atBottom = settings.dankBarAtBottom !== undefined ? settings.dankBarAtBottom : settings.topBarAtBottom;
position = atBottom ? 1 : 0;
} else if (settings.dankBarPosition !== undefined) {
position = settings.dankBarPosition;
}
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) {
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;
return settings;
}
function cleanup(fileText) {
@@ -104,7 +127,6 @@ function cleanup(fileText) {
for (var key in settings) {
if (validKeys.indexOf(key) < 0) {
console.log("SettingsData: Removing unused key:", key);
delete settings[key];
needsSave = true;
}

View File

@@ -22,7 +22,7 @@ import qs.Modules.ProcessList
import qs.Modules.Settings
import qs.Modules.DankBar
import qs.Modules.DankBar.Popouts
import qs.Modules.HyprWorkspaces
import qs.Modules.WorkspaceOverlays
import qs.Modules.Plugins
import qs.Services
@@ -66,38 +66,32 @@ Item {
id: lock
}
Loader {
id: dankBarLoader
asynchronous: false
Repeater {
id: dankBarRepeater
model: ScriptModel {
values: SettingsData.barConfigs
}
property var currentPosition: SettingsData.dankBarPosition
property bool initialized: false
property var hyprlandOverviewLoaderRef: hyprlandOverviewLoader
sourceComponent: DankBar {
hyprlandOverviewLoader: dankBarLoader.hyprlandOverviewLoaderRef
delegate: Loader {
id: barLoader
active: modelData.enabled
asynchronous: false
onColorPickerRequested: {
if (colorPickerModal.shouldBeVisible) {
colorPickerModal.close()
} else {
colorPickerModal.show()
sourceComponent: DankBar {
barConfig: modelData
hyprlandOverviewLoader: dankBarRepeater.hyprlandOverviewLoaderRef
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 {
@@ -126,7 +120,6 @@ Item {
if (!initialized)
return
console.log("DEBUG: Dock position changed to:", currentPosition, "- recreating dock")
const comp = sourceComponent
sourceComponent = null
sourceComponent = comp
@@ -137,7 +130,7 @@ Item {
id: dankDashPopoutLoader
active: false
asynchronous: true
asynchronous: false
sourceComponent: Component {
DankDashPopout {
@@ -513,8 +506,9 @@ Item {
dankDashPopoutLoader: dankDashPopoutLoader
notepadSlideoutVariants: notepadSlideoutVariants
hyprKeybindsModalLoader: hyprKeybindsModalLoader
dankBarLoader: dankBarLoader
dankBarRepeater: dankBarRepeater
hyprlandOverviewLoader: hyprlandOverviewLoader
settingsModal: settingsModal
}
Variants {
@@ -534,6 +528,14 @@ Item {
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
delegate: MediaVolumeOSD {
modelData: item
}
}
Variants {
model: SettingsData.getFilteredScreens("osd")
@@ -581,4 +583,12 @@ Item {
id: hyprlandOverview
}
}
LazyLoader {
id: niriOverviewOverlayLoader
active: CompositorService.isNiri
component: NiriOverviewOverlay {
id: niriOverviewOverlay
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Common
@@ -14,36 +13,44 @@ Item {
required property var dankDashPopoutLoader
required property var notepadSlideoutVariants
required property var hyprKeybindsModalLoader
required property var dankBarLoader
required property var dankBarRepeater
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 {
function open() {
root.powerMenuModalLoader.active = true
root.powerMenuModalLoader.active = true;
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.openCentered()
root.powerMenuModalLoader.item.openCentered();
return "POWERMENU_OPEN_SUCCESS"
return "POWERMENU_OPEN_SUCCESS";
}
function close() {
if (root.powerMenuModalLoader.item)
root.powerMenuModalLoader.item.close()
root.powerMenuModalLoader.item.close();
return "POWERMENU_CLOSE_SUCCESS"
return "POWERMENU_CLOSE_SUCCESS";
}
function toggle() {
root.powerMenuModalLoader.active = true
root.powerMenuModalLoader.active = true;
if (root.powerMenuModalLoader.item) {
if (root.powerMenuModalLoader.item.shouldBeVisible) {
root.powerMenuModalLoader.item.close()
root.powerMenuModalLoader.item.close();
} else {
root.powerMenuModalLoader.item.openCentered()
root.powerMenuModalLoader.item.openCentered();
}
}
return "POWERMENU_TOGGLE_SUCCESS"
return "POWERMENU_TOGGLE_SUCCESS";
}
target: "powermenu"
@@ -51,26 +58,26 @@ Item {
IpcHandler {
function open(): string {
root.processListModalLoader.active = true
root.processListModalLoader.active = true;
if (root.processListModalLoader.item)
root.processListModalLoader.item.show()
root.processListModalLoader.item.show();
return "PROCESSLIST_OPEN_SUCCESS"
return "PROCESSLIST_OPEN_SUCCESS";
}
function close(): string {
if (root.processListModalLoader.item)
root.processListModalLoader.item.hide()
root.processListModalLoader.item.hide();
return "PROCESSLIST_CLOSE_SUCCESS"
return "PROCESSLIST_CLOSE_SUCCESS";
}
function toggle(): string {
root.processListModalLoader.active = true
root.processListModalLoader.active = true;
if (root.processListModalLoader.item)
root.processListModalLoader.item.toggle()
root.processListModalLoader.item.toggle();
return "PROCESSLIST_TOGGLE_SUCCESS"
return "PROCESSLIST_TOGGLE_SUCCESS";
}
target: "processlist"
@@ -78,27 +85,33 @@ Item {
IpcHandler {
function open(): string {
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_OPEN_SUCCESS"
const bar = root.getFirstBar();
if (bar) {
bar.triggerControlCenterOnFocusedScreen();
return "CONTROL_CENTER_OPEN_SUCCESS";
}
return "CONTROL_CENTER_OPEN_FAILED"
return "CONTROL_CENTER_OPEN_FAILED";
}
function close(): string {
if (root.controlCenterLoader.item) {
root.controlCenterLoader.item.close()
return "CONTROL_CENTER_CLOSE_SUCCESS"
function hide(): string {
if (root.controlCenterLoader.item && root.controlCenterLoader.item.shouldBeVisible) {
root.controlCenterLoader.item.close();
return "CONTROL_CENTER_HIDE_SUCCESS";
}
return "CONTROL_CENTER_CLOSE_FAILED"
return "CONTROL_CENTER_HIDE_FAILED";
}
function toggle(): string {
if (root.dankBarLoader.item) {
root.dankBarLoader.item.triggerControlCenterOnFocusedScreen()
return "CONTROL_CENTER_TOGGLE_SUCCESS"
const bar = root.getFirstBar();
if (bar) {
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"
@@ -106,55 +119,56 @@ Item {
IpcHandler {
function open(tab: string): string {
root.dankDashPopoutLoader.active = true
root.dankDashPopoutLoader.active = true;
if (root.dankDashPopoutLoader.item) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0
break
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 2 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen)
root.dankDashPopoutLoader.item.dashVisible = true
return "DASH_OPEN_SUCCESS"
root.dankDashPopoutLoader.item.setTriggerPosition(Screen.width / 2, Theme.barHeight + Theme.spacingS, 100, "center", Screen);
root.dankDashPopoutLoader.item.dashVisible = true;
return "DASH_OPEN_SUCCESS";
}
return "DASH_OPEN_FAILED"
return "DASH_OPEN_FAILED";
}
function close(): string {
if (root.dankDashPopoutLoader.item) {
root.dankDashPopoutLoader.item.dashVisible = false
return "DASH_CLOSE_SUCCESS"
root.dankDashPopoutLoader.item.dashVisible = false;
return "DASH_CLOSE_SUCCESS";
}
return "DASH_CLOSE_FAILED"
return "DASH_CLOSE_FAILED";
}
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) {
switch (tab.toLowerCase()) {
case "media":
root.dankDashPopoutLoader.item.currentTabIndex = 1
break
root.dankDashPopoutLoader.item.currentTabIndex = 1;
break;
case "wallpaper":
root.dankDashPopoutLoader.item.currentTabIndex = 2
break
root.dankDashPopoutLoader.item.currentTabIndex = 2;
break;
case "weather":
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0
break
root.dankDashPopoutLoader.item.currentTabIndex = SettingsData.weatherEnabled ? 3 : 0;
break;
default:
root.dankDashPopoutLoader.item.currentTabIndex = 0
break
root.dankDashPopoutLoader.item.currentTabIndex = 0;
break;
}
}
return "DASH_TOGGLE_SUCCESS"
return "DASH_TOGGLE_SUCCESS";
}
return "DASH_TOGGLE_FAILED"
return "DASH_TOGGLE_FAILED";
}
target: "dash"
@@ -163,68 +177,68 @@ Item {
IpcHandler {
function getFocusedScreenName() {
if (CompositorService.isHyprland && Hyprland.focusedWorkspace && Hyprland.focusedWorkspace.monitor) {
return Hyprland.focusedWorkspace.monitor.name
return Hyprland.focusedWorkspace.monitor.name;
}
if (CompositorService.isNiri && NiriService.currentOutput) {
return NiriService.currentOutput
return NiriService.currentOutput;
}
return ""
return "";
}
function getActiveNotepadInstance() {
if (root.notepadSlideoutVariants.instances.length === 0) {
return null
return null;
}
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) {
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) {
return slideout
return slideout;
}
}
}
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) {
return slideout
return slideout;
}
}
return root.notepadSlideoutVariants.instances[0]
return root.notepadSlideoutVariants.instances[0];
}
function open(): string {
var instance = getActiveNotepadInstance()
var instance = getActiveNotepadInstance();
if (instance) {
instance.show()
return "NOTEPAD_OPEN_SUCCESS"
instance.show();
return "NOTEPAD_OPEN_SUCCESS";
}
return "NOTEPAD_OPEN_FAILED"
return "NOTEPAD_OPEN_FAILED";
}
function close(): string {
var instance = getActiveNotepadInstance()
var instance = getActiveNotepadInstance();
if (instance) {
instance.hide()
return "NOTEPAD_CLOSE_SUCCESS"
instance.hide();
return "NOTEPAD_CLOSE_SUCCESS";
}
return "NOTEPAD_CLOSE_FAILED"
return "NOTEPAD_CLOSE_FAILED";
}
function toggle(): string {
var instance = getActiveNotepadInstance()
var instance = getActiveNotepadInstance();
if (instance) {
instance.toggle()
return "NOTEPAD_TOGGLE_SUCCESS"
instance.toggle();
return "NOTEPAD_TOGGLE_SUCCESS";
}
return "NOTEPAD_TOGGLE_FAILED"
return "NOTEPAD_TOGGLE_FAILED";
}
target: "notepad"
@@ -232,31 +246,31 @@ Item {
IpcHandler {
function toggle(): string {
SessionService.toggleIdleInhibit()
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled"
SessionService.toggleIdleInhibit();
return SessionService.idleInhibited ? "Idle inhibit enabled" : "Idle inhibit disabled";
}
function enable(): string {
SessionService.enableIdleInhibit()
return "Idle inhibit enabled"
SessionService.enableIdleInhibit();
return "Idle inhibit enabled";
}
function disable(): string {
SessionService.disableIdleInhibit()
return "Idle inhibit disabled"
SessionService.disableIdleInhibit();
return "Idle inhibit disabled";
}
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 {
if (!newReason) {
return `Current reason: ${SessionService.inhibitReason}`
return `Current reason: ${SessionService.inhibitReason}`;
}
SessionService.setInhibitReason(newReason)
return `Inhibit reason set to: ${newReason}`
SessionService.setInhibitReason(newReason);
return `Inhibit reason set to: ${newReason}`;
}
target: "inhibit"
@@ -264,42 +278,42 @@ Item {
IpcHandler {
function list(): string {
return MprisController.availablePlayers.map(p => p.identity).join("\n")
return MprisController.availablePlayers.map(p => p.identity).join("\n");
}
function play(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPlay) {
MprisController.activePlayer.play()
MprisController.activePlayer.play();
}
}
function pause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canPause) {
MprisController.activePlayer.pause()
MprisController.activePlayer.pause();
}
}
function playPause(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canTogglePlaying) {
MprisController.activePlayer.togglePlaying()
MprisController.activePlayer.togglePlaying();
}
}
function previous(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoPrevious) {
MprisController.activePlayer.previous()
MprisController.activePlayer.previous();
}
}
function next(): void {
if (MprisController.activePlayer && MprisController.activePlayer.canGoNext) {
MprisController.activePlayer.next()
MprisController.activePlayer.next();
}
}
function stop(): void {
if (MprisController.activePlayer) {
MprisController.activePlayer.stop()
MprisController.activePlayer.stop();
}
}
@@ -309,78 +323,78 @@ Item {
IpcHandler {
function toggle(provider: string): string {
if (!provider) {
return "ERROR: No provider specified"
return "ERROR: No provider specified";
}
KeybindsService.loadProvider(provider)
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProvider(provider);
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
root.hyprKeybindsModalLoader.item.close();
} 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 {
if (!provider) {
return "ERROR: No provider specified"
return "ERROR: No provider specified";
}
KeybindsService.loadProviderWithPath(provider, path)
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProviderWithPath(provider, path);
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
root.hyprKeybindsModalLoader.item.close();
} 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 {
if (!provider) {
return "ERROR: No provider specified"
return "ERROR: No provider specified";
}
KeybindsService.loadProvider(provider)
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProvider(provider);
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return `KEYBINDS_OPEN_SUCCESS: ${provider}`
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider}`;
}
return `KEYBINDS_OPEN_FAILED: ${provider}`
return `KEYBINDS_OPEN_FAILED: ${provider}`;
}
function openWithPath(provider: string, path: string): string {
if (!provider) {
return "ERROR: No provider specified"
return "ERROR: No provider specified";
}
KeybindsService.loadProviderWithPath(provider, path)
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProviderWithPath(provider, path);
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`
root.hyprKeybindsModalLoader.item.open();
return `KEYBINDS_OPEN_SUCCESS: ${provider} (${path})`;
}
return `KEYBINDS_OPEN_FAILED: ${provider}`
return `KEYBINDS_OPEN_FAILED: ${provider}`;
}
function close(): string {
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close()
return "KEYBINDS_CLOSE_SUCCESS"
root.hyprKeybindsModalLoader.item.close();
return "KEYBINDS_CLOSE_SUCCESS";
}
return "KEYBINDS_CLOSE_FAILED"
return "KEYBINDS_CLOSE_FAILED";
}
target: "keybinds"
@@ -389,67 +403,67 @@ Item {
IpcHandler {
function openBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
KeybindsService.loadProvider("hyprland")
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProvider("hyprland");
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.open()
return "HYPR_KEYBINDS_OPEN_SUCCESS"
root.hyprKeybindsModalLoader.item.open();
return "HYPR_KEYBINDS_OPEN_SUCCESS";
}
return "HYPR_KEYBINDS_OPEN_FAILED"
return "HYPR_KEYBINDS_OPEN_FAILED";
}
function closeBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
if (root.hyprKeybindsModalLoader.item) {
root.hyprKeybindsModalLoader.item.close()
return "HYPR_KEYBINDS_CLOSE_SUCCESS"
root.hyprKeybindsModalLoader.item.close();
return "HYPR_KEYBINDS_CLOSE_SUCCESS";
}
return "HYPR_KEYBINDS_CLOSE_FAILED"
return "HYPR_KEYBINDS_CLOSE_FAILED";
}
function toggleBinds(): string {
if (!CompositorService.isHyprland) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
KeybindsService.loadProvider("hyprland")
root.hyprKeybindsModalLoader.active = true
KeybindsService.loadProvider("hyprland");
root.hyprKeybindsModalLoader.active = true;
if (root.hyprKeybindsModalLoader.item) {
if (root.hyprKeybindsModalLoader.item.shouldBeVisible) {
root.hyprKeybindsModalLoader.item.close()
root.hyprKeybindsModalLoader.item.close();
} 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 {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS"
root.hyprlandOverviewLoader.item.overviewOpen = !root.hyprlandOverviewLoader.item.overviewOpen;
return root.hyprlandOverviewLoader.item.overviewOpen ? "OVERVIEW_OPEN_SUCCESS" : "OVERVIEW_CLOSE_SUCCESS";
}
function closeOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
root.hyprlandOverviewLoader.item.overviewOpen = false
return "OVERVIEW_CLOSE_SUCCESS"
root.hyprlandOverviewLoader.item.overviewOpen = false;
return "OVERVIEW_CLOSE_SUCCESS";
}
function openOverview(): string {
if (!CompositorService.isHyprland || !root.hyprlandOverviewLoader.item) {
return "HYPR_NOT_AVAILABLE"
return "HYPR_NOT_AVAILABLE";
}
root.hyprlandOverviewLoader.item.overviewOpen = true
return "OVERVIEW_OPEN_SUCCESS"
root.hyprlandOverviewLoader.item.overviewOpen = true;
return "OVERVIEW_OPEN_SUCCESS";
}
target: "hypr"
@@ -457,12 +471,96 @@ Item {
IpcHandler {
function wallpaper(): string {
if (root.dankBarLoader.item && root.dankBarLoader.item.triggerWallpaperBrowserOnFocusedScreen()) {
return "SUCCESS: Toggled wallpaper browser"
const bar = root.getFirstBar();
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"
}
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"
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -9,6 +10,11 @@ DankModal {
layerNamespace: "dms:bluetooth-pairing"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string deviceName: ""
property string deviceAddress: ""
property string requestType: ""
@@ -18,38 +24,38 @@ DankModal {
property string passkeyInput: ""
function show(pairingData) {
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData))
token = pairingData.token || ""
deviceName = pairingData.deviceName || ""
deviceAddress = pairingData.deviceAddr || ""
requestType = pairingData.requestType || ""
passkey = pairingData.passkey || 0
pinInput = ""
passkeyInput = ""
console.log("BluetoothPairingModal.show() called:", JSON.stringify(pairingData));
token = pairingData.token || "";
deviceName = pairingData.deviceName || "";
deviceAddress = pairingData.deviceAddr || "";
requestType = pairingData.requestType || "";
passkey = pairingData.passkey || 0;
pinInput = "";
passkeyInput = "";
console.log("BluetoothPairingModal: Calling open()")
open()
console.log("BluetoothPairingModal: Calling open()");
open();
Qt.callLater(() => {
if (contentLoader.item) {
if (requestType === "pin" && contentLoader.item.pinInputField) {
contentLoader.item.pinInputField.forceActiveFocus()
contentLoader.item.pinInputField.forceActiveFocus();
} else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
contentLoader.item.passkeyInputField.forceActiveFocus()
contentLoader.item.passkeyInputField.forceActiveFocus();
}
}
})
});
}
shouldBeVisible: false
allowStacking: true
keepPopoutsOpen: true
width: 420
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240
modalWidth: 420
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) {
pinInput = ""
passkeyInput = ""
pinInput = "";
passkeyInput = "";
}
}
@@ -57,22 +63,22 @@ DankModal {
Qt.callLater(() => {
if (contentLoader.item) {
if (requestType === "pin" && contentLoader.item.pinInputField) {
contentLoader.item.pinInputField.forceActiveFocus()
contentLoader.item.pinInputField.forceActiveFocus();
} else if (requestType === "passkey" && contentLoader.item.passkeyInputField) {
contentLoader.item.passkeyInputField.forceActiveFocus()
contentLoader.item.passkeyInputField.forceActiveFocus();
}
}
})
});
}
onBackgroundClicked: () => {
if (token) {
DMSService.bluetoothCancelPairing(token)
DMSService.bluetoothCancelPairing(token);
}
close()
token = ""
pinInput = ""
passkeyInput = ""
close();
token = "";
pinInput = "";
passkeyInput = "";
}
content: Component {
@@ -88,13 +94,13 @@ DankModal {
Keys.onEscapePressed: event => {
if (token) {
DMSService.bluetoothCancelPairing(token)
DMSService.bluetoothCancelPairing(token);
}
close()
token = ""
pinInput = ""
passkeyInput = ""
event.accepted = true
close();
token = "";
pinInput = "";
passkeyInput = "";
event.accepted = true;
}
Column {
@@ -122,19 +128,19 @@ DankModal {
text: {
switch (requestType) {
case "confirm":
return I18n.tr("Confirm passkey for ") + deviceName
return I18n.tr("Confirm passkey for ") + deviceName;
case "display-passkey":
return I18n.tr("Enter this passkey on ") + deviceName
return I18n.tr("Enter this passkey on ") + deviceName;
case "authorize":
return I18n.tr("Authorize pairing with ") + deviceName
return I18n.tr("Authorize pairing with ") + deviceName;
case "pin":
return I18n.tr("Enter PIN for ") + deviceName
return I18n.tr("Enter PIN for ") + deviceName;
case "passkey":
return I18n.tr("Enter passkey for ") + deviceName
return I18n.tr("Enter passkey for ") + deviceName;
default:
if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize service for ") + deviceName
return deviceName
return I18n.tr("Authorize service for ") + deviceName;
return deviceName;
}
}
font.pixelSize: Theme.fontSizeMedium
@@ -156,7 +162,7 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
pinInputField.forceActiveFocus()
pinInputField.forceActiveFocus();
}
}
@@ -171,10 +177,10 @@ DankModal {
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
pinInput = text
pinInput = text;
}
onAccepted: () => {
submitPairing()
submitPairing();
}
}
}
@@ -191,7 +197,7 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
passkeyInputField.forceActiveFocus()
passkeyInputField.forceActiveFocus();
}
}
@@ -206,10 +212,10 @@ DankModal {
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
passkeyInput = text
passkeyInput = text;
}
onAccepted: () => {
submitPairing()
submitPairing();
}
}
}
@@ -277,12 +283,12 @@ DankModal {
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (token) {
DMSService.bluetoothCancelPairing(token)
DMSService.bluetoothCancelPairing(token);
}
close()
token = ""
pinInput = ""
passkeyInput = ""
close();
token = "";
pinInput = "";
passkeyInput = "";
}
}
}
@@ -294,10 +300,10 @@ DankModal {
color: pairArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: {
if (requestType === "pin")
return pinInput.length > 0
return pinInput.length > 0;
if (requestType === "passkey")
return passkeyInput.length === 6
return true
return passkeyInput.length === 6;
return true;
}
opacity: enabled ? 1 : 0.5
@@ -309,13 +315,13 @@ DankModal {
switch (requestType) {
case "confirm":
case "display-passkey":
return I18n.tr("Confirm")
return I18n.tr("Confirm");
case "authorize":
return I18n.tr("Authorize")
return I18n.tr("Authorize");
default:
if (requestType.startsWith("authorize-service"))
return I18n.tr("Authorize")
return I18n.tr("Pair")
return I18n.tr("Authorize");
return I18n.tr("Pair");
}
}
font.pixelSize: Theme.fontSizeMedium
@@ -331,7 +337,7 @@ DankModal {
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
submitPairing()
submitPairing();
}
}
@@ -356,48 +362,48 @@ DankModal {
iconColor: Theme.surfaceText
onClicked: () => {
if (token) {
DMSService.bluetoothCancelPairing(token)
DMSService.bluetoothCancelPairing(token);
}
close()
token = ""
pinInput = ""
passkeyInput = ""
close();
token = "";
pinInput = "";
passkeyInput = "";
}
}
}
}
function submitPairing() {
const secrets = {}
const secrets = {};
switch (requestType) {
case "pin":
secrets["pin"] = pinInput
break
secrets["pin"] = pinInput;
break;
case "passkey":
secrets["passkey"] = passkeyInput
break
secrets["passkey"] = passkeyInput;
break;
case "confirm":
case "display-passkey":
case "authorize":
secrets["decision"] = "yes"
break
secrets["decision"] = "yes";
break;
default:
if (requestType.startsWith("authorize-service")) {
secrets["decision"] = "yes"
secrets["decision"] = "yes";
}
break
break;
}
DMSService.bluetoothSubmitPairing(token, secrets, true, response => {
if (response.error) {
ToastService.showError(I18n.tr("Pairing failed"), response.error)
ToastService.showError(I18n.tr("Pairing failed"), response.error);
}
})
});
close()
token = ""
pinInput = ""
passkeyInput = ""
close();
token = "";
pinInput = "";
passkeyInput = "";
}
}

View File

@@ -1,19 +1,23 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
id: clipboardHistoryModal
layerNamespace: "dms:clipboard"
HyprlandFocusGrab {
windows: [clipboardHistoryModal]
active: CompositorService.isHyprland && clipboardHistoryModal.shouldHaveFocus
}
property int totalCount: 0
property var clipboardEntries: []
property string searchText: ""
@@ -25,119 +29,119 @@ DankModal {
readonly property int maxConcurrentLoads: 3
function updateFilteredModel() {
filteredClipboardModel.clear()
filteredClipboardModel.clear();
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) {
filteredClipboardModel.append({
"entry": entry
})
"entry": entry
});
} else {
const content = getEntryPreview(entry).toLowerCase()
const content = getEntryPreview(entry).toLowerCase();
if (content.includes(searchText.toLowerCase())) {
filteredClipboardModel.append({
"entry": entry
})
"entry": entry
});
}
}
}
clipboardHistoryModal.totalCount = filteredClipboardModel.count
clipboardHistoryModal.totalCount = filteredClipboardModel.count;
if (filteredClipboardModel.count === 0) {
keyboardNavigationActive = false
selectedIndex = 0
keyboardNavigationActive = false;
selectedIndex = 0;
} else if (selectedIndex >= filteredClipboardModel.count) {
selectedIndex = filteredClipboardModel.count - 1
selectedIndex = filteredClipboardModel.count - 1;
}
}
function toggle() {
if (shouldBeVisible) {
hide()
hide();
} else {
show()
show();
}
}
function show() {
open()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
clipboardHistoryModal.shouldHaveFocus = true
refreshClipboard()
keyboardController.reset()
open();
clipboardHistoryModal.searchText = "";
clipboardHistoryModal.activeImageLoads = 0;
clipboardHistoryModal.shouldHaveFocus = true;
refreshClipboard();
keyboardController.reset();
Qt.callLater(function () {
if (contentLoader.item && contentLoader.item.searchField) {
contentLoader.item.searchField.text = ""
contentLoader.item.searchField.forceActiveFocus()
contentLoader.item.searchField.text = "";
contentLoader.item.searchField.forceActiveFocus();
}
})
});
}
function hide() {
close()
clipboardHistoryModal.searchText = ""
clipboardHistoryModal.activeImageLoads = 0
updateFilteredModel()
keyboardController.reset()
cleanupTempFiles()
close();
clipboardHistoryModal.searchText = "";
clipboardHistoryModal.activeImageLoads = 0;
updateFilteredModel();
keyboardController.reset();
cleanupTempFiles();
}
function cleanupTempFiles() {
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"])
Quickshell.execDetached(["sh", "-c", "rm -f /tmp/clipboard_*.png"]);
}
function refreshClipboard() {
clipboardProcesses.refresh()
clipboardProcesses.refresh();
}
function copyEntry(entry) {
const entryId = entry.split('\t')[0]
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`])
ToastService.showInfo(I18n.tr("Copied to clipboard"))
hide()
const entryId = entry.split('\t')[0];
Quickshell.execDetached(["sh", "-c", `cliphist decode ${entryId} | wl-copy`]);
ToastService.showInfo(I18n.tr("Copied to clipboard"));
hide();
}
function deleteEntry(entry) {
clipboardProcesses.deleteEntry(entry)
clipboardProcesses.deleteEntry(entry);
}
function clearAll() {
clipboardProcesses.clearAll()
clipboardProcesses.clearAll();
}
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)) {
const dimensionMatch = content.match(/(\d+)x(\d+)/)
const dimensionMatch = content.match(/(\d+)x(\d+)/);
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) {
return `Image (${typeMatch[1].toUpperCase()})`
return `Image (${typeMatch[1].toUpperCase()})`;
}
return "Image"
return "Image";
}
if (content.length > ClipboardConstants.previewLength) {
return content.substring(0, ClipboardConstants.previewLength) + "..."
return content.substring(0, ClipboardConstants.previewLength) + "...";
}
return content
return content;
}
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)) {
return "image"
return "image";
}
if (entry.length > ClipboardConstants.longTextThreshold) {
return "long_text"
return "long_text";
}
return "text"
return "text";
}
visible: false
width: ClipboardConstants.modalWidth
height: ClipboardConstants.modalHeight
modalWidth: ClipboardConstants.modalWidth
modalHeight: ClipboardConstants.modalHeight
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
@@ -145,7 +149,7 @@ DankModal {
enableShadow: true
onBackgroundClicked: hide()
modalFocusScope.Keys.onPressed: function (event) {
keyboardController.handleKey(event)
keyboardController.handleKey(event);
}
content: clipboardContent
@@ -160,12 +164,12 @@ DankModal {
confirmButtonColor: Theme.primary
onVisibleChanged: {
if (visible) {
clipboardHistoryModal.shouldHaveFocus = false
clipboardHistoryModal.shouldHaveFocus = false;
} else if (clipboardHistoryModal.shouldBeVisible) {
clipboardHistoryModal.shouldHaveFocus = true
clipboardHistoryModal.modalFocusScope.forceActiveFocus()
clipboardHistoryModal.shouldHaveFocus = true;
clipboardHistoryModal.modalFocusScope.forceActiveFocus();
if (clipboardHistoryModal.contentLoader.item && clipboardHistoryModal.contentLoader.item.searchField) {
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus()
clipboardHistoryModal.contentLoader.item.searchField.forceActiveFocus();
}
}
}
@@ -192,18 +196,18 @@ DankModal {
IpcHandler {
function open(): string {
clipboardHistoryModal.show()
return "CLIPBOARD_OPEN_SUCCESS"
clipboardHistoryModal.show();
return "CLIPBOARD_OPEN_SUCCESS";
}
function close(): string {
clipboardHistoryModal.hide()
return "CLIPBOARD_CLOSE_SUCCESS"
clipboardHistoryModal.hide();
return "CLIPBOARD_CLOSE_SUCCESS";
}
function toggle(): string {
clipboardHistoryModal.toggle()
return "CLIPBOARD_TOGGLE_SUCCESS"
clipboardHistoryModal.toggle();
return "CLIPBOARD_TOGGLE_SUCCESS";
}
target: "clipboard"

View File

@@ -19,141 +19,141 @@ DankModal {
property bool keyboardNavigation: false
function show(title, message, onConfirmCallback, onCancelCallback) {
confirmTitle = title || ""
confirmMessage = message || ""
confirmButtonText = "Confirm"
cancelButtonText = "Cancel"
confirmButtonColor = Theme.primary
onConfirm = onConfirmCallback || (() => {})
onCancel = onCancelCallback || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
confirmTitle = title || "";
confirmMessage = message || "";
confirmButtonText = "Confirm";
cancelButtonText = "Cancel";
confirmButtonColor = Theme.primary;
onConfirm = onConfirmCallback || (() => {});
onCancel = onCancelCallback || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function showWithOptions(options) {
confirmTitle = options.title || ""
confirmMessage = options.message || ""
confirmButtonText = options.confirmText || "Confirm"
cancelButtonText = options.cancelText || "Cancel"
confirmButtonColor = options.confirmColor || Theme.primary
onConfirm = options.onConfirm || (() => {})
onCancel = options.onCancel || (() => {})
selectedButton = -1
keyboardNavigation = false
open()
confirmTitle = options.title || "";
confirmMessage = options.message || "";
confirmButtonText = options.confirmText || "Confirm";
cancelButtonText = options.cancelText || "Cancel";
confirmButtonColor = options.confirmColor || Theme.primary;
onConfirm = options.onConfirm || (() => {});
onCancel = options.onCancel || (() => {});
selectedButton = -1;
keyboardNavigation = false;
open();
}
function selectButton() {
close()
close();
if (selectedButton === 0) {
if (onCancel) {
onCancel()
onCancel();
}
} else {
if (onConfirm) {
onConfirm()
onConfirm();
}
}
}
shouldBeVisible: false
allowStacking: true
width: 350
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
modalWidth: 350
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 160
enableShadow: true
shouldHaveFocus: true
onBackgroundClicked: {
close()
close();
if (onCancel) {
onCancel()
onCancel();
}
}
onOpened: {
Qt.callLater(function () {
modalFocusScope.forceActiveFocus()
modalFocusScope.focus = true
shouldHaveFocus = true
})
modalFocusScope.forceActiveFocus();
modalFocusScope.focus = true;
shouldHaveFocus = true;
});
}
modalFocusScope.Keys.onPressed: function (event) {
switch (event.key) {
case Qt.Key_Escape:
close()
close();
if (onCancel) {
onCancel()
onCancel();
}
event.accepted = true
break
event.accepted = true;
break;
case Qt.Key_Left:
case Qt.Key_Up:
keyboardNavigation = true
selectedButton = 0
event.accepted = true
break
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
break;
case Qt.Key_Right:
case Qt.Key_Down:
keyboardNavigation = true
selectedButton = 1
event.accepted = true
break
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = (selectedButton + 1) % 2
event.accepted = true
keyboardNavigation = true;
selectedButton = (selectedButton + 1) % 2;
event.accepted = true;
}
break
break;
case Qt.Key_P:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2
event.accepted = true
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 1 : (selectedButton - 1 + 2) % 2;
event.accepted = true;
}
break
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break
break;
case Qt.Key_H:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 0
event.accepted = true
keyboardNavigation = true;
selectedButton = 0;
event.accepted = true;
}
break
break;
case Qt.Key_L:
if (event.modifiers & Qt.ControlModifier) {
keyboardNavigation = true
selectedButton = 1
event.accepted = true
keyboardNavigation = true;
selectedButton = 1;
event.accepted = true;
}
break
break;
case Qt.Key_Tab:
keyboardNavigation = true
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2
event.accepted = true
break
keyboardNavigation = true;
selectedButton = selectedButton === -1 ? 0 : (selectedButton + 1) % 2;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
if (selectedButton !== -1) {
selectButton()
selectButton();
} else {
selectedButton = 1
selectButton()
selectedButton = 1;
selectButton();
}
event.accepted = true
break
event.accepted = true;
break;
}
}
@@ -210,11 +210,11 @@ DankModal {
radius: Theme.cornerRadius
color: {
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) {
return Theme.surfacePressed
return Theme.surfacePressed;
} else {
return Theme.surfaceVariantAlpha
return Theme.surfaceVariantAlpha;
}
}
border.color: (keyboardNavigation && selectedButton === 0) ? Theme.primary : "transparent"
@@ -235,8 +235,8 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 0
selectButton()
selectedButton = 0;
selectButton();
}
}
}
@@ -246,13 +246,13 @@ DankModal {
height: 40
radius: Theme.cornerRadius
color: {
const baseColor = confirmButtonColor
const baseColor = confirmButtonColor;
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) {
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9)
return Qt.rgba(baseColor.r, baseColor.g, baseColor.b, 0.9);
} else {
return baseColor
return baseColor;
}
}
border.color: (keyboardNavigation && selectedButton === 1) ? "white" : "transparent"
@@ -273,8 +273,8 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
selectedButton = 1
selectButton()
selectedButton = 1;
selectButton();
}
}
}

View File

@@ -1,25 +1,24 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import Quickshell.Wayland
import qs.Common
import qs.Services
import qs.Widgets
PanelWindow {
Item {
id: root
property string layerNamespace: "dms:modal"
WlrLayershell.namespace: layerNamespace
property alias content: contentLoader.sourceComponent
property alias contentLoader: contentLoader
property Item directContent: null
property real width: 400
property real height: 300
readonly property real screenWidth: screen ? screen.width : 1920
readonly property real screenHeight: screen ? screen.height : 1080
readonly property real dpr: CompositorService.getScreenScale(screen)
property real modalWidth: 400
property real modalHeight: 300
property var targetScreen
readonly property var effectiveScreen: targetScreen || contentWindow.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 real backgroundOpacity: 0.5
property string positioning: "center"
@@ -44,295 +43,340 @@ PanelWindow {
property bool allowStacking: false
property bool keepContentLoaded: false
property bool keepPopoutsOpen: false
property var customKeyboardFocus: null
signal opened
signal dialogClosed
signal backgroundClicked
readonly property bool useBackgroundWindow: {
const layerOverride = Quickshell.env("DMS_MODAL_LAYER");
return !layerOverride || layerOverride === "overlay";
}
function open() {
ModalManager.openModal(root)
closeTimer.stop()
shouldBeVisible = true
visible = true
shouldHaveFocus = false
ModalManager.openModal(root);
closeTimer.stop();
shouldBeVisible = true;
if (useBackgroundWindow)
backgroundWindow.visible = true;
contentWindow.visible = true;
shouldHaveFocus = false;
Qt.callLater(() => {
shouldHaveFocus = Qt.binding(() => shouldBeVisible)
})
shouldHaveFocus = Qt.binding(() => shouldBeVisible);
});
}
function close() {
shouldBeVisible = false
shouldHaveFocus = false
closeTimer.restart()
shouldBeVisible = false;
shouldHaveFocus = false;
closeTimer.restart();
}
function toggle() {
if (shouldBeVisible) {
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()
}
shouldBeVisible ? close() : open();
}
Connections {
target: ModalManager
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== root && !allowStacking && shouldBeVisible) {
close()
close();
}
}
target: ModalManager
}
Timer {
id: closeTimer
interval: animationDuration + 120
onTriggered: {
visible = false
}
}
anchors {
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
if (!shouldBeVisible) {
contentWindow.visible = false;
if (useBackgroundWindow)
backgroundWindow.visible = false;
dialogClosed();
}
}
}
Item {
id: modalContainer
readonly property real shadowBuffer: 5
readonly property real alignedWidth: Theme.px(modalWidth, dpr)
readonly property real alignedHeight: Theme.px(modalHeight, dpr)
width: Theme.px(root.width, dpr)
height: Theme.px(root.height, dpr)
x: {
if (positioning === "center") {
return Theme.snap((root.screenWidth - width) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Math.max(Theme.spacingL, root.screenWidth - width - Theme.spacingL), dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.x, dpr)
readonly property real alignedX: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenWidth - alignedWidth) / 2;
case "top-right":
return Math.max(Theme.spacingL, screenWidth - alignedWidth - Theme.spacingL);
case "custom":
return customPosition.x;
default:
return 0;
}
return 0
}
y: {
if (positioning === "center") {
return Theme.snap((root.screenHeight - height) / 2, dpr)
} else if (positioning === "top-right") {
return Theme.px(Theme.barHeight + Theme.spacingXS, dpr)
} else if (positioning === "custom") {
return Theme.snap(root.customPosition.y, dpr)
})(), dpr)
readonly property real alignedY: Theme.snap((() => {
switch (positioning) {
case "center":
return (screenHeight - alignedHeight) / 2;
case "top-right":
return Theme.barHeight + Theme.spacingXS;
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"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
MouseArea {
anchors.fill: parent
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
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
if (!outsideContent)
return;
root.backgroundClicked();
}
}
Behavior on animX {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
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
}
}
}
}
Behavior on animY {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
PanelWindow {
id: contentWindow
visible: false
color: "transparent"
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 {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
anchors {
left: true
top: true
}
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 {
id: contentContainer
id: modalContainer
x: shadowBuffer
y: shadowBuffer
width: root.alignedWidth
height: root.alignedHeight
anchors.centerIn: parent
width: parent.width
height: parent.height
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)
readonly property bool slide: root.animationType === "slide"
readonly property real offsetX: slide ? 15 : 0
readonly property real offsetY: slide ? -30 : root.animationOffset
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 {
duration: animationDuration
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
radius: root.cornerRadius
Behavior on animY {
NumberAnimation {
duration: root.animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
FocusScope {
anchors.fill: parent
focus: root.shouldBeVisible
Behavior on scaleValue {
NumberAnimation {
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
Item {
id: directContentWrapper
id: animatedContent
anchors.fill: parent
visible: root.directContent !== null
focus: true
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: {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
Behavior on opacity {
NumberAnimation {
duration: animationDuration
easing.type: Easing.BezierSpline
easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
}
}
Connections {
function onDirectContentChanged() {
if (root.directContent) {
root.directContent.parent = directContentWrapper
root.directContent.anchors.fill = directContentWrapper
Qt.callLater(() => root.directContent.forceActiveFocus())
DankRectangle {
anchors.fill: parent
color: root.backgroundColor
borderColor: root.borderColor
borderWidth: root.borderWidth
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 {
id: contentLoader
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())
onLoaded: {
if (item) {
Qt.callLater(() => item.forceActiveFocus());
}
}
}
}
}
}
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || root.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close()
event.accepted = true
}
}
FocusScope {
id: focusScope
objectName: "modalFocusScope"
anchors.fill: parent
visible: root.shouldBeVisible || contentWindow.visible
focus: root.shouldBeVisible
Keys.onEscapePressed: event => {
if (root.closeOnEscapeKey && shouldHaveFocus) {
root.close();
event.accepted = true;
}
}
}
}
}

View File

@@ -1,7 +1,6 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -12,7 +11,12 @@ DankModal {
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 var onColorSelectedCallback: null
@@ -26,87 +30,79 @@ DankModal {
property real gradientX: 0
property real gradientY: 0
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"
]
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"]
function show() {
currentColor = selectedColor
updateFromColor(currentColor)
open()
currentColor = selectedColor;
updateFromColor(currentColor);
open();
}
function hide() {
onColorSelectedCallback = null
close()
onColorSelectedCallback = null;
close();
}
function hideInstant() {
onColorSelectedCallback = null
shouldBeVisible = false
visible = false
onColorSelectedCallback = null;
shouldBeVisible = false;
visible = false;
}
onColorSelected: (color) => {
onColorSelected: color => {
if (onColorSelectedCallback) {
onColorSelectedCallback(color)
onColorSelectedCallback(color);
}
}
function copyColorToClipboard(colorValue) {
Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`])
ToastService.showInfo(`Color ${colorValue} copied`)
SessionData.addRecentColor(currentColor)
Quickshell.execDetached(["sh", "-c", `echo -n "${colorValue}" | wl-copy`]);
ToastService.showInfo(`Color ${colorValue} copied`);
SessionData.addRecentColor(currentColor);
}
function updateFromColor(color) {
hue = color.hsvHue
saturation = color.hsvSaturation
value = color.hsvValue
alpha = color.a
gradientX = saturation
gradientY = 1 - value
hue = color.hsvHue;
saturation = color.hsvSaturation;
value = color.hsvValue;
alpha = color.a;
gradientX = saturation;
gradientY = 1 - value;
}
function updateColor() {
currentColor = Qt.hsva(hue, saturation, value, alpha)
currentColor = Qt.hsva(hue, saturation, value, alpha);
}
function updateColorFromGradient(x, y) {
saturation = Math.max(0, Math.min(1, x))
value = Math.max(0, Math.min(1, 1 - y))
updateColor()
selectedColor = currentColor
saturation = Math.max(0, Math.min(1, x));
value = Math.max(0, Math.min(1, 1 - y));
updateColor();
selectedColor = currentColor;
}
function pickColorFromScreen() {
hideInstant()
hideInstant();
Proc.runCommand("hyprpicker", ["hyprpicker", "--format=hex"], (output, errorCode) => {
if (errorCode !== 0) {
console.warn("hyprpicker exited with code:", errorCode)
root.show()
return
console.warn("hyprpicker exited with code:", errorCode);
root.show();
return;
}
const colorStr = output.trim()
const colorStr = output.trim();
if (colorStr.length >= 7 && colorStr.startsWith('#')) {
const pickedColor = Qt.color(colorStr)
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
copyColorToClipboard(colorStr)
root.show()
const pickedColor = Qt.color(colorStr);
root.selectedColor = pickedColor;
root.currentColor = pickedColor;
root.updateFromColor(pickedColor);
copyColorToClipboard(colorStr);
root.show();
}
})
});
}
width: 680
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
modalWidth: 680
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 680
backgroundColor: Theme.surfaceContainer
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
@@ -127,8 +123,8 @@ DankModal {
focus: true
Keys.onEscapePressed: event => {
root.hide()
event.accepted = true
root.hide();
event.accepted = true;
}
Column {
@@ -166,7 +162,7 @@ DankModal {
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
root.pickColorFromScreen()
root.pickColorFromScreen();
}
}
@@ -175,7 +171,7 @@ DankModal {
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
root.hide()
root.hide();
}
}
}
@@ -201,8 +197,14 @@ DankModal {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "#ffffff" }
GradientStop { position: 1.0; color: "transparent" }
GradientStop {
position: 0.0
color: "#ffffff"
}
GradientStop {
position: 1.0
color: "transparent"
}
}
}
@@ -210,8 +212,14 @@ DankModal {
anchors.fill: parent
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 1.0; color: "#000000" }
GradientStop {
position: 0.0
color: "transparent"
}
GradientStop {
position: 1.0
color: "#000000"
}
}
}
}
@@ -242,19 +250,19 @@ DankModal {
anchors.fill: parent
cursorShape: Qt.CrossCursor
onPressed: mouse => {
const x = Math.max(0, Math.min(1, mouse.x / width))
const y = Math.max(0, Math.min(1, mouse.y / height))
root.gradientX = x
root.gradientY = y
root.updateColorFromGradient(x, y)
const x = Math.max(0, Math.min(1, mouse.x / width));
const y = Math.max(0, Math.min(1, mouse.y / height));
root.gradientX = x;
root.gradientY = y;
root.updateColorFromGradient(x, y);
}
onPositionChanged: mouse => {
if (pressed) {
const x = Math.max(0, Math.min(1, mouse.x / width))
const y = Math.max(0, Math.min(1, mouse.y / height))
root.gradientX = x
root.gradientY = y
root.updateColorFromGradient(x, y)
const x = Math.max(0, Math.min(1, mouse.x / width));
const y = Math.max(0, Math.min(1, mouse.y / height));
root.gradientX = x;
root.gradientY = y;
root.updateColorFromGradient(x, y);
}
}
}
@@ -270,13 +278,34 @@ DankModal {
gradient: Gradient {
orientation: Gradient.Vertical
GradientStop { position: 0.00; color: "#ff0000" }
GradientStop { position: 0.17; 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" }
GradientStop {
position: 0.00
color: "#ff0000"
}
GradientStop {
position: 0.17
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 {
@@ -293,17 +322,17 @@ DankModal {
anchors.fill: parent
cursorShape: Qt.SizeVerCursor
onPressed: mouse => {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
root.selectedColor = root.currentColor
const h = Math.max(0, Math.min(1, mouse.y / height));
root.hue = h;
root.updateColor();
root.selectedColor = root.currentColor;
}
onPositionChanged: mouse => {
if (pressed) {
const h = Math.max(0, Math.min(1, mouse.y / height))
root.hue = h
root.updateColor()
root.selectedColor = root.currentColor
const h = Math.max(0, Math.min(1, mouse.y / height));
root.hue = h;
root.updateColor();
root.selectedColor = root.currentColor;
}
}
}
@@ -342,10 +371,10 @@ DankModal {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: () => {
const pickedColor = Qt.color(modelData)
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
const pickedColor = Qt.color(modelData);
root.selectedColor = pickedColor;
root.currentColor = pickedColor;
root.updateFromColor(pickedColor);
}
}
}
@@ -387,9 +416,9 @@ DankModal {
color: {
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
@@ -400,10 +429,10 @@ DankModal {
enabled: index < SessionData.recentColors.length
onClicked: () => {
if (index < SessionData.recentColors.length) {
const pickedColor = SessionData.recentColors[index]
root.selectedColor = pickedColor
root.currentColor = pickedColor
root.updateFromColor(pickedColor)
const pickedColor = SessionData.recentColors[index];
root.selectedColor = pickedColor;
root.currentColor = pickedColor;
root.updateFromColor(pickedColor);
}
}
}
@@ -429,10 +458,10 @@ DankModal {
minimum: 0
maximum: 100
showValue: false
onSliderValueChanged: (newValue) => {
root.alpha = newValue / 100
root.updateColor()
root.selectedColor = root.currentColor
onSliderValueChanged: newValue => {
root.alpha = newValue / 100;
root.updateColor();
root.selectedColor = root.currentColor;
}
}
}
@@ -479,9 +508,10 @@ DankModal {
text: root.currentColor.toString()
font.pixelSize: Theme.fontSizeMedium
textColor: {
if (text.length === 0) return Theme.surfaceText
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
return hexPattern.test(text) ? Theme.surfaceText : Theme.error
if (text.length === 0)
return Theme.surfaceText;
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
return hexPattern.test(text) ? Theme.surfaceText : Theme.error;
}
placeholderText: "#000000"
backgroundColor: Theme.surfaceHover
@@ -490,13 +520,14 @@ DankModal {
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onAccepted: () => {
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/
if (!hexPattern.test(text)) return
const color = Qt.color(text)
const hexPattern = /^#?[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/;
if (!hexPattern.test(text))
return;
const color = Qt.color(text);
if (color) {
root.selectedColor = color
root.currentColor = color
root.updateFromColor(color)
root.selectedColor = color;
root.currentColor = color;
root.updateFromColor(color);
}
}
}
@@ -508,7 +539,7 @@ DankModal {
buttonSize: 36
anchors.verticalCenter: parent.verticalCenter
onClicked: () => {
root.copyColorToClipboard(hexInput.text)
root.copyColorToClipboard(hexInput.text);
}
}
}
@@ -540,14 +571,14 @@ DankModal {
StyledText {
anchors.centerIn: parent
text: {
const r = Math.round(root.currentColor.r * 255)
const g = Math.round(root.currentColor.g * 255)
const b = Math.round(root.currentColor.b * 255)
const r = Math.round(root.currentColor.r * 255);
const g = Math.round(root.currentColor.g * 255);
const b = Math.round(root.currentColor.b * 255);
if (root.alpha < 1) {
const a = Math.round(root.alpha * 255)
return `${r}, ${g}, ${b}, ${a}`
const a = Math.round(root.alpha * 255);
return `${r}, ${g}, ${b}, ${a}`;
}
return `${r}, ${g}, ${b}`
return `${r}, ${g}, ${b}`;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
@@ -561,18 +592,18 @@ DankModal {
buttonSize: 36
anchors.verticalCenter: parent.verticalCenter
onClicked: () => {
const r = Math.round(root.currentColor.r * 255)
const g = Math.round(root.currentColor.g * 255)
const b = Math.round(root.currentColor.b * 255)
let rgbString
const r = Math.round(root.currentColor.r * 255);
const g = Math.round(root.currentColor.g * 255);
const b = Math.round(root.currentColor.b * 255);
let rgbString;
if (root.alpha < 1) {
const a = Math.round(root.alpha * 255)
rgbString = `rgba(${r}, ${g}, ${b}, ${a})`
const a = Math.round(root.alpha * 255);
rgbString = `rgba(${r}, ${g}, ${b}, ${a})`;
} else {
rgbString = `rgb(${r}, ${g}, ${b})`
rgbString = `rgb(${r}, ${g}, ${b})`;
}
Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`])
ToastService.showInfo(`${rgbString} copied`)
Quickshell.execDetached(["sh", "-c", `echo -n "${rgbString}" | wl-copy`]);
ToastService.showInfo(`${rgbString} copied`);
}
}
}
@@ -604,14 +635,14 @@ DankModal {
StyledText {
anchors.centerIn: parent
text: {
const h = Math.round(root.hue * 360)
const s = Math.round(root.saturation * 100)
const v = Math.round(root.value * 100)
const h = Math.round(root.hue * 360);
const s = Math.round(root.saturation * 100);
const v = Math.round(root.value * 100);
if (root.alpha < 1) {
const a = Math.round(root.alpha * 100)
return `${h}°, ${s}%, ${v}%, ${a}%`
const a = Math.round(root.alpha * 100);
return `${h}°, ${s}%, ${v}%, ${a}%`;
}
return `${h}°, ${s}%, ${v}%`
return `${h}°, ${s}%, ${v}%`;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
@@ -625,18 +656,18 @@ DankModal {
buttonSize: 36
anchors.verticalCenter: parent.verticalCenter
onClicked: () => {
const h = Math.round(root.hue * 360)
const s = Math.round(root.saturation * 100)
const v = Math.round(root.value * 100)
let hsvString
const h = Math.round(root.hue * 360);
const s = Math.round(root.saturation * 100);
const v = Math.round(root.value * 100);
let hsvString;
if (root.alpha < 1) {
const a = Math.round(root.alpha * 100)
hsvString = `${h}, ${s}, ${v}, ${a}`
const a = Math.round(root.alpha * 100);
hsvString = `${h}, ${s}, ${v}, ${a}`;
} else {
hsvString = `${h}, ${s}, ${v}`
hsvString = `${h}, ${s}, ${v}`;
}
Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`])
ToastService.showInfo(`HSV ${hsvString} copied`)
Quickshell.execDetached(["sh", "-c", `echo -n "${hsvString}" | wl-copy`]);
ToastService.showInfo(`HSV ${hsvString} copied`);
}
}
}
@@ -652,9 +683,9 @@ DankModal {
textColor: Theme.background
anchors.right: parent.right
onClicked: {
SessionData.addRecentColor(root.currentColor)
root.colorSelected(root.currentColor)
root.hide()
SessionData.addRecentColor(root.currentColor);
root.colorSelected(root.currentColor);
root.hide();
}
}
}

View File

@@ -1,7 +1,6 @@
import QtQuick
import qs.Common
import qs.Modals.Common
import qs.Services
import qs.Widgets
DankModal {
@@ -15,8 +14,8 @@ DankModal {
shouldBeVisible: false
allowStacking: true
width: 420
height: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
modalWidth: 420
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 200
Timer {
id: countdownTimer
@@ -24,20 +23,20 @@ DankModal {
repeat: true
running: root.shouldBeVisible
onTriggered: {
countdown--
countdown--;
if (countdown <= 0) {
revert()
revert();
}
}
}
onOpened: {
countdown = 15
countdownTimer.start()
countdown = 15;
countdownTimer.start();
}
onClosed: {
countdownTimer.stop()
countdownTimer.stop();
}
onBackgroundClicked: revert
@@ -51,13 +50,13 @@ DankModal {
implicitHeight: mainColumn.implicitHeight
Keys.onEscapePressed: event => {
revert()
event.accepted = true
revert();
event.accepted = true;
}
Keys.onReturnPressed: event => {
confirm()
event.accepted = true
confirm();
event.accepted = true;
}
Column {
@@ -235,12 +234,12 @@ DankModal {
}
function confirm() {
displaysTab.confirmChanges()
close()
displaysTab.confirmChanges();
close();
}
function revert() {
displaysTab.revertChanges()
close()
displaysTab.revertChanges();
close();
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -10,34 +9,36 @@ DankModal {
layerNamespace: "dms:network-info"
keepPopoutsOpen: true
property bool networkInfoModalVisible: false
property string networkSSID: ""
property var networkData: null
function showNetworkInfo(ssid, data) {
networkSSID = ssid
networkData = data
networkInfoModalVisible = true
open()
NetworkService.fetchNetworkInfo(ssid)
networkSSID = ssid;
networkData = data;
networkInfoModalVisible = true;
open();
NetworkService.fetchNetworkInfo(ssid);
}
function hideDialog() {
networkInfoModalVisible = false
close()
networkSSID = ""
networkData = null
networkInfoModalVisible = false;
close();
networkSSID = "";
networkData = null;
}
visible: networkInfoModalVisible
width: 600
height: 500
modalWidth: 600
modalHeight: 500
enableShadow: true
onBackgroundClicked: hideDialog()
onVisibleChanged: {
if (!visible) {
networkSSID = ""
networkData = null
networkSSID = "";
networkData = null;
}
}
@@ -71,7 +72,6 @@ DankModal {
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
@@ -80,7 +80,6 @@ DankModal {
iconColor: Theme.surfaceText
onClicked: root.hideDialog()
}
}
Rectangle {
@@ -109,7 +108,6 @@ DankModal {
wrapMode: Text.WordWrap
}
}
}
Item {
@@ -148,17 +146,10 @@ DankModal {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -1,5 +1,4 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -10,34 +9,36 @@ DankModal {
layerNamespace: "dms:network-info-wired"
keepPopoutsOpen: true
property bool networkWiredInfoModalVisible: false
property string networkID: ""
property var networkData: null
function showNetworkInfo(id, data) {
networkID = id
networkData = data
networkWiredInfoModalVisible = true
open()
NetworkService.fetchWiredNetworkInfo(data.uuid)
networkID = id;
networkData = data;
networkWiredInfoModalVisible = true;
open();
NetworkService.fetchWiredNetworkInfo(data.uuid);
}
function hideDialog() {
networkWiredInfoModalVisible = false
close()
networkID = ""
networkData = null
networkWiredInfoModalVisible = false;
close();
networkID = "";
networkData = null;
}
visible: networkWiredInfoModalVisible
width: 600
height: 500
modalWidth: 600
modalHeight: 500
enableShadow: true
onBackgroundClicked: hideDialog()
onVisibleChanged: {
if (!visible) {
networkID = ""
networkData = null
networkID = "";
networkData = null;
}
}
@@ -71,7 +72,6 @@ DankModal {
width: parent.width
elide: Text.ElideRight
}
}
DankActionButton {
@@ -80,7 +80,6 @@ DankModal {
iconColor: Theme.surfaceText
onClicked: root.hideDialog()
}
}
Rectangle {
@@ -109,7 +108,6 @@ DankModal {
wrapMode: Text.WordWrap
}
}
}
Item {
@@ -148,17 +146,10 @@ DankModal {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}

View File

@@ -1,72 +1,77 @@
import QtQuick
import Quickshell.Hyprland
import Quickshell.Io
import qs.Common
import qs.Modals.Common
import qs.Modules.Notifications.Center
import qs.Services
import qs.Widgets
DankModal {
id: notificationModal
layerNamespace: "dms:notification-center-modal"
HyprlandFocusGrab {
windows: [notificationModal]
active: CompositorService.isHyprland && notificationModal.shouldHaveFocus
}
property bool notificationModalOpen: false
property var notificationListRef: null
function show() {
notificationModalOpen = true
NotificationService.onOverlayOpen()
open()
modalKeyboardController.reset()
notificationModalOpen = true;
NotificationService.onOverlayOpen();
open();
modalKeyboardController.reset();
if (modalKeyboardController && notificationListRef) {
modalKeyboardController.listView = notificationListRef
modalKeyboardController.rebuildFlatNavigation()
modalKeyboardController.listView = notificationListRef;
modalKeyboardController.rebuildFlatNavigation();
Qt.callLater(() => {
modalKeyboardController.keyboardNavigationActive = true
modalKeyboardController.selectedFlatIndex = 0
modalKeyboardController.updateSelectedIdFromIndex()
modalKeyboardController.keyboardNavigationActive = true;
modalKeyboardController.selectedFlatIndex = 0;
modalKeyboardController.updateSelectedIdFromIndex();
if (notificationListRef) {
notificationListRef.keyboardActive = true
notificationListRef.currentIndex = 0
notificationListRef.keyboardActive = true;
notificationListRef.currentIndex = 0;
}
modalKeyboardController.selectionVersion++
modalKeyboardController.ensureVisible()
})
modalKeyboardController.selectionVersion++;
modalKeyboardController.ensureVisible();
});
}
}
function hide() {
notificationModalOpen = false
NotificationService.onOverlayClose()
close()
modalKeyboardController.reset()
notificationModalOpen = false;
NotificationService.onOverlayClose();
close();
modalKeyboardController.reset();
}
function toggle() {
if (shouldBeVisible) {
hide()
hide();
} else {
show()
show();
}
}
width: 500
height: 700
modalWidth: 500
modalHeight: 700
visible: false
onBackgroundClicked: hide()
onOpened: () => {
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
onShouldBeVisibleChanged: (shouldBeVisible) => {
onShouldBeVisibleChanged: shouldBeVisible => {
if (!shouldBeVisible) {
notificationModalOpen = false
modalKeyboardController.reset()
NotificationService.onOverlayClose()
notificationModalOpen = false;
modalKeyboardController.reset();
NotificationService.onOverlayClose();
}
}
modalFocusScope.Keys.onPressed: (event) => modalKeyboardController.handleKey(event)
modalFocusScope.Keys.onPressed: event => modalKeyboardController.handleKey(event)
NotificationKeyboardController {
id: modalKeyboardController
@@ -125,14 +130,13 @@ DankModal {
height: parent.height - y
keyboardController: modalKeyboardController
Component.onCompleted: {
notificationModal.notificationListRef = notificationList
notificationModal.notificationListRef = notificationList;
if (modalKeyboardController) {
modalKeyboardController.listView = notificationList
modalKeyboardController.rebuildFlatNavigation()
modalKeyboardController.listView = notificationList;
modalKeyboardController.rebuildFlatNavigation();
}
}
}
}
NotificationKeyboardHints {
@@ -144,9 +148,6 @@ DankModal {
anchors.margins: Theme.spacingL
showHints: modalKeyboardController.showKeyboardHints
}
}
}
}

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -9,33 +10,38 @@ DankModal {
layerNamespace: "dms:polkit"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string passwordInput: ""
property var currentFlow: PolkitService.agent?.flow
property bool isLoading: false
property real minHeight: 240
function show() {
passwordInput = ""
isLoading = false
open()
passwordInput = "";
isLoading = false;
open();
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
contentLoader.item.passwordField.forceActiveFocus();
}
})
});
}
shouldBeVisible: false
width: 420
height: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
modalWidth: 420
modalHeight: Math.max(minHeight, contentLoader.item ? contentLoader.item.implicitHeight + Theme.spacingM * 2 : 240)
Connections {
target: contentLoader.item
function onImplicitHeightChanged() {
if (shouldBeVisible && contentLoader.item) {
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2
const newHeight = contentLoader.item.implicitHeight + Theme.spacingM * 2;
if (newHeight > minHeight) {
minHeight = newHeight
minHeight = newHeight;
}
}
}
@@ -44,19 +50,19 @@ DankModal {
onOpened: {
Qt.callLater(() => {
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
contentLoader.item.passwordField.forceActiveFocus();
}
})
});
}
onClosed: {
passwordInput = ""
isLoading = false
onDialogClosed: {
passwordInput = "";
isLoading = false;
}
onBackgroundClicked: () => {
if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest()
currentFlow.cancelAuthenticationRequest();
}
}
@@ -65,12 +71,12 @@ DankModal {
enabled: PolkitService.polkitAvailable
function onAuthenticationRequestStarted() {
show()
show();
}
function onIsActiveChanged() {
if (!(PolkitService.agent?.isActive ?? false)) {
close()
close();
}
}
}
@@ -81,24 +87,24 @@ DankModal {
function onIsResponseRequiredChanged() {
if (currentFlow.isResponseRequired) {
isLoading = false
passwordInput = ""
isLoading = false;
passwordInput = "";
if (contentLoader.item && contentLoader.item.passwordField) {
contentLoader.item.passwordField.forceActiveFocus()
contentLoader.item.passwordField.forceActiveFocus();
}
}
}
function onAuthenticationSucceeded() {
close()
close();
}
function onAuthenticationFailed() {
isLoading = false
isLoading = false;
}
function onAuthenticationRequestCancelled() {
close()
close();
}
}
@@ -114,9 +120,9 @@ DankModal {
Keys.onEscapePressed: event => {
if (currentFlow && !isLoading) {
currentFlow.cancelAuthenticationRequest()
currentFlow.cancelAuthenticationRequest();
}
event.accepted = true
event.accepted = true;
}
Row {
@@ -171,7 +177,7 @@ DankModal {
opacity: enabled ? 1 : 0.5
onClicked: () => {
if (currentFlow) {
currentFlow.cancelAuthenticationRequest()
currentFlow.cancelAuthenticationRequest();
}
}
}
@@ -208,7 +214,7 @@ DankModal {
anchors.fill: parent
enabled: !isLoading
onClicked: () => {
passwordField.forceActiveFocus()
passwordField.forceActiveFocus();
}
}
@@ -224,13 +230,13 @@ DankModal {
backgroundColor: "transparent"
enabled: !isLoading
onTextEdited: () => {
passwordInput = text
passwordInput = text;
}
onAccepted: () => {
if (passwordInput.length > 0 && currentFlow && !isLoading) {
isLoading = true
currentFlow.submit(passwordInput)
passwordInput = ""
isLoading = true;
currentFlow.submit(passwordInput);
passwordInput = "";
}
}
}
@@ -303,7 +309,7 @@ DankModal {
enabled: parent.enabled
onClicked: () => {
if (currentFlow) {
currentFlow.cancelAuthenticationRequest()
currentFlow.cancelAuthenticationRequest();
}
}
}
@@ -336,9 +342,9 @@ DankModal {
enabled: parent.enabled
onClicked: () => {
if (currentFlow && !isLoading) {
isLoading = true
currentFlow.submit(passwordInput)
passwordInput = ""
isLoading = true;
currentFlow.submit(passwordInput);
passwordInput = "";
}
}
}

View File

@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -10,6 +11,11 @@ DankModal {
layerNamespace: "dms:power-menu"
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property int selectedIndex: 0
property int selectedRow: 0
property int selectedCol: 0
@@ -23,64 +29,64 @@ DankModal {
signal lockRequested
function openCentered() {
parentBounds = Qt.rect(0, 0, 0, 0)
parentScreen = null
backgroundOpacity = 0.5
open()
parentBounds = Qt.rect(0, 0, 0, 0);
parentScreen = null;
backgroundOpacity = 0.5;
open();
}
function openFromControlCenter(bounds, targetScreen) {
parentBounds = bounds
parentScreen = targetScreen
backgroundOpacity = 0
keepPopoutsOpen = true
open()
keepPopoutsOpen = false
parentBounds = bounds;
parentScreen = targetScreen;
backgroundOpacity = 0;
keepPopoutsOpen = true;
open();
keepPopoutsOpen = false;
}
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 => {
if (action === "hibernate" && !SessionService.hibernateSupported)
return false
return true
})
if (action === "hibernate" && !SessionService.hibernateSupported)
return false;
return true;
});
if (!SettingsData.powerMenuGridLayout) return
const count = visibleActions.length
if (!SettingsData.powerMenuGridLayout)
return;
const count = visibleActions.length;
if (count === 0) {
gridColumns = 1
gridRows = 1
return
gridColumns = 1;
gridRows = 1;
return;
}
if (count <= 3) {
gridColumns = 1
gridRows = count
return
gridColumns = 1;
gridRows = count;
return;
}
if (count === 4) {
gridColumns = 2
gridRows = 2
return
gridColumns = 2;
gridRows = 2;
return;
}
gridColumns = 3
gridRows = Math.ceil(count / 3)
gridColumns = 3;
gridRows = Math.ceil(count / 3);
}
function getDefaultActionIndex() {
const defaultAction = SettingsData.powerMenuDefaultAction || "logout"
const index = visibleActions.indexOf(defaultAction)
return index >= 0 ? index : 0
const defaultAction = SettingsData.powerMenuDefaultAction || "logout";
const index = visibleActions.indexOf(defaultAction);
return index >= 0 ? index : 0;
}
function getActionAtIndex(index) {
if (index < 0 || index >= visibleActions.length)
return ""
return visibleActions[index]
return "";
return visibleActions[index];
}
function getActionData(action) {
@@ -90,64 +96,64 @@ DankModal {
"icon": "restart_alt",
"label": I18n.tr("Reboot"),
"key": "R"
}
};
case "logout":
return {
"icon": "logout",
"label": I18n.tr("Log Out"),
"key": "X"
}
};
case "poweroff":
return {
"icon": "power_settings_new",
"label": I18n.tr("Power Off"),
"key": "P"
}
};
case "lock":
return {
"icon": "lock",
"label": I18n.tr("Lock"),
"key": "L"
}
};
case "suspend":
return {
"icon": "bedtime",
"label": I18n.tr("Suspend"),
"key": "S"
}
};
case "hibernate":
return {
"icon": "ac_unit",
"label": I18n.tr("Hibernate"),
"key": "H"
}
};
case "restart":
return {
"icon": "refresh",
"label": I18n.tr("Restart DMS"),
"key": "D"
}
};
default:
return {
"icon": "help",
"label": action,
"key": "?"
}
};
}
}
function selectOption(action) {
if (action === "lock") {
close()
lockRequested()
return
close();
lockRequested();
return;
}
if (action === "restart") {
close()
Quickshell.execDetached(["dms", "restart"])
return
close();
Quickshell.execDetached(["dms", "restart"]);
return;
}
close()
close();
const actions = {
"logout": {
"title": I18n.tr("Log Out"),
@@ -169,227 +175,223 @@ DankModal {
"title": I18n.tr("Power Off"),
"message": I18n.tr("Are you sure you want to power off the system?")
}
}
const selected = actions[action]
};
const selected = actions[action];
if (selected) {
root.powerActionRequested(action, selected.title, selected.message)
root.powerActionRequested(action, selected.title, selected.message);
}
}
shouldBeVisible: false
width: SettingsData.powerMenuGridLayout
? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2)
: 400
height: contentLoader.item ? contentLoader.item.implicitHeight : 300
modalWidth: SettingsData.powerMenuGridLayout ? Math.min(550, gridColumns * 180 + Theme.spacingS * (gridColumns - 1) + Theme.spacingL * 2) : 400
modalHeight: contentLoader.item ? contentLoader.item.implicitHeight : 300
enableShadow: true
screen: parentScreen
targetScreen: parentScreen
positioning: parentBounds.width > 0 ? "custom" : "center"
customPosition: {
if (parentBounds.width > 0) {
const effectiveBarThickness = Math.max(26 + SettingsData.dankBarInnerPadding * 0.6 + SettingsData.dankBarInnerPadding + 4, Theme.barHeight - 4 - (8 - SettingsData.dankBarInnerPadding))
const barExclusionZone = effectiveBarThickness + SettingsData.dankBarSpacing + SettingsData.dankBarBottomGap
const screenW = parentScreen?.width ?? 1920
const screenH = parentScreen?.height ?? 1080
const margin = Theme.spacingL
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.barConfigs[0]?.spacing ?? 4) + (SettingsData.barConfigs[0]?.bottomGap ?? 0);
const screenW = parentScreen?.width ?? 1920;
const screenH = parentScreen?.height ?? 1080;
const margin = Theme.spacingL;
let targetX = parentBounds.x + (parentBounds.width - width) / 2
let targetY = parentBounds.y + (parentBounds.height - height) / 2
let targetX = parentBounds.x + (parentBounds.width - modalWidth) / 2;
let targetY = parentBounds.y + (parentBounds.height - modalHeight) / 2;
const minY = SettingsData.dankBarPosition === SettingsData.Position.Top ? barExclusionZone + margin : margin
const maxY = SettingsData.dankBarPosition === SettingsData.Position.Bottom ? screenH - height - barExclusionZone - margin : screenH - height - margin
const minY = (SettingsData.barConfigs[0]?.position ?? SettingsData.Position.Top) === SettingsData.Position.Top ? barExclusionZone + margin : 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()
onOpened: () => {
updateVisibleActions()
const defaultIndex = getDefaultActionIndex()
if (SettingsData.powerMenuGridLayout) {
selectedRow = Math.floor(defaultIndex / gridColumns)
selectedCol = defaultIndex % gridColumns
selectedIndex = defaultIndex
} else {
selectedIndex = defaultIndex
}
Qt.callLater(() => modalFocusScope.forceActiveFocus())
}
updateVisibleActions();
const defaultIndex = getDefaultActionIndex();
if (SettingsData.powerMenuGridLayout) {
selectedRow = Math.floor(defaultIndex / gridColumns);
selectedCol = defaultIndex % gridColumns;
selectedIndex = defaultIndex;
} else {
selectedIndex = defaultIndex;
}
Qt.callLater(() => modalFocusScope.forceActiveFocus());
}
Component.onCompleted: updateVisibleActions()
modalFocusScope.Keys.onPressed: event => {
if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event)
} else {
handleListNavigation(event)
}
}
if (SettingsData.powerMenuGridLayout) {
handleGridNavigation(event);
} else {
handleListNavigation(event);
}
}
function handleListNavigation(event) {
switch (event.key) {
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex))
event.accepted = true
break
case Qt.Key_N:
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex));
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break
case Qt.Key_P:
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff")
event.accepted = true
selectOption("poweroff");
event.accepted = true;
} else {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break
case Qt.Key_J:
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex + 1) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex + 1) % visibleActions.length;
event.accepted = true;
}
break
case Qt.Key_K:
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length
event.accepted = true
selectedIndex = (selectedIndex - 1 + visibleActions.length) % visibleActions.length;
event.accepted = true;
}
break
case Qt.Key_R:
selectOption("reboot")
event.accepted = true
break
case Qt.Key_X:
selectOption("logout")
event.accepted = true
break
case Qt.Key_L:
selectOption("lock")
event.accepted = true
break
case Qt.Key_S:
selectOption("suspend")
event.accepted = true
break
case Qt.Key_H:
selectOption("hibernate")
event.accepted = true
break
case Qt.Key_D:
selectOption("restart")
event.accepted = true
break
break;
case Qt.Key_R:
selectOption("reboot");
event.accepted = true;
break;
case Qt.Key_X:
selectOption("logout");
event.accepted = true;
break;
case Qt.Key_L:
selectOption("lock");
event.accepted = true;
break;
case Qt.Key_S:
selectOption("suspend");
event.accepted = true;
break;
case Qt.Key_H:
selectOption("hibernate");
event.accepted = true;
break;
case Qt.Key_D:
selectOption("restart");
event.accepted = true;
break;
}
}
function handleGridNavigation(event) {
switch (event.key) {
case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
case Qt.Key_Down:
case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
break
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex))
event.accepted = true
break
case Qt.Key_N:
case Qt.Key_Left:
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Right:
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Up:
case Qt.Key_Backtab:
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Down:
case Qt.Key_Tab:
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
break;
case Qt.Key_Return:
case Qt.Key_Enter:
selectOption(getActionAtIndex(selectedIndex));
event.accepted = true;
break;
case Qt.Key_N:
if (event.modifiers & Qt.ControlModifier) {
selectedCol = (selectedCol + 1) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedCol = (selectedCol + 1) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
case Qt.Key_P:
break;
case Qt.Key_P:
if (!(event.modifiers & Qt.ControlModifier)) {
selectOption("poweroff")
event.accepted = true
selectOption("poweroff");
event.accepted = true;
} else {
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedCol = (selectedCol - 1 + gridColumns) % gridColumns;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
case Qt.Key_J:
break;
case Qt.Key_J:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow + 1) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedRow = (selectedRow + 1) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
case Qt.Key_K:
break;
case Qt.Key_K:
if (event.modifiers & Qt.ControlModifier) {
selectedRow = (selectedRow - 1 + gridRows) % gridRows
selectedIndex = selectedRow * gridColumns + selectedCol
event.accepted = true
selectedRow = (selectedRow - 1 + gridRows) % gridRows;
selectedIndex = selectedRow * gridColumns + selectedCol;
event.accepted = true;
}
break
case Qt.Key_R:
selectOption("reboot")
event.accepted = true
break
case Qt.Key_X:
selectOption("logout")
event.accepted = true
break
case Qt.Key_L:
selectOption("lock")
event.accepted = true
break
case Qt.Key_S:
selectOption("suspend")
event.accepted = true
break
case Qt.Key_H:
selectOption("hibernate")
event.accepted = true
break
case Qt.Key_D:
selectOption("restart")
event.accepted = true
break
break;
case Qt.Key_R:
selectOption("reboot");
event.accepted = true;
break;
case Qt.Key_X:
selectOption("logout");
event.accepted = true;
break;
case Qt.Key_L:
selectOption("lock");
event.accepted = true;
break;
case Qt.Key_S:
selectOption("suspend");
event.accepted = true;
break;
case Qt.Key_H:
selectOption("hibernate");
event.accepted = true;
break;
case Qt.Key_D:
selectOption("restart");
event.accepted = true;
break;
}
}
content: Component {
Item {
anchors.fill: parent
implicitHeight: SettingsData.powerMenuGridLayout
? buttonGrid.implicitHeight + Theme.spacingL * 2
: buttonColumn.implicitHeight + Theme.spacingL * 2
implicitHeight: SettingsData.powerMenuGridLayout ? buttonGrid.implicitHeight + Theme.spacingL * 2 : buttonColumn.implicitHeight + Theme.spacingL * 2
Grid {
id: buttonGrid
@@ -410,15 +412,15 @@ DankModal {
readonly property bool isSelected: root.selectedIndex === index
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
radius: Theme.cornerRadius
color: {
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)
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.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0
@@ -432,9 +434,9 @@ DankModal {
size: Theme.iconSize + 8
color: {
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
}
@@ -444,9 +446,9 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium
color: {
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
anchors.horizontalCenter: parent.horizontalCenter
@@ -475,9 +477,9 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedRow = Math.floor(index / root.gridColumns)
root.selectedCol = index % root.gridColumns
root.selectOption(modelData)
root.selectedRow = Math.floor(index / root.gridColumns);
root.selectedCol = index % root.gridColumns;
root.selectOption(modelData);
}
}
}
@@ -512,10 +514,10 @@ DankModal {
radius: Theme.cornerRadius
color: {
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)
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.primary.r, Theme.primary.g, Theme.primary.b, 0.08);
return Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.08);
}
border.color: isSelected ? Theme.primary : "transparent"
border.width: isSelected ? 2 : 0
@@ -535,9 +537,9 @@ DankModal {
size: Theme.iconSize + 4
color: {
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
}
@@ -547,9 +549,9 @@ DankModal {
font.pixelSize: Theme.fontSizeMedium
color: {
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
anchors.verticalCenter: parent.verticalCenter
@@ -582,8 +584,8 @@ DankModal {
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.selectedIndex = index
root.selectOption(modelData)
root.selectedIndex = index;
root.selectOption(modelData);
}
}
}

View File

@@ -1,56 +1,62 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Modules.ProcessList
import qs.Services
import qs.Widgets
DankModal {
FloatingWindow {
id: processListModal
layerNamespace: "dms:process-list-modal"
property int currentTab: 0
property var tabNames: ["Processes", "Performance", "System"]
property bool shouldHaveFocus: visible
property alias shouldBeVisible: processListModal.visible
signal closingModal
function show() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
return;
}
open();
visible = true;
UserInfoService.getUptime();
}
function hide() {
close();
visible = false;
if (processContextMenu.visible) {
processContextMenu.close();
}
}
function toggle() {
if (!DgopService.dgopAvailable) {
console.warn("ProcessListModal: dgop is not available");
return ;
}
if (shouldBeVisible) {
hide();
} else {
show();
return;
}
visible = !visible;
}
width: 900
height: 680
objectName: "processListModal"
title: I18n.tr("System Monitor", "sysmon window title")
implicitWidth: 900
implicitHeight: 680
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
enableShadow: true
onBackgroundClicked: () => {
return hide();
onVisibleChanged: {
if (!visible) {
closingModal();
} else {
Qt.callLater(() => {
if (contentFocusScope) {
contentFocusScope.forceActiveFocus();
}
});
}
}
Component {
@@ -59,300 +65,277 @@ DankModal {
ProcessesTab {
contextMenu: processContextMenu
}
}
Component {
id: performanceTabComponent
PerformanceTab {
}
PerformanceTab {}
}
Component {
id: systemTabComponent
SystemTab {
}
SystemTab {}
}
ProcessContextMenu {
id: processContextMenu
}
content: Component {
Item {
anchors.fill: parent
focus: true
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Escape) {
processListModal.hide();
event.accepted = true;
} else if (event.key === Qt.Key_1) {
currentTab = 0;
event.accepted = true;
} else if (event.key === Qt.Key_2) {
currentTab = 1;
event.accepted = true;
} else if (event.key === Qt.Key_3) {
currentTab = 2;
event.accepted = true;
}
FocusScope {
id: contentFocusScope
anchors.fill: parent
focus: true
Keys.onPressed: event => {
if (event.key === Qt.Key_Escape) {
hide();
event.accepted = true;
return;
}
// Show error message when dgop is not available
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
}
}
switch (event.key) {
case Qt.Key_1:
currentTab = 0;
event.accepted = true;
return;
case Qt.Key_2:
currentTab = 1;
event.accepted = true;
return;
case Qt.Key_3:
currentTab = 2;
event.accepted = true;
return;
}
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
}
}
}
}
}
}
}

View File

@@ -22,7 +22,7 @@ Item {
width: parent.width
height: lockScreenSection.implicitHeight + Theme.spacingL * 2
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.width: 0
@@ -106,7 +106,7 @@ Item {
width: parent.width
height: timeoutSection.implicitHeight + Theme.spacingL * 2
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.width: 0
@@ -329,7 +329,7 @@ Item {
width: parent.width
height: powerMenuCustomSection.implicitHeight + Theme.spacingL * 2
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.width: 0
@@ -516,7 +516,7 @@ Item {
width: parent.width
height: powerCommandConfirmSection.implicitHeight + Theme.spacingL * 2
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.width: 0
@@ -560,7 +560,7 @@ Item {
width: parent.width
height: powerCommandCustomization.implicitHeight + Theme.spacingL * 2
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.width: 0

View File

@@ -1,142 +1,87 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Io
import Quickshell
import qs.Common
import qs.Modals.Common
import qs.Modals.FileBrowser
import qs.Modules.Settings
import qs.Services
import qs.Widgets
DankModal {
FloatingWindow {
id: settingsModal
layerNamespace: "dms:settings"
property Component settingsContent
property alias profileBrowser: profileBrowser
property alias wallpaperBrowser: wallpaperBrowser
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() {
open();
visible = true;
}
function hide() {
close();
visible = false;
}
function toggle() {
if (shouldBeVisible) {
hide();
} else {
show();
}
visible = !visible;
}
function toggleMenu() {
enableAnimations = true;
menuVisible = !menuVisible;
}
objectName: "settingsModal"
width: Math.min(800, screenWidth * 0.9)
height: Math.min(800, screenHeight * 0.85)
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
title: I18n.tr("Settings", "settings window title")
implicitWidth: 800
implicitHeight: 800
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
visible: false
onBackgroundClicked: () => {
return hide();
}
content: settingsContent
onOpened: () => {
onIsCompactModeChanged: {
enableAnimations = false;
if (!isCompactMode) {
menuVisible = true;
}
Qt.callLater(() => {
modalFocusScope.forceActiveFocus()
if (contentLoader.item) {
contentLoader.item.forceActiveFocus()
}
})
enableAnimations = true;
});
}
onVisibleChanged: {
if (visible && shouldBeVisible) {
if (!visible) {
closingModal();
} else {
Qt.callLater(() => {
modalFocusScope.forceActiveFocus()
if (contentLoader.item) {
contentLoader.item.forceActiveFocus()
if (contentFocusScope) {
contentFocusScope.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 {
id: profileBrowser
allowStacking: true
parentModal: settingsModal
browserTitle: "Select Profile Image"
browserTitle: I18n.tr("Select Profile Image", "profile image file browser title")
browserIcon: "person"
browserType: "profile"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
onFileSelected: path => {
PortalService.setProfileImage(path);
close();
}
onDialogClosed: () => {
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
parentModal: settingsModal
browserTitle: "Select Wallpaper"
browserTitle: I18n.tr("Select Wallpaper", "wallpaper file browser title")
browserIcon: "wallpaper"
browserType: "wallpaper"
showHiddenFiles: true
fileExtensions: ["*.jpg", "*.jpeg", "*.png", "*.bmp", "*.gif", "*.webp"]
onFileSelected: (path) => {
onFileSelected: path => {
SessionData.setWallpaper(path);
close();
}
onDialogClosed: () => {
allowStacking = true;
if (settingsModal.shouldBeVisible) {
Qt.callLater(() => {
settingsModal.modalFocusScope.forceActiveFocus()
if (settingsModal.contentLoader.item) {
settingsModal.contentLoader.item.forceActiveFocus()
}
})
}
}
}
settingsContent: Component {
Item {
id: rootScope
anchors.fill: parent
FocusScope {
id: contentFocusScope
Keys.onEscapePressed: event => {
settingsModal.hide()
event.accepted = true
anchors.fill: parent
focus: 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 {
anchors.fill: parent
anchors.leftMargin: Theme.spacingL
anchors.rightMargin: Theme.spacingL
anchors.topMargin: Theme.spacingM
anchors.bottomMargin: Theme.spacingL
spacing: 0
Column {
anchors.fill: parent
spacing: 0
Item {
width: parent.width
height: 35
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();
}
}
Item {
width: parent.width
height: 48
z: 10
Rectangle {
anchors.fill: parent
color: Theme.surfaceContainer
opacity: 0.5
}
Row {
width: parent.width
height: parent.height - 35
spacing: 0
anchors.left: parent.left
anchors.leftMargin: Theme.spacingL
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingM
SettingsSidebar {
id: sidebar
parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex
onCurrentIndexChanged: {
settingsModal.currentTabIndex = currentIndex
DankActionButton {
visible: settingsModal.isCompactMode
circular: false
iconName: "menu"
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
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 {
id: content
width: parent.width - sidebar.width
width: Math.min(550, parent.width - Theme.spacingL * 2)
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
parentModal: settingsModal
currentIndex: settingsModal.currentTabIndex
}
Behavior on x {
enabled: settingsModal.enableAnimations
NumberAnimation {
duration: Theme.mediumDuration
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
}
}
}
}
}
}
}

View File

@@ -10,47 +10,59 @@ Rectangle {
property int currentIndex: 0
property var parentModal: null
readonly property var sidebarItems: [{
"text": I18n.tr("Personalization"),
"icon": "person"
}, {
"text": I18n.tr("Time & Weather"),
"icon": "schedule"
}, {
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
}, {
"text": I18n.tr("Widgets"),
"icon": "widgets"
}, {
"text": I18n.tr("Dock"),
"icon": "dock_to_bottom"
}, {
"text": I18n.tr("Displays"),
"icon": "monitor"
}, {
"text": I18n.tr("Launcher"),
"icon": "apps"
}, {
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
}, {
"text": I18n.tr("Power & Security"),
"icon": "power"
}, {
"text": I18n.tr("Plugins"),
"icon": "extension"
}, {
"text": I18n.tr("About"),
"icon": "info"
}]
readonly property var sidebarItems: [
{
"text": I18n.tr("Personalization"),
"icon": "person"
},
{
"text": I18n.tr("Time & Weather"),
"icon": "schedule"
},
{
"text": I18n.tr("Dank Bar"),
"icon": "toolbar"
},
{
"text": I18n.tr("Widgets"),
"icon": "widgets"
},
{
"text": I18n.tr("Dock"),
"icon": "dock_to_bottom"
},
{
"text": I18n.tr("Displays"),
"icon": "monitor"
},
{
"text": I18n.tr("Launcher"),
"icon": "apps"
},
{
"text": I18n.tr("Theme & Colors"),
"icon": "palette"
},
{
"text": I18n.tr("Power & Security"),
"icon": "power"
},
{
"text": I18n.tr("Plugins"),
"icon": "extension"
},
{
"text": I18n.tr("About"),
"icon": "info"
}
]
function navigateNext() {
currentIndex = (currentIndex + 1) % sidebarItems.length
currentIndex = (currentIndex + 1) % sidebarItems.length;
}
function navigatePrevious() {
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length
currentIndex = (currentIndex - 1 + sidebarItems.length) % sidebarItems.length;
}
width: 270
@@ -61,31 +73,32 @@ Rectangle {
DankFlickable {
anchors.fill: parent
clip: true
contentHeight: sidebarColumn.implicitHeight
contentHeight: sidebarColumn.height
Column {
id: sidebarColumn
anchors.fill: parent
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.bottomMargin: Theme.spacingS
anchors.topMargin: Theme.spacingM + 2
width: parent.width
leftPadding: Theme.spacingS
rightPadding: Theme.spacingS
bottomPadding: Theme.spacingL
topPadding: Theme.spacingM + 2
spacing: Theme.spacingXS
ProfileSection {
width: parent.width - parent.leftPadding - parent.rightPadding
parentModal: sidebarContainer.parentModal
}
Rectangle {
width: parent.width - Theme.spacingS * 2
width: parent.width - parent.leftPadding - parent.rightPadding
height: 1
color: Theme.outline
opacity: 0.2
}
Item {
width: parent.width
width: parent.width - parent.leftPadding - parent.rightPadding
height: Theme.spacingL
}
@@ -100,7 +113,7 @@ Rectangle {
property bool isActive: sidebarContainer.currentIndex === index
width: parent.width
width: parent.width - parent.leftPadding - parent.rightPadding
height: 44
radius: Theme.cornerRadius
color: isActive ? Theme.primary : tabMouseArea.containsMouse ? Theme.surfaceHover : "transparent"
@@ -125,7 +138,6 @@ Rectangle {
font.weight: parent.parent.isActive ? Font.Medium : Font.Normal
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
@@ -144,15 +156,9 @@ Rectangle {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}

View File

@@ -158,6 +158,10 @@ Item {
onAppLaunched: () => {
if (parentModal)
parentModal.hide()
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview()
}
}
onViewModeSelected: mode => {
SettingsData.set("spotlightModalViewMode", mode)
@@ -170,6 +174,10 @@ Item {
onFileOpened: () => {
if (parentModal)
parentModal.hide()
if (SettingsData.spotlightCloseNiriOverview && NiriService.inOverview) {
NiriService.toggleOverview()
}
}
}

View File

@@ -1,88 +1,92 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import Quickshell.Hyprland
import Quickshell.Io
import Quickshell.Widgets
import qs.Common
import qs.Modals.Common
import qs.Modules.AppDrawer
import qs.Services
import qs.Widgets
DankModal {
id: spotlightModal
layerNamespace: "dms:spotlight"
HyprlandFocusGrab {
windows: [spotlightModal]
active: CompositorService.isHyprland && spotlightModal.shouldHaveFocus
}
property bool spotlightOpen: false
property alias spotlightContent: spotlightContentInstance
property bool openedFromOverview: false
function show() {
spotlightOpen = true
open()
openedFromOverview = false;
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
}
function showWithQuery(query) {
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = query
spotlightContent.appLauncher.searchQuery = query;
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = query
spotlightContent.searchField.text = query;
}
}
spotlightOpen = true
open()
spotlightOpen = true;
open();
Qt.callLater(() => {
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
if (spotlightContent && spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
}
function hide() {
spotlightOpen = false
close()
openedFromOverview = false;
spotlightOpen = false;
close();
}
onDialogClosed: {
if (spotlightContent) {
if (spotlightContent.appLauncher) {
spotlightContent.appLauncher.searchQuery = ""
spotlightContent.appLauncher.selectedIndex = 0
spotlightContent.appLauncher.setCategory(I18n.tr("All"))
spotlightContent.appLauncher.searchQuery = "";
spotlightContent.appLauncher.selectedIndex = 0;
spotlightContent.appLauncher.setCategory(I18n.tr("All"));
}
if (spotlightContent.fileSearchController) {
spotlightContent.fileSearchController.reset()
spotlightContent.fileSearchController.reset();
}
if (spotlightContent.resetScroll) {
spotlightContent.resetScroll()
spotlightContent.resetScroll();
}
if (spotlightContent.searchField) {
spotlightContent.searchField.text = ""
spotlightContent.searchField.text = "";
}
}
}
function toggle() {
if (spotlightOpen) {
hide()
hide();
} else {
show()
show();
}
}
shouldBeVisible: spotlightOpen
width: 500
height: 600
modalWidth: 500
modalHeight: 600
backgroundColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
cornerRadius: Theme.cornerRadius
borderColor: Theme.outlineMedium
@@ -90,25 +94,25 @@ DankModal {
enableShadow: true
keepContentLoaded: true
onVisibleChanged: () => {
if (visible && !spotlightOpen) {
show()
}
if (visible && spotlightContent) {
Qt.callLater(() => {
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus()
}
})
}
}
if (visible && !spotlightOpen) {
show();
}
if (visible && spotlightContent) {
Qt.callLater(() => {
if (spotlightContent.searchField) {
spotlightContent.searchField.forceActiveFocus();
}
});
}
}
onBackgroundClicked: () => {
return hide()
}
return hide();
}
Connections {
function onCloseAllModalsExcept(excludedModal) {
if (excludedModal !== spotlightModal && !allowStacking && spotlightOpen) {
spotlightOpen = false
spotlightOpen = false;
}
}
@@ -117,32 +121,32 @@ DankModal {
IpcHandler {
function open(): string {
spotlightModal.show()
return "SPOTLIGHT_OPEN_SUCCESS"
spotlightModal.show();
return "SPOTLIGHT_OPEN_SUCCESS";
}
function close(): string {
spotlightModal.hide()
return "SPOTLIGHT_CLOSE_SUCCESS"
spotlightModal.hide();
return "SPOTLIGHT_CLOSE_SUCCESS";
}
function toggle(): string {
spotlightModal.toggle()
return "SPOTLIGHT_TOGGLE_SUCCESS"
spotlightModal.toggle();
return "SPOTLIGHT_TOGGLE_SUCCESS";
}
function openQuery(query: string): string {
spotlightModal.showWithQuery(query)
return "SPOTLIGHT_OPEN_QUERY_SUCCESS"
spotlightModal.showWithQuery(query);
return "SPOTLIGHT_OPEN_QUERY_SUCCESS";
}
function toggleQuery(query: string): string {
if (spotlightModal.spotlightOpen) {
spotlightModal.hide()
spotlightModal.hide();
} else {
spotlightModal.showWithQuery(query)
spotlightModal.showWithQuery(query);
}
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS"
return "SPOTLIGHT_TOGGLE_QUERY_SUCCESS";
}
target: "spotlight"

View File

@@ -1,4 +1,5 @@
import QtQuick
import Quickshell.Hyprland
import qs.Common
import qs.Modals.Common
import qs.Services
@@ -8,6 +9,12 @@ DankModal {
id: root
layerNamespace: "dms:wifi-password"
keepPopoutsOpen: true
HyprlandFocusGrab {
windows: [root]
active: CompositorService.isHyprland && root.shouldHaveFocus
}
property string wifiPasswordSSID: ""
property string wifiPasswordInput: ""
@@ -29,118 +36,124 @@ DankModal {
property string connectionType: ""
function show(ssid) {
wifiPasswordSSID = ssid
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
isPromptMode = false
promptToken = ""
promptReason = ""
promptFields = []
promptSetting = ""
isVpnPrompt = false
connectionName = ""
vpnServiceType = ""
connectionType = ""
wifiPasswordSSID = ssid;
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
isPromptMode = false;
promptToken = "";
promptReason = "";
promptFields = [];
promptSetting = "";
isVpnPrompt = false;
connectionName = "";
vpnServiceType = "";
connectionType = "";
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid)
requiresEnterprise = network?.enterprise || false
const network = NetworkService.wifiNetworks.find(n => n.ssid === ssid);
requiresEnterprise = network?.enterprise || false;
open()
open();
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus();
}
}
});
}
function showFromPrompt(token, ssid, setting, fields, hints, reason, connType, connName, vpnService) {
isPromptMode = true
promptToken = token
promptReason = reason
promptFields = fields || []
promptSetting = setting || "802-11-wireless-security"
connectionType = connType || "802-11-wireless"
connectionName = connName || ssid || ""
vpnServiceType = vpnService || ""
isPromptMode = true;
promptToken = token;
promptReason = reason;
promptFields = fields || [];
promptSetting = setting || "802-11-wireless-security";
connectionType = connType || "802-11-wireless";
connectionName = connName || ssid || "";
vpnServiceType = vpnService || "";
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard")
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid
isVpnPrompt = (connectionType === "vpn" || connectionType === "wireguard");
wifiPasswordSSID = isVpnPrompt ? connectionName : ssid;
requiresEnterprise = setting === "802-1x"
requiresEnterprise = setting === "802-1x";
if (reason === "wrong-password") {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiPasswordInput = "";
wifiUsernameInput = "";
} else {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
}
open()
open();
Qt.callLater(() => {
if (contentLoader.item) {
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.text = ""
contentLoader.item.passwordInput.forceActiveFocus()
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
if (contentLoader.item) {
if (reason === "wrong-password" && contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.text = "";
contentLoader.item.passwordInput.forceActiveFocus();
} else if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus();
}
}
});
}
shouldBeVisible: false
width: 420
height: requiresEnterprise ? 430 : 230
modalWidth: 420
modalHeight: {
if (requiresEnterprise)
return 430;
if (isVpnPrompt)
return 260;
return 230;
}
onShouldBeVisibleChanged: () => {
if (!shouldBeVisible) {
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
}
if (!shouldBeVisible) {
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
}
}
onOpened: {
Qt.callLater(() => {
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus()
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus()
}
}
})
if (contentLoader.item) {
if (requiresEnterprise && contentLoader.item.usernameInput) {
contentLoader.item.usernameInput.forceActiveFocus();
} else if (contentLoader.item.passwordInput) {
contentLoader.item.passwordInput.forceActiveFocus();
}
}
});
}
onBackgroundClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
}
Connections {
target: NetworkService
function onPasswordDialogShouldReopenChanged() {
if (NetworkService.passwordDialogShouldReopen && NetworkService.connectingSSID !== "") {
wifiPasswordSSID = NetworkService.connectingSSID
wifiPasswordInput = ""
open()
NetworkService.passwordDialogShouldReopen = false
wifiPasswordSSID = NetworkService.connectingSSID;
wifiPasswordInput = "";
open();
NetworkService.passwordDialogShouldReopen = false;
}
}
}
@@ -155,16 +168,16 @@ DankModal {
anchors.fill: parent
focus: true
Keys.onEscapePressed: event => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
event.accepted = true
}
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
event.accepted = true;
}
Column {
anchors.centerIn: parent
@@ -181,9 +194,9 @@ DankModal {
StyledText {
text: {
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
color: Theme.surfaceText
@@ -197,10 +210,10 @@ DankModal {
StyledText {
text: {
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 ")
return prefix + wifiPasswordSSID
const prefix = requiresEnterprise ? I18n.tr("Enter credentials for ") : I18n.tr("Enter password for ");
return prefix + wifiPasswordSSID;
}
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceTextMedium
@@ -223,15 +236,15 @@ DankModal {
iconSize: Theme.iconSize - 4
iconColor: Theme.surfaceText
onClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
}
}
}
@@ -247,8 +260,8 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
usernameInput.forceActiveFocus()
}
usernameInput.forceActiveFocus();
}
}
DankTextField {
@@ -262,13 +275,13 @@ DankModal {
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiUsernameInput = text
}
wifiUsernameInput = text;
}
onAccepted: () => {
if (passwordInput) {
passwordInput.forceActiveFocus()
}
}
if (passwordInput) {
passwordInput.forceActiveFocus();
}
}
}
}
@@ -283,8 +296,8 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
passwordInput.forceActiveFocus()
}
passwordInput.forceActiveFocus();
}
}
DankTextField {
@@ -300,43 +313,42 @@ DankModal {
focus: !requiresEnterprise
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiPasswordInput = text
}
wifiPasswordInput = text;
}
onAccepted: () => {
if (isPromptMode) {
const secrets = {}
if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text
if (passwordInput.text) secrets["password"] = passwordInput.text
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
}
NetworkService.submitCredentials(promptToken, secrets, true)
} else {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
if (isPromptMode) {
const secrets = {};
if (isVpnPrompt) {
if (passwordInput.text)
secrets["password"] = passwordInput.text;
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text;
} else if (promptSetting === "802-1x") {
if (usernameInput.text)
secrets["identity"] = usernameInput.text;
if (passwordInput.text)
secrets["password"] = passwordInput.text;
if (wifiAnonymousIdentityInput)
secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
}
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
} else {
const username = requiresEnterprise ? usernameInput.text : "";
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
passwordInput.text = "";
if (requiresEnterprise)
usernameInput.text = "";
}
Component.onCompleted: () => {
if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start()
}
if (root.shouldBeVisible && !requiresEnterprise)
focusDelayTimer.start();
}
Timer {
id: focusDelayTimer
@@ -344,14 +356,14 @@ DankModal {
interval: 100
repeat: false
onTriggered: () => {
if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus()
} else {
passwordInput.forceActiveFocus()
}
}
}
if (root.shouldBeVisible) {
if (requiresEnterprise && usernameInput) {
usernameInput.forceActiveFocus();
} else {
passwordInput.forceActiveFocus();
}
}
}
}
Connections {
@@ -359,7 +371,7 @@ DankModal {
function onShouldBeVisibleChanged() {
if (root.shouldBeVisible)
focusDelayTimer.start()
focusDelayTimer.start();
}
}
}
@@ -377,8 +389,8 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
anonInput.forceActiveFocus()
}
anonInput.forceActiveFocus();
}
}
DankTextField {
@@ -392,8 +404,8 @@ DankModal {
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiAnonymousIdentityInput = text
}
wifiAnonymousIdentityInput = text;
}
}
}
@@ -409,8 +421,8 @@ DankModal {
MouseArea {
anchors.fill: parent
onClicked: () => {
domainMatchInput.forceActiveFocus()
}
domainMatchInput.forceActiveFocus();
}
}
DankTextField {
@@ -424,49 +436,96 @@ DankModal {
backgroundColor: "transparent"
enabled: root.shouldBeVisible
onTextEdited: () => {
wifiDomainInput = text
}
wifiDomainInput = text;
}
}
}
Row {
Column {
spacing: Theme.spacingS
width: parent.width
Rectangle {
id: showPasswordCheckbox
Row {
spacing: Theme.spacingS
property bool checked: false
Rectangle {
id: showPasswordCheckbox
width: 20
height: 20
radius: 4
color: checked ? Theme.primary : "transparent"
border.color: checked ? Theme.primary : Theme.outlineButton
border.width: 2
property bool checked: false
DankIcon {
anchors.centerIn: parent
name: "check"
size: 12
color: Theme.background
visible: parent.checked
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: () => {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked;
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: () => {
showPasswordCheckbox.checked = !showPasswordCheckbox.checked
}
StyledText {
text: I18n.tr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
}
}
StyledText {
text: I18n.tr("Show password")
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
Row {
spacing: Theme.spacingS
visible: isVpnPrompt
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
cursorShape: Qt.PointingHandCursor
onClicked: () => {
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
}
if (isPromptMode) {
NetworkService.cancelCredentials(promptToken);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
}
}
}
@@ -523,9 +582,9 @@ DankModal {
color: connectArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
enabled: {
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
@@ -547,36 +606,35 @@ DankModal {
cursorShape: Qt.PointingHandCursor
enabled: parent.enabled
onClicked: () => {
if (isPromptMode) {
const secrets = {}
if (isVpnPrompt) {
if (passwordInput.text) secrets["password"] = passwordInput.text
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text
} else if (promptSetting === "802-1x") {
if (usernameInput.text) secrets["identity"] = usernameInput.text
if (passwordInput.text) secrets["password"] = passwordInput.text
if (wifiAnonymousIdentityInput) secrets["anonymous-identity"] = wifiAnonymousIdentityInput
}
NetworkService.submitCredentials(promptToken, secrets, true)
} else {
const username = requiresEnterprise ? usernameInput.text : ""
NetworkService.connectToWifi(
wifiPasswordSSID,
passwordInput.text,
username,
wifiAnonymousIdentityInput,
wifiDomainInput
)
}
close()
wifiPasswordInput = ""
wifiUsernameInput = ""
wifiAnonymousIdentityInput = ""
wifiDomainInput = ""
passwordInput.text = ""
if (requiresEnterprise) usernameInput.text = ""
}
if (isPromptMode) {
const secrets = {};
if (isVpnPrompt) {
if (passwordInput.text)
secrets["password"] = passwordInput.text;
} else if (promptSetting === "802-11-wireless-security") {
secrets["psk"] = passwordInput.text;
} else if (promptSetting === "802-1x") {
if (usernameInput.text)
secrets["identity"] = usernameInput.text;
if (passwordInput.text)
secrets["password"] = passwordInput.text;
if (wifiAnonymousIdentityInput)
secrets["anonymous-identity"] = wifiAnonymousIdentityInput;
}
NetworkService.submitCredentials(promptToken, secrets, savePasswordCheckbox.checked);
} else {
const username = requiresEnterprise ? usernameInput.text : "";
NetworkService.connectToWifi(wifiPasswordSSID, passwordInput.text, username, wifiAnonymousIdentityInput, wifiDomainInput);
}
close();
wifiPasswordInput = "";
wifiUsernameInput = "";
wifiAnonymousIdentityInput = "";
wifiDomainInput = "";
passwordInput.text = "";
if (requiresEnterprise)
usernameInput.text = "";
}
}
Behavior on color {

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