mirror of
https://github.com/AvengeMedia/DankMaterialShell.git
synced 2026-08-02 03:28:28 -04:00
Compare commits
133 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| accdd0972c | |||
| 00c28f69fd | |||
| 2e0337b68c | |||
| 4975960ea4 | |||
| 0381a112b3 | |||
| ed5e2a22c0 | |||
| 847718459c | |||
| 95b120b475 | |||
| 4dd2f1d5be | |||
| bf5faf725b | |||
| e5eff919b8 | |||
| 606115748a | |||
| ad936bfdc9 | |||
| 110396cb2a | |||
| 7b08e61863 | |||
| 876ead665b | |||
| 74896fb87c | |||
| f1f528b2cb | |||
| 9853e1431b | |||
| e7b8196a4a | |||
| 85ebb402cb | |||
| f37f4a1f35 | |||
| fc11dfbc57 | |||
| 7806cf4a58 | |||
| 89895da4fa | |||
| 4ff93bf46e | |||
| 63ffd7caec | |||
| dac08b34c6 | |||
| 840a448b4a | |||
| 26d429fa31 | |||
| fd20cce9b7 | |||
| ad5a96d95b | |||
| 8ba5e1a082 | |||
| 7d6a7bae06 | |||
| 986a6e0230 | |||
| 64ba1fbb1b | |||
| ab9df791b3 | |||
| 65f4a9a044 | |||
| 59164cbd30 | |||
| 7ab1f6a956 | |||
| 841167ac44 | |||
| c2a6a7aebd | |||
| b17ef80c91 | |||
| a55177f1d2 | |||
| 3d0ce17a8c | |||
| 6306bc7aa2 | |||
| eca92898f9 | |||
| db9fb0bd90 | |||
| 815820aa27 | |||
| 01c0ed2778 | |||
| 9967db0c5a | |||
| 883a787db7 | |||
| 3b1a3c1c22 | |||
| 970262f276 | |||
| b708e97ec5 | |||
| e94fc7fe99 | |||
| c4bca6f6cd | |||
| 9e7fc833e9 | |||
| 9e2d493749 | |||
| 93120fd73b | |||
| 536ebf7b0a | |||
| 1105047a80 | |||
| 8b46397200 | |||
| 6b70491ba9 | |||
| deb0138198 | |||
| 2e0587ca16 | |||
| 45cafc6bdb | |||
| 82d5081b7d | |||
| b5b083305f | |||
| 039d14c790 | |||
| ee7ac9d2bd | |||
| bd159be414 | |||
| 4dda0e3a5e | |||
| 724ee9422d | |||
| c91cb5d89a | |||
| 660953ebc5 | |||
| bd70a6286e | |||
| d87e2fcb01 | |||
| 928b272628 | |||
| 781ed16538 | |||
| 417da7408d | |||
| 0d550662de | |||
| 35e35a16fa | |||
| d96559f7df | |||
| 7f37799e53 | |||
| fcda17f517 | |||
| 4b05e7a0fa | |||
| 7c6ad65b5a | |||
| acda199e28 | |||
| b3a129af2f | |||
| e9d958b32b | |||
| dd9edd8a00 | |||
| 28c25d46d5 | |||
| 58530ae56c | |||
| c0134c249a | |||
| f7157f0e92 | |||
| e29b9749df | |||
| 6791c20f40 | |||
| 6c2572fbfe | |||
| 3ee07f7f20 | |||
| ef244cb645 | |||
| 860c347362 | |||
| c7b58e52d7 | |||
| e0fb817a75 | |||
| b5908f4d1b | |||
| ee6c91e094 | |||
| 59d367f4a4 | |||
| ee84489b95 | |||
| b1beecb083 | |||
| ffd3840058 | |||
| 447b4d89b6 | |||
| 36772735f3 | |||
| 375afaaf3a | |||
| fe0214d522 | |||
| fce1a09790 | |||
| 554e55c63a | |||
| f7d8e2b56c | |||
| 1d4572ac1c | |||
| c02a24fecf | |||
| bf644516f8 | |||
| 61f9ea4954 | |||
| 5a169bea72 | |||
| 3d60f35683 | |||
| b95f4cdbf3 | |||
| b834e82668 | |||
| 302cfda2f4 | |||
| c86676df9b | |||
| fa26dbb544 | |||
| 067d01f678 | |||
| 95c8cc80f6 | |||
| b8f216dc0f | |||
| e84e63fa3a | |||
| 6d08b91004 |
@@ -1,56 +0,0 @@
|
||||
name: Sync flake.lock to dank-qml-common
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
paths:
|
||||
- "dank-qml-common"
|
||||
branches:
|
||||
- master
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.APP_ID }}
|
||||
private-key: ${{ secrets.APP_PRIVATE_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
token: ${{ steps.app_token.outputs.token }}
|
||||
|
||||
- name: Install Nix
|
||||
uses: cachix/install-nix-action@v31
|
||||
|
||||
- name: Point flake input at the submodule commit
|
||||
run: |
|
||||
set -euo pipefail
|
||||
submodule_rev=$(git ls-tree HEAD dank-qml-common --object-only)
|
||||
flake_rev=$(python3 -c "import json; print(json.load(open('flake.lock'))['nodes']['dank-qml-common']['locked']['rev'])")
|
||||
[ "$submodule_rev" = "$flake_rev" ] && { echo "flake.lock already matches $submodule_rev"; exit 0; }
|
||||
nix flake lock --override-input dank-qml-common "github:AvengeMedia/dank-qml-common/$submodule_rev"
|
||||
|
||||
- name: Commit and push flake.lock update
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.app_token.outputs.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if git diff --quiet flake.lock; then
|
||||
echo "No changes to flake.lock"
|
||||
exit 0
|
||||
fi
|
||||
git config user.name "dms-ci[bot]"
|
||||
git config user.email "dms-ci[bot]@users.noreply.github.com"
|
||||
git add flake.lock
|
||||
git commit -m "nix: sync flake.lock to dank-qml-common submodule"
|
||||
git pull --rebase origin ${{ github.ref_name }}
|
||||
git push https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git HEAD:${{ github.ref_name }}
|
||||
@@ -27,8 +27,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
@@ -10,8 +10,6 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install flatpak
|
||||
run: sudo apt update && sudo apt install -y flatpak
|
||||
|
||||
@@ -42,7 +42,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -192,13 +191,11 @@ jobs:
|
||||
with:
|
||||
ref: ${{ inputs.tag }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Fetch updated tag after version bump
|
||||
run: |
|
||||
git fetch origin --force tag ${TAG}
|
||||
git checkout ${TAG}
|
||||
git submodule update --init --recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
@@ -255,7 +252,6 @@ jobs:
|
||||
- **`dankinstall-arm64.gz`** - Installer binary for ARM64 systems
|
||||
- **`dms-cli-<version>.tar.gz`** - Go source code with vendored modules (for distro packaging)
|
||||
- **`dms-qml.tar.gz`** - QML source code only
|
||||
- **`dms-source.tar.gz`** - Full repository source with bundled DankCommon (for distro packaging)
|
||||
|
||||
### Checksums
|
||||
- **`*.sha256`** - SHA256 checksums for verifying download integrity
|
||||
@@ -303,22 +299,6 @@ jobs:
|
||||
# Copy completions
|
||||
cp _core_assets/completion.* _release_assets/ 2>/dev/null || true
|
||||
|
||||
# Replace the DankCommon symlink with real submodule content for packaging
|
||||
rm quickshell/DankCommon
|
||||
cp -r dank-qml-common/DankCommon quickshell/DankCommon
|
||||
|
||||
# Create full source tarball (GitHub tag archives never contain submodule content)
|
||||
VERSION_NUM=${TAG#v}
|
||||
tar --exclude='.git' \
|
||||
--exclude='.github' \
|
||||
--exclude='dank-qml-common' \
|
||||
--exclude='_release_assets' \
|
||||
--exclude='_core_assets' \
|
||||
--exclude='RELEASE_BODY.md' \
|
||||
--transform "s,^\.,DankMaterialShell-${VERSION_NUM},S" \
|
||||
-czf _release_assets/dms-source.tar.gz .
|
||||
(cd _release_assets && sha256sum dms-source.tar.gz > dms-source.tar.gz.sha256)
|
||||
|
||||
# Create QML source package (exclude build artifacts and git files)
|
||||
# Copy root LICENSE and CONTRIBUTING.md to quickshell/ for packaging
|
||||
cp LICENSE CONTRIBUTING.md quickshell/
|
||||
|
||||
@@ -36,7 +36,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Check for updates
|
||||
id: check
|
||||
@@ -200,7 +199,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Wait before OBS upload
|
||||
run: sleep 3
|
||||
|
||||
@@ -36,7 +36,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -95,7 +94,6 @@ jobs:
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
|
||||
@@ -83,7 +83,6 @@ jobs:
|
||||
with:
|
||||
ref: ${{ github.event.release.tag_name || (github.event.inputs.version && (startsWith(github.event.inputs.version, 'v') && github.event.inputs.version || format('v{0}', github.event.inputs.version))) || github.ref }}
|
||||
fetch-depth: 0
|
||||
submodules: recursive
|
||||
|
||||
- name: Record checked-out source commit
|
||||
run: echo "SOURCE_COMMIT=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
@@ -226,7 +225,7 @@ jobs:
|
||||
if [ -n "$RELEASE_VER" ] && { [ "$BUILD_DMS" = "true" ] || [ "$BUILD_GREETER" = "true" ]; }; then
|
||||
echo "🔧 Updating stable templates for $ARCHIVE_TAG"
|
||||
TARBALL="$(mktemp)"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
|
||||
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
|
||||
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
|
||||
rm -f "$TARBALL"
|
||||
|
||||
@@ -247,7 +246,7 @@ jobs:
|
||||
# into $wrksrc (create_wrksrc=yes handles the rest).
|
||||
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
|
||||
mkdir -p "$SRC_CACHE"
|
||||
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
|
||||
--exclude=void-packages \
|
||||
--exclude=r2-repo \
|
||||
--exclude=.git \
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
[submodule "dank-qml-common"]
|
||||
path = dank-qml-common
|
||||
url = https://github.com/AvengeMedia/dank-qml-common.git
|
||||
@@ -36,14 +36,6 @@ repos:
|
||||
language: system
|
||||
files: ^quickshell/(.*\.qml|translations/(term_freeze\.json|check_term_freeze\.py|extract_translations\.py))$
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: i18n-term-variants
|
||||
name: i18n term variants (no case/punctuation duplicates)
|
||||
entry: python3 quickshell/translations/check_term_variants.py
|
||||
language: system
|
||||
files: ^quickshell/(.*\.qml|translations/(check_term_variants\.py|extract_translations\.py))$
|
||||
pass_filenames: false
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: no-console-in-qml
|
||||
|
||||
@@ -6,14 +6,6 @@ To contribute fork this repository, make your changes, and open a pull request.
|
||||
|
||||
## Setup
|
||||
|
||||
Clone with submodules — the shared widget library ([dank-qml-common](https://github.com/AvengeMedia/dank-qml-common)) is vendored at `dank-qml-common/` and symlinked into `quickshell/DankCommon`:
|
||||
|
||||
```bash
|
||||
git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git
|
||||
# or, in an existing clone:
|
||||
git submodule update --init
|
||||
```
|
||||
|
||||
Install [prek](https://prek.j178.dev/) then activate pre-commit hooks:
|
||||
|
||||
```bash
|
||||
@@ -36,24 +28,6 @@ This will provide:
|
||||
|
||||
The dev shell automatically creates the `.qmlls.ini` file in the `quickshell/` directory.
|
||||
|
||||
## Shared widgets (dank-qml-common)
|
||||
|
||||
Everything under `quickshell/DankCommon/` (core widgets, the file browser, scroll physics, bundled fonts) is shared across the DMS suite and lives in the `dank-qml-common` submodule. It is a normal git worktree:
|
||||
|
||||
1. Edit files under `dank-qml-common/` (or through the `quickshell/DankCommon` symlink — same files) and test in the running shell; hot reload works as usual. For isolated widget work, the library is its own runnable config with a gallery: `qs -c dank-qml-common`.
|
||||
2. Commit and PR those changes in the `dank-qml-common` repo: `cd dank-qml-common && git switch -c my-change`, push, open the PR there.
|
||||
3. Once merged, bump the pointer here: `make update-common` (updates the submodule and the nix flake input together), then commit alongside any DMS-side changes. If you only bump the submodule, CI syncs `flake.lock` to it automatically on master.
|
||||
|
||||
The submodule URL in `.gitmodules` is HTTPS so CI and anonymous clones keep working. To push over SSH instead of being prompted for credentials, add a push rewrite to your git config — fetches stay HTTPS, pushes use SSH:
|
||||
|
||||
```bash
|
||||
git config --global url."git@github.com:AvengeMedia/".pushInsteadOf "https://github.com/AvengeMedia/"
|
||||
```
|
||||
|
||||
Shared widgets read app-provided singletons (`Theme`, `SettingsData`, ...) through a documented contract — see the dank-qml-common README. If your change needs a new contract property, add it to the library's stub singletons in the same PR, then to `quickshell/Common/` here when you bump.
|
||||
|
||||
Files in `quickshell/Widgets/`, `quickshell/Common/`, and `quickshell/Modals/FileBrowser/` that moved to the library remain in place as thin wrappers, so `import qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` keep working for the shell and for plugins.
|
||||
|
||||
## VSCode Setup
|
||||
|
||||
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.
|
||||
@@ -130,8 +104,6 @@ Text {
|
||||
|
||||
Preferably, try to keep new terms to a minimum and re-use existing terms where possible. See `quickshell/translations/en.json` for the list of existing terms. (This isn't always possible obviously, but instead of using `Auto-connect` you would use `Autoconnect` since it's already translated)
|
||||
|
||||
Strings inside `quickshell/DankCommon/` are owned by the dank-qml-common repo but stay in the DMS POEditor project — extraction here deliberately skips them, and `scripts/i18nsync.py sync` uploads the union of app terms and the submodule's terms instead (common terms carry the `dank-qml-common` tag). On download the sync splits the exports: app translations go to `quickshell/translations/poexports/`, common translations go to `dank-qml-common/DankCommon/translations/poexports/` for you to commit in that repo and bump. At runtime `I18n` merges both catalogs (app terms win). Other apps (dankcalendar) keep their own POEditor projects and merge the `dank-qml-common`-tagged terms from the DMS project.
|
||||
|
||||
### GO (`core` directory)
|
||||
|
||||
1. Install the [Go Extension](https://code.visualstudio.com/docs/languages/go)
|
||||
|
||||
@@ -35,12 +35,6 @@ clean:
|
||||
lint-qml:
|
||||
@./quickshell/scripts/qmllint-entrypoints.sh
|
||||
|
||||
# Pull the latest dank-qml-common and pin it everywhere it is consumed
|
||||
# (submodule pointer + nix flake input). Commit both in one change.
|
||||
update-common:
|
||||
git submodule update --remote --merge dank-qml-common
|
||||
nix --extra-experimental-features 'nix-command flakes' flake update dank-qml-common
|
||||
|
||||
# Installation targets
|
||||
install-bin:
|
||||
@echo "Installing $(BINARY_NAME) to $(INSTALL_DIR)..."
|
||||
@@ -49,9 +43,8 @@ install-bin:
|
||||
|
||||
install-shell:
|
||||
@echo "Installing shell files to $(SHELL_INSTALL_DIR)..."
|
||||
@test -e $(SHELL_DIR)/DankCommon/Widgets/DankIcon.qml || { echo "DankCommon missing: run git submodule update --init"; exit 1; }
|
||||
@mkdir -p $(SHELL_INSTALL_DIR)
|
||||
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@cp -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
|
||||
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
|
||||
@echo "Shell files installed"
|
||||
|
||||
|
||||
@@ -250,10 +250,6 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("failed to fetch changes: %w", err)
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
if currentTag != "" {
|
||||
latestTagCmd := exec.Command("git", "tag", "-l", "v*", "--sort=-version:refname")
|
||||
latestTagOutput, err := latestTagCmd.Output()
|
||||
@@ -295,10 +291,6 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("\nUpdate complete! Updated from %s to %s\n", currentTag, latestTag)
|
||||
return nil
|
||||
}
|
||||
@@ -328,21 +320,10 @@ func updateOtherDistros() error {
|
||||
return fmt.Errorf("update cancelled")
|
||||
}
|
||||
|
||||
if err := updateSubmodules(); err != nil {
|
||||
return fmt.Errorf("failed to update submodules: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println("\nUpdate complete!")
|
||||
return nil
|
||||
}
|
||||
|
||||
func updateSubmodules() error {
|
||||
submoduleCmd := exec.Command("git", "submodule", "update", "--init", "--recursive")
|
||||
submoduleCmd.Stdout = os.Stdout
|
||||
submoduleCmd.Stderr = os.Stderr
|
||||
return submoduleCmd.Run()
|
||||
}
|
||||
|
||||
func offerReclone(dmsPath string) bool {
|
||||
fmt.Println("\nWould you like to backup and re-clone the repository? (y/N): ")
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
@@ -361,7 +342,7 @@ func offerReclone(dmsPath string) bool {
|
||||
}
|
||||
|
||||
fmt.Println("Cloning fresh copy...")
|
||||
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
cloneCmd.Stdout = os.Stdout
|
||||
cloneCmd.Stderr = os.Stderr
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
|
||||
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
Progress: 0.90,
|
||||
Step: "Cloning DankMaterialShell...",
|
||||
IsComplete: false,
|
||||
CommandInfo: "git clone --recurse-submodules https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
|
||||
}
|
||||
|
||||
configDir := filepath.Dir(dmsPath)
|
||||
@@ -528,7 +528,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return fmt.Errorf("failed to create quickshell config directory: %w", err)
|
||||
}
|
||||
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
|
||||
cloneCmd := exec.CommandContext(ctx, "git", "clone",
|
||||
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
|
||||
if err := cloneCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
|
||||
@@ -553,8 +553,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log(fmt.Sprintf("Checked out latest tag: %s", latestTag))
|
||||
m.log("DankMaterialShell cloned successfully")
|
||||
return nil
|
||||
@@ -593,8 +591,6 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log("DankMaterialShell updated successfully (git variant)")
|
||||
return nil
|
||||
}
|
||||
@@ -613,19 +609,10 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
|
||||
return nil
|
||||
}
|
||||
|
||||
m.syncDMSSubmodules(ctx, dmsPath)
|
||||
|
||||
m.log(fmt.Sprintf("Updated to tag: %s", latestTag))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) syncDMSSubmodules(ctx context.Context, dmsPath string) {
|
||||
submoduleCmd := exec.CommandContext(ctx, "git", "-C", dmsPath, "submodule", "update", "--init", "--recursive")
|
||||
if err := submoduleCmd.Run(); err != nil {
|
||||
m.logError("Failed to update submodules", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ManualPackageInstaller) installXwaylandSatellite(ctx context.Context, sudoPassword string, progressChan chan<- InstallProgressMsg) error {
|
||||
m.log("Installing xwayland-satellite from source...")
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ type minimalInstallGroup struct {
|
||||
|
||||
func shouldPreferMinimalInstall(pkg string) bool {
|
||||
switch pkg {
|
||||
case "niri", "niri-git", "hyprland", "hyprland-git":
|
||||
case "niri", "niri-git":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
|
||||
@@ -44,6 +44,7 @@ type TemplateDef struct {
|
||||
ID string
|
||||
Commands []string
|
||||
Flatpaks []string
|
||||
ConfigDirs []string
|
||||
ConfigFile string
|
||||
Kind TemplateKind
|
||||
RunUnconditionally bool
|
||||
@@ -60,9 +61,9 @@ var templateRegistry = []TemplateDef{
|
||||
{ID: "firefox", Commands: []string{"firefox"}, ConfigFile: "firefox.toml"},
|
||||
{ID: "pywalfox", Commands: []string{"pywalfox"}, ConfigFile: "pywalfox.toml"},
|
||||
{ID: "zenbrowser", Commands: []string{"zen", "zen-browser", "zen-beta", "zen-twilight"}, Flatpaks: []string{"app.zen_browser.zen"}, ConfigFile: "zenbrowser.toml"},
|
||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigFile: "vesktop.toml"},
|
||||
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigFile: "vencord.toml"},
|
||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||
{ID: "vesktop", Commands: []string{"vesktop"}, Flatpaks: []string{"dev.vencord.Vesktop"}, ConfigDirs: []string{"vesktop"}, ConfigFile: "vesktop.toml"},
|
||||
{ID: "vencord", Commands: []string{"discord", "Discord", "discord-canary", "DiscordCanary"}, Flatpaks: []string{"com.discordapp.Discord", "com.discordapp.DiscordCanary"}, ConfigDirs: []string{"Vencord"}, ConfigFile: "vencord.toml"},
|
||||
{ID: "equibop", Commands: []string{"equibop"}, ConfigDirs: []string{"equibop"}, ConfigFile: "equibop.toml"},
|
||||
{ID: "ghostty", Commands: []string{"ghostty"}, ConfigFile: "ghostty.toml", Kind: TemplateKindTerminal},
|
||||
{ID: "kitty", Commands: []string{"kitty"}, ConfigFile: "kitty.toml", Kind: TemplateKindTerminal},
|
||||
{ID: "foot", Commands: []string{"foot"}, ConfigFile: "foot.toml", Kind: TemplateKindTerminal},
|
||||
@@ -459,9 +460,9 @@ output_path = '%s'
|
||||
case TemplateKindGTK:
|
||||
switch opts.Mode {
|
||||
case ColorModeLight:
|
||||
appendConfig(opts, cfgFile, nil, nil, "gtk3-light.toml")
|
||||
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-light.toml")
|
||||
default:
|
||||
appendConfig(opts, cfgFile, nil, nil, "gtk3-dark.toml")
|
||||
appendConfig(opts, cfgFile, nil, nil, nil, "gtk3-dark.toml")
|
||||
}
|
||||
case TemplateKindTerminal:
|
||||
appendTerminalConfig(opts, cfgFile, tmpDir, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
@@ -474,10 +475,10 @@ output_path = '%s'
|
||||
appendVSCodeConfig(cfgFile, "vscode-insiders", filepath.Join(homeDir, ".vscode-insiders/extensions"), opts.ShellDir)
|
||||
case TemplateKindEmacs:
|
||||
if utils.EmacsConfigDir() != "" {
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
|
||||
}
|
||||
default:
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigFile)
|
||||
appendConfig(opts, cfgFile, tmpl.Commands, tmpl.Flatpaks, tmpl.ConfigDirs, tmpl.ConfigFile)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,13 +513,14 @@ func appendConfig(
|
||||
cfgFile *os.File,
|
||||
checkCmd []string,
|
||||
checkFlatpaks []string,
|
||||
checkConfigDirs []string,
|
||||
fileName string,
|
||||
) {
|
||||
configPath := filepath.Join(opts.ShellDir, "matugen", "configs", fileName)
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
return
|
||||
}
|
||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) {
|
||||
if !appExists(opts.AppChecker, checkCmd, checkFlatpaks) && !configDirExists(checkConfigDirs) {
|
||||
return
|
||||
}
|
||||
data, err := os.ReadFile(configPath)
|
||||
@@ -599,6 +601,20 @@ func templateSessionActive(tmpl TemplateDef) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func configDirExists(names []string) bool {
|
||||
configHome := utils.XDGConfigHome()
|
||||
if configHome == "" {
|
||||
return false
|
||||
}
|
||||
for _, name := range names {
|
||||
info, err := os.Stat(filepath.Join(configHome, name))
|
||||
if err == nil && info.IsDir() {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func appExists(checker utils.AppChecker, checkCmd []string, checkFlatpaks []string) bool {
|
||||
// Both nil is treated as "skip check" / unconditionally run
|
||||
if checkCmd == nil && checkFlatpaks == nil {
|
||||
@@ -1096,7 +1112,7 @@ func CheckTemplates(checker utils.AppChecker) []TemplateCheck {
|
||||
case tmpl.Kind == TemplateKindEmacs:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && utils.EmacsConfigDir() != ""
|
||||
default:
|
||||
detected = appExists(checker, tmpl.Commands, tmpl.Flatpaks) && templateSessionActive(tmpl)
|
||||
detected = (appExists(checker, tmpl.Commands, tmpl.Flatpaks) || configDirExists(tmpl.ConfigDirs)) && templateSessionActive(tmpl)
|
||||
}
|
||||
|
||||
checks = append(checks, TemplateCheck{ID: tmpl.ID, Detected: detected})
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestAppendConfigBinaryExists(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, nil, "test.toml")
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, nil, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -82,7 +82,7 @@ func TestAppendConfigBinaryDoesNotExist(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, "test.toml")
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{}, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -122,7 +122,7 @@ func TestAppendConfigFlatpakExists(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, "test.toml")
|
||||
appendConfig(opts, cfgFile, nil, []string{"app.zen_browser.zen"}, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -163,7 +163,7 @@ func TestAppendConfigFlatpakDoesNotExist(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||
appendConfig(opts, cfgFile, []string{}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -203,7 +203,7 @@ func TestAppendConfigBothExist(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, "test.toml")
|
||||
appendConfig(opts, cfgFile, []string{"sh"}, []string{"app.zen_browser.zen"}, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -244,7 +244,7 @@ func TestAppendConfigNeitherExists(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, "test.toml")
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -281,7 +281,7 @@ func TestAppendConfigNoChecks(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
|
||||
appendConfig(opts, cfgFile, nil, nil, "test.toml")
|
||||
appendConfig(opts, cfgFile, nil, nil, nil, "test.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -312,7 +312,7 @@ func TestAppendConfigFileDoesNotExist(t *testing.T) {
|
||||
|
||||
opts := &Options{ShellDir: shellDir}
|
||||
|
||||
appendConfig(opts, cfgFile, nil, nil, "nonexistent.toml")
|
||||
appendConfig(opts, cfgFile, nil, nil, nil, "nonexistent.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
@@ -487,3 +487,90 @@ func TestBuildMergedConfigSkipsMangowcWithoutActiveSession(t *testing.T) {
|
||||
}
|
||||
assert.NotContains(t, string(output), "[templates.dmsmango]")
|
||||
}
|
||||
|
||||
func TestAppendConfigConfigDirExists(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create configs dir: %v", err)
|
||||
}
|
||||
|
||||
testConfig := "vencord config content"
|
||||
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte(testConfig), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
configHome := filepath.Join(tempDir, "config")
|
||||
if err := os.MkdirAll(filepath.Join(configHome, "Vencord"), 0o755); err != nil {
|
||||
t.Fatalf("failed to create Vencord config dir: %v", err)
|
||||
}
|
||||
t.Setenv("XDG_CONFIG_HOME", configHome)
|
||||
|
||||
outFile := filepath.Join(tempDir, "output.toml")
|
||||
cfgFile, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create output file: %v", err)
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read output: %v", err)
|
||||
}
|
||||
|
||||
assert.Equal(t, testConfig+"\n", string(output))
|
||||
}
|
||||
|
||||
func TestAppendConfigConfigDirDoesNotExist(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
shellDir := filepath.Join(tempDir, "shell")
|
||||
configsDir := filepath.Join(shellDir, "matugen", "configs")
|
||||
if err := os.MkdirAll(configsDir, 0o755); err != nil {
|
||||
t.Fatalf("failed to create configs dir: %v", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(filepath.Join(configsDir, "vencord.toml"), []byte("vencord config content"), 0o644); err != nil {
|
||||
t.Fatalf("failed to write config: %v", err)
|
||||
}
|
||||
|
||||
configHome := filepath.Join(tempDir, "config")
|
||||
if err := os.MkdirAll(configHome, 0o755); err != nil {
|
||||
t.Fatalf("failed to create config home: %v", err)
|
||||
}
|
||||
t.Setenv("XDG_CONFIG_HOME", configHome)
|
||||
|
||||
outFile := filepath.Join(tempDir, "output.toml")
|
||||
cfgFile, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create output file: %v", err)
|
||||
}
|
||||
defer cfgFile.Close()
|
||||
|
||||
mockChecker := mocks_utils.NewMockAppChecker(t)
|
||||
mockChecker.EXPECT().AnyCommandExists("nonexistent-binary-12345").Return(false)
|
||||
mockChecker.EXPECT().AnyFlatpakExists("com.nonexistent.flatpak").Return(false)
|
||||
|
||||
opts := &Options{ShellDir: shellDir, AppChecker: mockChecker}
|
||||
|
||||
appendConfig(opts, cfgFile, []string{"nonexistent-binary-12345"}, []string{"com.nonexistent.flatpak"}, []string{"Vencord"}, "vencord.toml")
|
||||
|
||||
cfgFile.Close()
|
||||
output, err := os.ReadFile(outFile)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read output: %v", err)
|
||||
}
|
||||
|
||||
assert.Empty(t, string(output))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package qmlchecks
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGreeterRememberLastSessionFallsBackToDesktopID(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../quickshell/Modules/Greetd/GreeterContent.qml")
|
||||
if err != nil {
|
||||
t.Fatalf("read greeter QML: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
if !strings.Contains(content, "GreetdMemory.lastSessionDesktopId || desktopIdFromPath(GreetdMemory.lastSessionId)") {
|
||||
t.Fatalf("remembered greeter sessions should derive a desktop id from legacy absolute session paths")
|
||||
}
|
||||
if !strings.Contains(content, "GreeterState.sessionDesktopIds[i] === savedDesktopId") {
|
||||
t.Fatalf("remembered greeter sessions should match current sessions by desktop id")
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,32 @@ func TestLockScreenPasswordFieldBypassesTextInputIME(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenAuthenticationCardOwnsFactorControls(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../quickshell/Modules/Settings/LockScreenTab.qml")
|
||||
if err != nil {
|
||||
t.Fatalf("read lock screen settings QML: %v", err)
|
||||
}
|
||||
|
||||
content := string(data)
|
||||
authCard := strings.Index(content, `title: I18n.tr("Authentication")`)
|
||||
behaviorCard := strings.Index(content, `title: I18n.tr("Behavior")`)
|
||||
fingerprintToggle := strings.Index(content, `settingKey: "enableFprint"`)
|
||||
u2fToggle := strings.Index(content, `settingKey: "enableU2f"`)
|
||||
u2fSource := strings.Index(content, `settingKey: "lockU2fPamPath"`)
|
||||
if authCard < 0 || behaviorCard < 0 || fingerprintToggle < 0 || u2fToggle < 0 || u2fSource < 0 {
|
||||
t.Fatalf("expected authentication card, factor toggles, and U2F source setting")
|
||||
}
|
||||
for name, position := range map[string]int{
|
||||
"fingerprint toggle": fingerprintToggle,
|
||||
"U2F toggle": u2fToggle,
|
||||
"U2F source": u2fSource,
|
||||
} {
|
||||
if position < authCard || position > behaviorCard {
|
||||
t.Fatalf("%s must remain in the authentication card", name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLockScreenPamSupportsManagedAndSystemPolicies(t *testing.T) {
|
||||
data, err := os.ReadFile("../../../quickshell/Modules/Lock/Pam.qml")
|
||||
if err != nil {
|
||||
|
||||
Submodule dank-qml-common deleted from 5942bc1c60
@@ -70,7 +70,7 @@ override_dh_auto_install:
|
||||
|
||||
mkdir -p debian/dms-git/usr/share/quickshell/dms debian/dms-git/usr/lib/systemd/user
|
||||
if [ -d quickshell ]; then \
|
||||
cp -rL quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
|
||||
cp -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
|
||||
install -Dm644 assets/systemd/dms.service debian/dms-git/usr/lib/systemd/user/dms.service; \
|
||||
install -Dm644 assets/dms-open.desktop debian/dms-git/usr/share/applications/dms-open.desktop; \
|
||||
install -Dm644 assets/com.danklinux.dms.desktop debian/dms-git/usr/share/applications/com.danklinux.dms.desktop; \
|
||||
|
||||
@@ -20,7 +20,7 @@ override_dh_auto_install:
|
||||
fi; \
|
||||
if [ -n "$$SOURCE_DIR" ]; then \
|
||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter && \
|
||||
( cd $$SOURCE_DIR && tar chf - --exclude=debian . ) | \
|
||||
( cd $$SOURCE_DIR && tar cf - --exclude=debian . ) | \
|
||||
( cd debian/dms-greeter/usr/share/quickshell/dms-greeter && tar xf - ) && \
|
||||
install -Dm755 $$SOURCE_DIR/Modules/Greetd/assets/dms-greeter \
|
||||
debian/dms-greeter/usr/bin/dms-greeter && \
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<service name="download_url">
|
||||
<param name="protocol">https</param>
|
||||
<param name="host">github.com</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.2.3/dms-source.tar.gz</param>
|
||||
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v1.2.3.tar.gz</param>
|
||||
<param name="filename">dms-source.tar.gz</param>
|
||||
</service>
|
||||
<!-- Download amd64 binary -->
|
||||
|
||||
@@ -63,7 +63,7 @@ override_dh_auto_install:
|
||||
mv "$$SOURCE_DIR" DankMaterialShell-$(UPSTREAM_VERSION); \
|
||||
fi
|
||||
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
|
||||
cp -rL DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
||||
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/systemd/dms.service debian/dms/usr/lib/systemd/user/dms.service; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/dms-open.desktop debian/dms/usr/share/applications/dms-open.desktop; \
|
||||
install -Dm644 DankMaterialShell-$(UPSTREAM_VERSION)/assets/com.danklinux.dms.desktop debian/dms/usr/share/applications/com.danklinux.dms.desktop; \
|
||||
|
||||
@@ -216,7 +216,7 @@ update_debian_dms_service() {
|
||||
return 0
|
||||
fi
|
||||
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-source\.tar\.gz|/releases/download/v${base_version}/dms-source.tar.gz|" "$service_path"
|
||||
sed -i "s|/archive/refs/tags/v[0-9][^\"]*\.tar\.gz|/archive/refs/tags/v${base_version}.tar.gz|" "$service_path"
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-amd64\.gz|/releases/download/v${base_version}/dms-distropkg-amd64.gz|" "$service_path"
|
||||
sed -i "s|/releases/download/v[0-9][^\"]*/dms-distropkg-arm64\.gz|/releases/download/v${base_version}/dms-distropkg-arm64.gz|" "$service_path"
|
||||
}
|
||||
@@ -542,8 +542,8 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
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 --recurse-submodules --shallow-submodules --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 --recurse-submodules --shallow-submodules "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
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
|
||||
@@ -586,7 +586,7 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
tar -czhf "$WORK_DIR/$SOURCE0" "$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)"
|
||||
;;
|
||||
@@ -604,7 +604,7 @@ if [[ "$UPLOAD_OPENSUSE" == true ]] && [[ "$UPLOAD_DEBIAN" == false ]] && [[ -f
|
||||
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
tar -czhf "$WORK_DIR/$SOURCE0" "$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)"
|
||||
|
||||
@@ -656,8 +656,8 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
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 --recurse-submodules --shallow-submodules --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
|
||||
git clone --depth 1 --recurse-submodules --shallow-submodules "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
|
||||
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
|
||||
@@ -820,11 +820,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
mkdir -p "$EXPECTED_DIR"
|
||||
cp -a "$SOURCE_DIR"/. "$EXPECTED_DIR/"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -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)"
|
||||
@@ -835,11 +835,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -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)"
|
||||
@@ -849,11 +849,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $EXPECTED_DIR)"
|
||||
cp -r "$SOURCE_DIR" "$EXPECTED_DIR"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -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)"
|
||||
@@ -863,11 +863,11 @@ if [[ "$UPLOAD_DEBIAN" == true ]] && [[ -d "distro/debian/$PACKAGE/debian" ]]; t
|
||||
echo " Creating $SOURCE0 (directory: $DIR_NAME)"
|
||||
cp -r "$SOURCE_DIR" "$DIR_NAME"
|
||||
if [[ "$SOURCE0" == *.tar.xz ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
else
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
|
||||
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -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)"
|
||||
|
||||
@@ -344,7 +344,7 @@ EOF
|
||||
|
||||
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/releases/download/v${VERSION}/dms-source.tar.gz"; then
|
||||
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"
|
||||
@@ -356,7 +356,7 @@ EOF
|
||||
info "Downloading source for dms-greeter..."
|
||||
if [ ! -f "dms-greeter-source.tar.gz" ]; then
|
||||
info "Downloading dms-greeter source..."
|
||||
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.tar.gz"; then
|
||||
if wget -O dms-greeter-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-greeter-source.tar.gz"
|
||||
@@ -386,7 +386,7 @@ if [ "$IS_GIT_PACKAGE" = true ] && [ -n "$GIT_REPO" ]; then
|
||||
|
||||
info "Cloning $GIT_REPO from GitHub (getting latest commit info)..."
|
||||
TEMP_CLONE=$(mktemp -d "$TEMP_BASE/ppa_clone_XXXXXX")
|
||||
if git clone --recurse-submodules --shallow-submodules "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
|
||||
if git clone "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
|
||||
GIT_COMMIT_HASH=$(cd "$TEMP_CLONE" && git rev-parse --short HEAD)
|
||||
GIT_COMMIT_COUNT=$(cd "$TEMP_CLONE" && git rev-list --count HEAD)
|
||||
UPSTREAM_VERSION=$(cd "$TEMP_CLONE" && git tag -l "v*" | sed 's/^v//' | sort -V | tail -1)
|
||||
|
||||
@@ -60,7 +60,7 @@ override_dh_auto_install:
|
||||
|
||||
# Install QML files from git clone
|
||||
mkdir -p debian/dms-git/usr/share/quickshell/dms
|
||||
cp -rL dms-git-repo/* 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
|
||||
|
||||
@@ -26,7 +26,7 @@ override_dh_auto_build:
|
||||
override_dh_auto_install:
|
||||
# Install greeter files to shared data location
|
||||
mkdir -p debian/dms-greeter/usr/share/quickshell/dms-greeter
|
||||
cp -rL DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
|
||||
cp -r DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
|
||||
|
||||
# Install launcher script
|
||||
install -Dm755 DankMaterialShell-$(BASE_VERSION)/quickshell/Modules/Greetd/assets/dms-greeter \
|
||||
|
||||
@@ -63,7 +63,7 @@ override_dh_auto_install:
|
||||
|
||||
# Install QML files from source tarball
|
||||
mkdir -p debian/dms/usr/share/quickshell/dms
|
||||
cp -rL DankMaterialShell-$(BASE_VERSION)/* 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
|
||||
|
||||
@@ -11,7 +11,7 @@ short_desc="DankMaterialShell greeter for greetd"
|
||||
maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://danklinux.com"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
|
||||
|
||||
depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri"
|
||||
|
||||
@@ -19,7 +19,7 @@ maintainer="AvengeMedia <AvengeMedia.US@gmail.com>"
|
||||
license="MIT"
|
||||
homepage="https://danklinux.com"
|
||||
changelog="https://github.com/AvengeMedia/DankMaterialShell/releases"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
|
||||
distfiles="https://github.com/AvengeMedia/DankMaterialShell/archive/refs/tags/v${version}.tar.gz"
|
||||
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
|
||||
|
||||
# Optional feature deps are listed in distro/void/README.md.
|
||||
|
||||
Generated
-17
@@ -1,21 +1,5 @@
|
||||
{
|
||||
"nodes": {
|
||||
"dank-qml-common": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1784332064,
|
||||
"narHash": "sha256-n0LKLirPnNSXYxElK1IpnybEwU0nnyso4kpsEFX88U0=",
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"rev": "5942bc1c60b4538940922b58112c8ae125880891",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "AvengeMedia",
|
||||
"repo": "dank-qml-common",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@@ -50,7 +34,6 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"dank-qml-common": "dank-qml-common",
|
||||
"flake-compat": "flake-compat",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
|
||||
@@ -7,17 +7,12 @@
|
||||
url = "github:NixOS/flake-compat";
|
||||
flake = false;
|
||||
};
|
||||
dank-qml-common = {
|
||||
url = "github:AvengeMedia/dank-qml-common";
|
||||
flake = false;
|
||||
};
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
dank-qml-common,
|
||||
...
|
||||
}:
|
||||
let
|
||||
@@ -130,10 +125,6 @@
|
||||
mkdir -p $out/share/quickshell/dms
|
||||
cp -r ${rootSrc}/quickshell/. $out/share/quickshell/dms/
|
||||
|
||||
rm -f $out/share/quickshell/dms/DankCommon
|
||||
cp -r ${dank-qml-common}/DankCommon $out/share/quickshell/dms/DankCommon
|
||||
chmod -R u+w $out/share/quickshell/dms/DankCommon
|
||||
|
||||
chmod u+w $out/share/quickshell/dms/VERSION
|
||||
echo "${version}" > $out/share/quickshell/dms/VERSION
|
||||
|
||||
|
||||
@@ -141,15 +141,13 @@ DankMaterialShell/
|
||||
│ │ ├── ipp/ # Internet Printing Protocol
|
||||
│ │ └── syncmap/ # Thread-safe map
|
||||
│ └── go.mod # Go module definition
|
||||
├── dank-qml-common/ # Shared widget library (git submodule)
|
||||
├── quickshell/ # QML frontend (UI layer) - see "QML Frontend Architecture" below
|
||||
│ ├── shell.qml # Main entry point
|
||||
│ ├── Services/ # IPC client wrappers
|
||||
│ ├── Modules/ # UI components
|
||||
│ ├── Widgets/ # Reusable controls
|
||||
│ ├── Modals/ # Full-screen overlays
|
||||
│ ├── Common/ # Shared resources
|
||||
│ └── DankCommon/ # Symlink to ../dank-qml-common/DankCommon
|
||||
│ └── Common/ # Shared resources
|
||||
├── distro/ # Distribution packaging
|
||||
│ ├── arch/ # AUR packages
|
||||
│ ├── fedora/ # RPM specs
|
||||
@@ -492,8 +490,6 @@ import qs.Services // For service access
|
||||
import qs.Widgets // For reusable widgets (DankIcon, etc.)
|
||||
```
|
||||
|
||||
Many widgets in `Widgets/`, `Common/`, and `Modals/FileBrowser/` are thin wrappers over the shared dank-qml-common library (`quickshell/DankCommon/`, a submodule symlink). Keep importing them through `qs.Widgets`, `qs.Common`, and `qs.Modals.FileBrowser` — the wrappers are the stable API for the shell and for plugins. Edit the implementations in `dank-qml-common/` (see CONTRIBUTING.md, "Shared widgets").
|
||||
|
||||
#### Go Import Order
|
||||
|
||||
Follow standard Go conventions:
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
Marble Tabby
|
||||
The Wolverine
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
|
||||
Singleton {
|
||||
readonly property var rounding: DankCommon.Appearance.rounding
|
||||
readonly property var spacing: DankCommon.Appearance.spacing
|
||||
readonly property var fontSize: DankCommon.Appearance.fontSize
|
||||
readonly property var anim: DankCommon.Appearance.anim
|
||||
id: root
|
||||
|
||||
readonly property Rounding rounding: Rounding {}
|
||||
readonly property Spacing spacing: Spacing {}
|
||||
readonly property FontSize fontSize: FontSize {}
|
||||
readonly property Anim anim: Anim {}
|
||||
|
||||
component Rounding: QtObject {
|
||||
readonly property int small: 8
|
||||
readonly property int normal: 12
|
||||
readonly property int large: 16
|
||||
readonly property int extraLarge: 24
|
||||
readonly property int full: 1000
|
||||
}
|
||||
|
||||
component Spacing: QtObject {
|
||||
readonly property int small: 4
|
||||
readonly property int normal: 8
|
||||
readonly property int large: 12
|
||||
readonly property int extraLarge: 16
|
||||
readonly property int huge: 24
|
||||
}
|
||||
|
||||
component FontSize: QtObject {
|
||||
readonly property int small: 12
|
||||
readonly property int normal: 14
|
||||
readonly property int large: 16
|
||||
readonly property int extraLarge: 20
|
||||
readonly property int huge: 24
|
||||
}
|
||||
|
||||
component AnimCurves: QtObject {
|
||||
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1
|
||||
/ 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1]
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1]
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1]
|
||||
}
|
||||
|
||||
component AnimDurations: QtObject {
|
||||
readonly property int quick: 150
|
||||
readonly property int normal: 300
|
||||
readonly property int slow: 500
|
||||
readonly property int extraSlow: 1000
|
||||
readonly property int expressiveFastSpatial: 350
|
||||
readonly property int expressiveDefaultSpatial: 500
|
||||
readonly property int expressiveEffects: 200
|
||||
}
|
||||
|
||||
component Anim: QtObject {
|
||||
readonly property AnimCurves curves: AnimCurves {}
|
||||
readonly property AnimDurations durations: AnimDurations {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
DankCommon.DankAnim {}
|
||||
// Reusable NumberAnimation wrapper
|
||||
NumberAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
DankCommon.DankColorAnim {}
|
||||
// Reusable ColorAnimation wrapper
|
||||
ColorAnimation {
|
||||
duration: Theme.expressiveDurations.normal
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
|
||||
@@ -1,3 +1,64 @@
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
DankCommon.DankSocket {}
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property alias path: socket.path
|
||||
property alias parser: socket.parser
|
||||
property bool connected: false
|
||||
property bool linkUp: false
|
||||
|
||||
property int reconnectBaseMs: 400
|
||||
property int reconnectMaxMs: 15000
|
||||
|
||||
property int _reconnectAttempt: 0
|
||||
|
||||
signal connectionStateChanged
|
||||
|
||||
onConnectedChanged: {
|
||||
socket.connected = connected;
|
||||
}
|
||||
|
||||
Socket {
|
||||
id: socket
|
||||
|
||||
onConnectionStateChanged: {
|
||||
root.linkUp = connected;
|
||||
root.connectionStateChanged();
|
||||
if (connected) {
|
||||
root._reconnectAttempt = 0;
|
||||
return;
|
||||
}
|
||||
if (root.connected) {
|
||||
root._scheduleReconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: reconnectTimer
|
||||
interval: 0
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
socket.connected = false;
|
||||
Qt.callLater(() => socket.connected = true);
|
||||
}
|
||||
}
|
||||
|
||||
function send(data) {
|
||||
const json = typeof data === "string" ? data : JSON.stringify(data);
|
||||
const message = json.endsWith("\n") ? json : json + "\n";
|
||||
socket.write(message);
|
||||
socket.flush();
|
||||
}
|
||||
|
||||
function _scheduleReconnect() {
|
||||
const pow = Math.min(_reconnectAttempt, 10);
|
||||
const base = Math.min(reconnectBaseMs * Math.pow(2, pow), reconnectMaxMs);
|
||||
const jitter = Math.floor(Math.random() * Math.floor(base / 4));
|
||||
reconnectTimer.interval = base + jitter;
|
||||
reconnectTimer.restart();
|
||||
_reconnectAttempt++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,54 @@
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
DankCommon.ElevationShadow {}
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property var level: Theme.elevationLevel2
|
||||
property string direction: Theme.elevationLightDirection
|
||||
property real fallbackOffset: 4
|
||||
|
||||
property color targetColor: "white"
|
||||
property real targetRadius: Theme.cornerRadius
|
||||
property real topLeftRadius: targetRadius
|
||||
property real topRightRadius: targetRadius
|
||||
property real bottomLeftRadius: targetRadius
|
||||
property real bottomRightRadius: targetRadius
|
||||
property color borderColor: "transparent"
|
||||
property real borderWidth: 0
|
||||
|
||||
property real sourceX: 0
|
||||
property real sourceY: 0
|
||||
property real sourceWidth: width
|
||||
property real sourceHeight: height
|
||||
|
||||
property bool shadowEnabled: Theme.elevationEnabled
|
||||
property real shadowBlurPx: level && level.blurPx !== undefined ? level.blurPx : 0
|
||||
property real shadowSpreadPx: level && level.spreadPx !== undefined ? level.spreadPx : 0
|
||||
property real shadowOffsetX: Theme.elevationOffsetXFor(level, direction, fallbackOffset)
|
||||
property real shadowOffsetY: Theme.elevationOffsetYFor(level, direction, fallbackOffset)
|
||||
property color shadowColor: Theme.elevationShadowColor(level)
|
||||
property real shadowOpacity: 1
|
||||
|
||||
readonly property var _ambient: Theme.elevationAmbient(level)
|
||||
readonly property real _pad: shadowEnabled ? Math.ceil(Math.max(shadowBlurPx + shadowSpreadPx + Math.max(Math.abs(shadowOffsetX), Math.abs(shadowOffsetY)), _ambient.blurPx + _ambient.spreadPx) + 2) : 0
|
||||
|
||||
ShaderEffect {
|
||||
anchors.fill: parent
|
||||
anchors.margins: -root._pad
|
||||
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/elevation_rect.frag.qsb")
|
||||
|
||||
property real widthPx: width
|
||||
property real heightPx: height
|
||||
property real borderWidth: root.borderWidth
|
||||
property vector4d rectPx: Qt.vector4d(root._pad + root.sourceX, root._pad + root.sourceY, root.sourceWidth, root.sourceHeight)
|
||||
property vector4d cornerRadius: Qt.vector4d(root.topLeftRadius, root.topRightRadius, root.bottomRightRadius, root.bottomLeftRadius)
|
||||
property vector4d fillColor: Qt.vector4d(root.targetColor.r, root.targetColor.g, root.targetColor.b, root.targetColor.a)
|
||||
property vector4d borderColor: Qt.vector4d(root.borderColor.r, root.borderColor.g, root.borderColor.b, root.borderColor.a)
|
||||
property vector4d shadowColor: Qt.vector4d(root.shadowColor.r, root.shadowColor.g, root.shadowColor.b, root.shadowEnabled ? root.shadowColor.a * root.shadowOpacity : 0)
|
||||
property vector4d shadowParam: Qt.vector4d(Math.max(0, root.shadowBlurPx), root.shadowSpreadPx, root.shadowOffsetX, root.shadowOffsetY)
|
||||
property vector4d ambientParam: Qt.vector4d(root._ambient.blurPx, root._ambient.spreadPx, root.shadowEnabled ? root._ambient.alpha * root.shadowOpacity : 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
+12
-78
@@ -25,7 +25,6 @@ Singleton {
|
||||
readonly property bool isRtl: _rtlLanguages.includes(_lang)
|
||||
|
||||
readonly property url translationsFolder: Qt.resolvedUrl("../translations/poexports")
|
||||
readonly property url commonTranslationsFolder: Qt.resolvedUrl("../DankCommon/translations/poexports")
|
||||
|
||||
readonly property alias folder: dir.folder
|
||||
property var presentLocales: ({
|
||||
@@ -33,11 +32,8 @@ Singleton {
|
||||
})
|
||||
property var translations: ({})
|
||||
property bool translationsLoaded: false
|
||||
property var commonTranslations: ({})
|
||||
property bool commonTranslationsLoaded: false
|
||||
|
||||
property url _selectedPath: ""
|
||||
property url _commonSelectedPath: ""
|
||||
|
||||
FolderListModel {
|
||||
id: dir
|
||||
@@ -52,18 +48,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
FolderListModel {
|
||||
id: commonDir
|
||||
folder: root.commonTranslationsFolder
|
||||
nameFilters: ["*.json"]
|
||||
showDirs: false
|
||||
showDotAndDotDot: false
|
||||
|
||||
onStatusChanged: if (status === FolderListModel.Ready) {
|
||||
root._pickCommonTranslation();
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: translationLoader
|
||||
path: root._selectedPath
|
||||
@@ -85,22 +69,6 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: commonTranslationLoader
|
||||
path: root._commonSelectedPath
|
||||
printErrors: false
|
||||
|
||||
onLoaded: {
|
||||
try {
|
||||
root.commonTranslations = JSON.parse(text());
|
||||
root.commonTranslationsLoaded = true;
|
||||
log.info(`I18n: Loaded DankCommon translations (${Object.keys(root.commonTranslations).length} contexts)`);
|
||||
} catch (e) {
|
||||
log.warn("I18n: Error parsing DankCommon translations:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function locale() {
|
||||
if (SessionData.timeLocale)
|
||||
return Qt.locale(SessionData.timeLocale);
|
||||
@@ -149,58 +117,24 @@ Singleton {
|
||||
log.warn("Falling back to built-in English strings");
|
||||
}
|
||||
|
||||
function _pickCommonTranslation() {
|
||||
const present = {};
|
||||
for (let i = 0; i < commonDir.count; i++) {
|
||||
const name = commonDir.get(i, "fileName");
|
||||
if (name && name.endsWith(".json"))
|
||||
present[name.slice(0, -5)] = true;
|
||||
}
|
||||
for (let i = 0; i < _candidates.length; i++) {
|
||||
if (!present[_candidates[i]])
|
||||
continue;
|
||||
_commonSelectedPath = commonTranslationsFolder + "/" + _candidates[i] + ".json";
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function _lookup(table, term, context) {
|
||||
if (!table)
|
||||
return "";
|
||||
if (context && table[context] && table[context][term])
|
||||
return table[context][term];
|
||||
if (table[term] && table[term][term])
|
||||
return table[term][term];
|
||||
for (const c in table) {
|
||||
if (table[c] && table[c][term])
|
||||
return table[c][term];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// isRealContext is consumed by translations/extract_translations.py only:
|
||||
// pass a literal `true` (same line) to give (term, context) its own POEditor
|
||||
// translation slot. Lookup ignores it -- a real context exists as a bucket
|
||||
// in the export, a comment-only context does not.
|
||||
function tr(term, context, isRealContext) {
|
||||
if (translationsLoaded) {
|
||||
const hit = _lookup(translations, term, context);
|
||||
if (hit)
|
||||
return hit;
|
||||
}
|
||||
if (commonTranslationsLoaded) {
|
||||
const hit = _lookup(commonTranslations, term, context);
|
||||
if (hit)
|
||||
return hit;
|
||||
function tr(term, context) {
|
||||
if (!translationsLoaded || !translations)
|
||||
return term;
|
||||
const ctx = context || term;
|
||||
if (translations[ctx] && translations[ctx][term])
|
||||
return translations[ctx][term];
|
||||
for (const c in translations) {
|
||||
if (translations[c] && translations[c][term])
|
||||
return translations[c][term];
|
||||
}
|
||||
return term;
|
||||
}
|
||||
|
||||
function trContext(context, term) {
|
||||
if (translationsLoaded && translations[context] && translations[context][term])
|
||||
if (!translationsLoaded || !translations)
|
||||
return term;
|
||||
if (translations[context] && translations[context][term])
|
||||
return translations[context][term];
|
||||
if (commonTranslationsLoaded && commonTranslations[context] && commonTranslations[context][term])
|
||||
return commonTranslations[context][term];
|
||||
return term;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,72 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import qs.Common
|
||||
|
||||
// Reusable ListView/GridView transitions
|
||||
Singleton {
|
||||
readonly property bool enabled: DankCommon.ListViewTransitions.enabled
|
||||
readonly property Transition add: DankCommon.ListViewTransitions.add
|
||||
readonly property Transition remove: DankCommon.ListViewTransitions.remove
|
||||
readonly property Transition displaced: DankCommon.ListViewTransitions.displaced
|
||||
readonly property Transition move: DankCommon.ListViewTransitions.move
|
||||
id: root
|
||||
|
||||
// 0ms ViewTransitions break ListView delegate cleanup, so null the set when the shortest
|
||||
// duration truncates to 0. Keep this gate - don't inline these back into add/remove/etc.
|
||||
readonly property bool enabled: Math.floor(Theme.currentAnimationBaseDuration * 0.4) >= 1
|
||||
|
||||
readonly property Transition add: enabled ? _add : null
|
||||
readonly property Transition remove: null
|
||||
readonly property Transition displaced: enabled ? _displaced : null
|
||||
readonly property Transition move: enabled ? _move : null
|
||||
|
||||
readonly property int _staggerMs: Math.round(Theme.currentAnimationBaseDuration * 0.03)
|
||||
readonly property int _staggerCap: 8
|
||||
|
||||
readonly property Transition _add: Transition {
|
||||
id: addTransition
|
||||
|
||||
SequentialAnimation {
|
||||
PropertyAction {
|
||||
property: "opacity"
|
||||
value: 0
|
||||
}
|
||||
PauseAnimation {
|
||||
duration: Math.max(0, Math.min(addTransition.ViewTransition.index - (addTransition.ViewTransition.targetIndexes[0] ?? 0), root._staggerCap)) * root._staggerMs
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Transition _displaced: Transition {
|
||||
DankAnim {
|
||||
property: "y"
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
readonly property Transition _move: Transition {
|
||||
DankAnim {
|
||||
property: "y"
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
DankAnim {
|
||||
property: "opacity"
|
||||
to: 1
|
||||
duration: Theme.expressiveDurations.fast
|
||||
easing.bezierCurve: Theme.expressiveCurves.standard
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,14 +108,6 @@ Singleton {
|
||||
return themedIconPath(iconName) || DesktopService.resolveIconPath(iconName);
|
||||
}
|
||||
|
||||
function trashPath(path: string, callback): void {
|
||||
TrashService.trashPath(path, callback);
|
||||
}
|
||||
|
||||
function copyPathToClipboard(path: string): void {
|
||||
Quickshell.execDetached([Proc.dmsBin, "cl", "copy", path]);
|
||||
}
|
||||
|
||||
function resolveIconUrl(iconName: string): string {
|
||||
if (!iconName)
|
||||
return "";
|
||||
|
||||
@@ -65,16 +65,35 @@ Singleton {
|
||||
|
||||
function _openPopout(popout) {
|
||||
if (popout.dashVisible !== undefined) {
|
||||
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing)
|
||||
let flagStayedTrue = popout.dashVisible === true;
|
||||
if (popout.dashVisible && !popout.shouldBeVisible && !popout.isClosing) {
|
||||
popout.dashVisible = false;
|
||||
flagStayedTrue = false;
|
||||
}
|
||||
popout.dashVisible = true;
|
||||
// Flag already true (e.g. retargeting to another monitor) won't re-fire
|
||||
// the change handler, so drive the surface open explicitly.
|
||||
if (flagStayedTrue)
|
||||
_ensureSurfaceOpen(popout);
|
||||
return;
|
||||
}
|
||||
if (popout.notificationHistoryVisible !== undefined) {
|
||||
const flagStayedTrue = popout.notificationHistoryVisible === true;
|
||||
popout.notificationHistoryVisible = true;
|
||||
if (flagStayedTrue)
|
||||
_ensureSurfaceOpen(popout);
|
||||
return;
|
||||
}
|
||||
popout.open();
|
||||
_ensureSurfaceOpen(popout);
|
||||
}
|
||||
|
||||
function _ensureSurfaceOpen(popout) {
|
||||
if (typeof popout.present === "function") {
|
||||
popout.present();
|
||||
return;
|
||||
}
|
||||
if (typeof popout.open === "function")
|
||||
popout.open();
|
||||
}
|
||||
|
||||
function _closePopout(popout) {
|
||||
|
||||
+148
-4
@@ -1,13 +1,157 @@
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import Quickshell.Io
|
||||
import qs.Services
|
||||
|
||||
Singleton {
|
||||
readonly property int noTimeout: DankCommon.Proc.noTimeout
|
||||
readonly property string dmsBin: DankCommon.Proc.dmsBin
|
||||
id: root
|
||||
readonly property var log: Log.scoped("Proc")
|
||||
|
||||
readonly property int noTimeout: -1
|
||||
readonly property string dmsBin: Quickshell.env("DMS_EXECUTABLE") || "dms"
|
||||
property int defaultDebounceMs: 50
|
||||
property int defaultTimeoutMs: 10000
|
||||
property var _procDebouncers: ({})
|
||||
|
||||
function runCommand(id, command, callback, debounceMs, timeoutMs) {
|
||||
DankCommon.Proc.runCommand(id, command, callback, debounceMs, timeoutMs);
|
||||
const wait = (typeof debounceMs === "number" && debounceMs >= 0) ? debounceMs : defaultDebounceMs;
|
||||
const timeout = (typeof timeoutMs === "number") ? timeoutMs : defaultTimeoutMs;
|
||||
let procId = id ? id : Math.random();
|
||||
const isRandomId = !id;
|
||||
|
||||
if (!_procDebouncers[procId]) {
|
||||
const t = debounceTimerComp.createObject(root);
|
||||
t.triggered.connect(function () {
|
||||
_launchProc(procId, isRandomId);
|
||||
});
|
||||
_procDebouncers[procId] = {
|
||||
timer: t,
|
||||
command: command,
|
||||
callback: callback,
|
||||
waitMs: wait,
|
||||
timeoutMs: timeout,
|
||||
isRandomId: isRandomId
|
||||
};
|
||||
} else {
|
||||
_procDebouncers[procId].command = command;
|
||||
_procDebouncers[procId].callback = callback;
|
||||
_procDebouncers[procId].waitMs = wait;
|
||||
_procDebouncers[procId].timeoutMs = timeout;
|
||||
}
|
||||
|
||||
const entry = _procDebouncers[procId];
|
||||
entry.timer.interval = entry.waitMs;
|
||||
entry.timer.restart();
|
||||
}
|
||||
|
||||
function _launchProc(id, isRandomId) {
|
||||
const entry = _procDebouncers[id];
|
||||
if (!entry)
|
||||
return;
|
||||
const proc = procComp.createObject(root, {
|
||||
command: entry.command
|
||||
});
|
||||
const timeoutTimer = debounceTimerComp.createObject(root);
|
||||
|
||||
let capturedOut = "";
|
||||
let capturedErr = "";
|
||||
let exitSeen = false;
|
||||
let exitCodeValue = -1;
|
||||
let outSeen = false;
|
||||
let errSeen = false;
|
||||
let timedOut = false;
|
||||
|
||||
timeoutTimer.interval = entry.timeoutMs;
|
||||
timeoutTimer.triggered.connect(function () {
|
||||
if (!exitSeen) {
|
||||
timedOut = true;
|
||||
proc.running = false;
|
||||
exitSeen = true;
|
||||
exitCodeValue = 124;
|
||||
maybeComplete();
|
||||
}
|
||||
});
|
||||
|
||||
proc.stdout.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedOut = proc.stdout.text || "";
|
||||
} catch (e) {
|
||||
capturedOut = "";
|
||||
}
|
||||
outSeen = true;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
proc.stderr.streamFinished.connect(function () {
|
||||
try {
|
||||
capturedErr = proc.stderr.text || "";
|
||||
} catch (e) {
|
||||
capturedErr = "";
|
||||
}
|
||||
errSeen = true;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
proc.exited.connect(function (code) {
|
||||
timeoutTimer.stop();
|
||||
exitSeen = true;
|
||||
exitCodeValue = code;
|
||||
maybeComplete();
|
||||
});
|
||||
|
||||
function maybeComplete() {
|
||||
if (!exitSeen || !outSeen || !errSeen)
|
||||
return;
|
||||
timeoutTimer.stop();
|
||||
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) {
|
||||
log.warn("runCommand callback error for command:", entry.command, "Error:", e);
|
||||
}
|
||||
}
|
||||
try {
|
||||
proc.destroy();
|
||||
} catch (_) {}
|
||||
try {
|
||||
timeoutTimer.destroy();
|
||||
} catch (_) {}
|
||||
|
||||
if (isRandomId || entry.isRandomId) {
|
||||
Qt.callLater(function () {
|
||||
if (_procDebouncers[id]) {
|
||||
try {
|
||||
_procDebouncers[id].timer.destroy();
|
||||
} catch (_) {}
|
||||
delete _procDebouncers[id];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
proc.running = true;
|
||||
if (entry.timeoutMs !== noTimeout)
|
||||
timeoutTimer.start();
|
||||
}
|
||||
|
||||
Component {
|
||||
id: debounceTimerComp
|
||||
Timer {
|
||||
repeat: false
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: procComp
|
||||
Process {
|
||||
running: false
|
||||
stdout: StdioCollector {}
|
||||
stderr: StdioCollector {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ Singleton {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse session.json - file will not be overwritten.");
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg));
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,7 +371,7 @@ Singleton {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse session.json - file will not be overwritten.");
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("session.json"), msg));
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse session.json"), msg));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -760,7 +760,6 @@ Singleton {
|
||||
property int batteryLowNotificationType: 0
|
||||
property int batteryCriticalNotificationType: 1
|
||||
property bool batteryAutoPowerSaver: false
|
||||
property bool lowerDisplayRefreshRateOnBattery: false
|
||||
property bool showBatteryPercent: true
|
||||
property bool showBatteryPercentOnlyOnBattery: false
|
||||
property bool showBatteryTime: false
|
||||
@@ -975,8 +974,6 @@ Singleton {
|
||||
property var hyprlandOutputSettings: ({})
|
||||
property var displayProfiles: ({})
|
||||
property var activeDisplayProfile: ({})
|
||||
property var activeDisplayProfileModes: ({})
|
||||
property var displayPreviousRefreshModes: ({})
|
||||
property bool displayProfileAutoSelect: false
|
||||
property bool displayShowDisconnected: false
|
||||
property bool displaySnapToEdge: true
|
||||
@@ -1799,7 +1796,7 @@ Singleton {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse settings.json - file will not be overwritten. Error:", msg);
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("settings.json"), msg));
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
|
||||
applyStoredTheme();
|
||||
} finally {
|
||||
_loading = false;
|
||||
@@ -1897,7 +1894,7 @@ Singleton {
|
||||
_pluginParseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to parse plugin_settings.json - file will not be overwritten. Error:", msg);
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("plugin_settings.json"), msg));
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse plugin_settings.json"), msg));
|
||||
pluginSettings = {};
|
||||
} finally {
|
||||
_pluginSettingsLoading = false;
|
||||
@@ -3564,27 +3561,6 @@ Singleton {
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setActiveDisplayProfileModes(compositor, modes) {
|
||||
if (JSON.stringify(activeDisplayProfileModes[compositor] || {}) === JSON.stringify(modes || {}))
|
||||
return;
|
||||
const updated = JSON.parse(JSON.stringify(activeDisplayProfileModes));
|
||||
updated[compositor] = modes;
|
||||
activeDisplayProfileModes = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
function setDisplayPreviousRefreshModes(compositor, modes) {
|
||||
if (JSON.stringify(displayPreviousRefreshModes[compositor] || {}) === JSON.stringify(modes || {}))
|
||||
return;
|
||||
const updated = JSON.parse(JSON.stringify(displayPreviousRefreshModes));
|
||||
if (Object.keys(modes || {}).length > 0)
|
||||
updated[compositor] = modes;
|
||||
else
|
||||
delete updated[compositor];
|
||||
displayPreviousRefreshModes = updated;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
ListModel {
|
||||
id: leftWidgetsModel
|
||||
}
|
||||
@@ -3680,7 +3656,7 @@ Singleton {
|
||||
_parseError = true;
|
||||
const msg = e.message;
|
||||
log.error("Failed to reload settings.json - file will not be overwritten. Error:", msg);
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse %1").arg("settings.json"), msg));
|
||||
Qt.callLater(() => ToastService.showError(I18n.tr("Failed to parse settings.json"), msg));
|
||||
} finally {
|
||||
_loading = false;
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import qs.Services
|
||||
import qs.Modules.Greetd
|
||||
import "StockThemes.js" as StockThemes
|
||||
@@ -1231,28 +1230,16 @@ Singleton {
|
||||
|
||||
property string fontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return resolvedFontFamily(GreetdSettings.getEffectiveFontFamily());
|
||||
return GreetdSettings.getEffectiveFontFamily();
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans;
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
|
||||
}
|
||||
|
||||
property string monoFontFamily: {
|
||||
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
|
||||
return resolvedMonoFontFamily(GreetdSettings.monoFontFamily);
|
||||
return GreetdSettings.monoFontFamily;
|
||||
}
|
||||
return typeof SettingsData !== "undefined" ? resolvedMonoFontFamily(SettingsData.monoFontFamily) : DankCommon.Fonts.mono;
|
||||
}
|
||||
|
||||
function resolvedFontFamily(family) {
|
||||
if (family === defaultFontFamily)
|
||||
return DankCommon.Fonts.sans;
|
||||
return family;
|
||||
}
|
||||
|
||||
function resolvedMonoFontFamily(family) {
|
||||
if (family === defaultMonoFontFamily)
|
||||
return DankCommon.Fonts.mono;
|
||||
return family;
|
||||
return typeof SettingsData !== "undefined" ? SettingsData.monoFontFamily : "Fira Code";
|
||||
}
|
||||
|
||||
property int fontWeight: {
|
||||
@@ -1973,7 +1960,7 @@ Singleton {
|
||||
function applyGtkColors() {
|
||||
if (!matugenAvailable) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors").arg("GTK"));
|
||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply GTK colors"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -1986,7 +1973,7 @@ Singleton {
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError(I18n.tr("Failed to apply %1 colors").arg("GTK"));
|
||||
ToastService.showError(I18n.tr("Failed to apply GTK colors"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -1995,7 +1982,7 @@ Singleton {
|
||||
function applyQtColors() {
|
||||
if (!matugenAvailable) {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply %1 colors").arg("Qt"));
|
||||
ToastService.showError(I18n.tr("matugen not available or disabled - cannot apply Qt colors"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -2007,7 +1994,7 @@ Singleton {
|
||||
}
|
||||
} else {
|
||||
if (typeof ToastService !== "undefined") {
|
||||
ToastService.showError(I18n.tr("Failed to apply %1 colors").arg("Qt"));
|
||||
ToastService.showError(I18n.tr("Failed to apply Qt colors"));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -604,7 +604,7 @@ Singleton {
|
||||
let details = out;
|
||||
if (err !== "")
|
||||
details = details !== "" ? details + "\n\nstderr:\n" + err : "stderr:\n" + err;
|
||||
ToastService.showInfo(I18n.tr("Authentication changes applied"), details, "", "auth-sync");
|
||||
ToastService.showInfo(I18n.tr("Authentication changes applied."), details, "", "auth-sync");
|
||||
root.detectAuthCapabilities();
|
||||
root.finishAuthApply();
|
||||
return;
|
||||
@@ -650,7 +650,7 @@ Singleton {
|
||||
|
||||
onExited: exitCode => {
|
||||
if (exitCode === 0) {
|
||||
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication there; it will close automatically when done.");
|
||||
const message = root.authApplyTerminalFallbackFromPrecheck ? I18n.tr("Terminal opened. Complete authentication setup there; it will close automatically when done.") : I18n.tr("Terminal fallback opened. Complete authentication setup there; it will close automatically when done.");
|
||||
ToastService.showInfo(message, "", "", "auth-sync");
|
||||
} else {
|
||||
let details = (root.authApplyTerminalFallbackStderr || "").trim();
|
||||
|
||||
@@ -347,7 +347,6 @@ var SPEC = {
|
||||
batteryLowNotificationType: { def: 0 },
|
||||
batteryCriticalNotificationType: { def: 1 },
|
||||
batteryAutoPowerSaver: { def: false },
|
||||
lowerDisplayRefreshRateOnBattery: { def: false },
|
||||
lockBeforeSuspend: { def: false },
|
||||
loginctlLockIntegration: { def: true },
|
||||
fadeToLockEnabled: { def: true },
|
||||
@@ -533,8 +532,6 @@ var SPEC = {
|
||||
hyprlandOutputSettings: { def: {} },
|
||||
displayProfiles: { def: {} },
|
||||
activeDisplayProfile: { def: {} },
|
||||
activeDisplayProfileModes: { def: {} },
|
||||
displayPreviousRefreshModes: { def: {} },
|
||||
displayProfileAutoSelect: { def: false },
|
||||
displayShowDisconnected: { def: false },
|
||||
displaySnapToEdge: { def: true },
|
||||
|
||||
@@ -139,6 +139,14 @@ Item {
|
||||
return;
|
||||
fadeDpmsWindowLoader.loadedWindow.cancelFade();
|
||||
}
|
||||
|
||||
function onMonitorsOffChanged() {
|
||||
if (IdleService.monitorsOff)
|
||||
return;
|
||||
if (!fadeDpmsWindowLoader.loadedWindow)
|
||||
return;
|
||||
fadeDpmsWindowLoader.loadedWindow.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,7 +331,7 @@ Item {
|
||||
target: TrashService
|
||||
function onEmptyTrashConfirmRequested(itemCount) {
|
||||
emptyTrashConfirm.showWithOptions({
|
||||
title: I18n.tr("Empty Trash"),
|
||||
title: I18n.tr("Empty Trash?"),
|
||||
message: I18n.tr("Permanently delete %1 item(s)? This cannot be undone.").arg(itemCount),
|
||||
confirmText: I18n.tr("Empty"),
|
||||
cancelText: I18n.tr("Cancel"),
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../dank-qml-common/DankCommon
|
||||
@@ -408,14 +408,14 @@ FocusScope {
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "↵ " + I18n.tr("Open")
|
||||
text: "↵ " + I18n.tr("open")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: "Tab " + I18n.tr("Actions")
|
||||
text: "Tab " + I18n.tr("actions")
|
||||
font.pixelSize: Theme.fontSizeSmall - 1
|
||||
color: Theme.surfaceVariantText
|
||||
visible: actionPanel.hasActions
|
||||
|
||||
@@ -156,7 +156,7 @@ Item {
|
||||
|
||||
function statusTitle() {
|
||||
if (root.controller?.isSearching || root.controller?.isFileSearching)
|
||||
return I18n.tr("Searching...");
|
||||
return I18n.tr("Searching");
|
||||
if ((root.controller?.searchMode ?? "") === "files" && !DSearchService.dsearchAvailable)
|
||||
return I18n.tr("File search unavailable");
|
||||
if ((root.controller?.searchMode ?? "") === "files" && (root.controller?.searchQuery?.length ?? 0) < 2)
|
||||
|
||||
@@ -1,3 +1,935 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import Qt.labs.folderlistmodel
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserContent {}
|
||||
FocusScope {
|
||||
id: root
|
||||
|
||||
LayoutMirroring.enabled: I18n.isRtl
|
||||
LayoutMirroring.childrenInherit: true
|
||||
|
||||
property string homeDir: StandardPaths.writableLocation(StandardPaths.HomeLocation)
|
||||
property string docsDir: StandardPaths.writableLocation(StandardPaths.DocumentsLocation)
|
||||
property string musicDir: StandardPaths.writableLocation(StandardPaths.MusicLocation)
|
||||
property string videosDir: StandardPaths.writableLocation(StandardPaths.MoviesLocation)
|
||||
property string picsDir: StandardPaths.writableLocation(StandardPaths.PicturesLocation)
|
||||
property string downloadDir: StandardPaths.writableLocation(StandardPaths.DownloadLocation)
|
||||
property string desktopDir: StandardPaths.writableLocation(StandardPaths.DesktopLocation)
|
||||
property string currentPath: ""
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: root.fileExtensions
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property bool showHiddenFiles: false
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool backButtonFocused: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property int keyboardSelectionIndex: -1
|
||||
property bool keyboardSelectionRequested: false
|
||||
property bool showKeyboardHints: false
|
||||
property bool showFileInfo: false
|
||||
property string selectedFilePath: ""
|
||||
property string selectedFileName: ""
|
||||
property bool selectedFileIsDir: false
|
||||
property bool showOverwriteConfirmation: false
|
||||
property string pendingFilePath: ""
|
||||
property bool showSidebar: true
|
||||
property string viewMode: "grid"
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property int iconSizeIndex: 1
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
property int actualGridColumns: 5
|
||||
property bool _initialized: false
|
||||
property bool closeOnEscape: true
|
||||
property var windowControls: null
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal closeRequested
|
||||
|
||||
function encodeFileUrl(path) {
|
||||
if (!path)
|
||||
return "";
|
||||
return "file://" + path.split('/').map(s => encodeURIComponent(s)).join('/');
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
loadSettings();
|
||||
currentPath = getLastPath();
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentPath = getLastPath();
|
||||
selectedIndex = -1;
|
||||
keyboardNavigationActive = false;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function loadSettings() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const isImageBrowser = ["wallpaper", "profile"].includes(browserType);
|
||||
|
||||
if (settings) {
|
||||
viewMode = settings.viewMode || (isImageBrowser ? "grid" : "list");
|
||||
sortBy = settings.sortBy || "name";
|
||||
sortAscending = settings.sortAscending !== undefined ? settings.sortAscending : true;
|
||||
iconSizeIndex = settings.iconSizeIndex !== undefined ? settings.iconSizeIndex : 1;
|
||||
showSidebar = settings.showSidebar !== undefined ? settings.showSidebar : true;
|
||||
} else {
|
||||
viewMode = isImageBrowser ? "grid" : "list";
|
||||
}
|
||||
}
|
||||
|
||||
function saveSettings() {
|
||||
if (!_initialized)
|
||||
return;
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].viewMode = viewMode;
|
||||
settings[type].sortBy = sortBy;
|
||||
settings[type].sortAscending = sortAscending;
|
||||
settings[type].iconSizeIndex = iconSizeIndex;
|
||||
settings[type].showSidebar = showSidebar;
|
||||
settings[type].lastPath = currentPath;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = currentPath;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = currentPath;
|
||||
}
|
||||
|
||||
CacheData.saveCache();
|
||||
}
|
||||
|
||||
onViewModeChanged: saveSettings()
|
||||
onSortByChanged: saveSettings()
|
||||
onSortAscendingChanged: saveSettings()
|
||||
onIconSizeIndexChanged: saveSettings()
|
||||
onShowSidebarChanged: saveSettings()
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName)
|
||||
return false;
|
||||
const ext = fileName.toLowerCase().split('.').pop();
|
||||
return ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(ext);
|
||||
}
|
||||
|
||||
function getLastPath() {
|
||||
const type = browserType || "default";
|
||||
const settings = CacheData.fileBrowserSettings[type];
|
||||
const lastPath = settings?.lastPath || "";
|
||||
return (lastPath && lastPath !== "") ? lastPath : homeDir;
|
||||
}
|
||||
|
||||
function saveLastPath(path) {
|
||||
const type = browserType || "default";
|
||||
let settings = CacheData.fileBrowserSettings;
|
||||
if (!settings[type]) {
|
||||
settings[type] = {};
|
||||
}
|
||||
settings[type].lastPath = path;
|
||||
CacheData.fileBrowserSettings = settings;
|
||||
CacheData.saveCache();
|
||||
|
||||
if (browserType === "wallpaper") {
|
||||
CacheData.wallpaperLastPath = path;
|
||||
} else if (browserType === "profile") {
|
||||
CacheData.profileLastPath = path;
|
||||
}
|
||||
}
|
||||
|
||||
function setSelectedFileData(path, name, isDir) {
|
||||
selectedFilePath = path;
|
||||
selectedFileName = name;
|
||||
selectedFileIsDir = isDir;
|
||||
}
|
||||
|
||||
function openItemContextMenu(sender, localX, localY, path, name, isDir) {
|
||||
if (!sender)
|
||||
return;
|
||||
const pos = sender.mapToItem(root, localX, localY);
|
||||
itemContextMenu.showAt(root, pos.x, pos.y, path, name, isDir);
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
const path = currentPath;
|
||||
if (path === homeDir)
|
||||
return;
|
||||
const lastSlash = path.lastIndexOf('/');
|
||||
if (lastSlash <= 0)
|
||||
return;
|
||||
const newPath = path.substring(0, lastSlash);
|
||||
if (newPath.length < homeDir.length) {
|
||||
currentPath = homeDir;
|
||||
saveLastPath(homeDir);
|
||||
} else {
|
||||
currentPath = newPath;
|
||||
saveLastPath(newPath);
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(path) {
|
||||
currentPath = path;
|
||||
saveLastPath(path);
|
||||
selectedIndex = -1;
|
||||
backButtonFocused = false;
|
||||
}
|
||||
|
||||
function keyboardFileSelection(index) {
|
||||
if (index < 0)
|
||||
return;
|
||||
keyboardSelectionTimer.targetIndex = index;
|
||||
keyboardSelectionTimer.start();
|
||||
}
|
||||
|
||||
function executeKeyboardSelection(index) {
|
||||
keyboardSelectionIndex = index;
|
||||
keyboardSelectionRequested = true;
|
||||
}
|
||||
|
||||
function activateFile(path, name, isDir) {
|
||||
if (isDir) {
|
||||
navigateTo(path);
|
||||
return;
|
||||
}
|
||||
if (saveMode) {
|
||||
saveRow.fileName = name;
|
||||
pendingFilePath = path;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(path);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
function handleSaveFile(filePath) {
|
||||
var normalizedPath = filePath;
|
||||
if (!normalizedPath.startsWith("file://")) {
|
||||
normalizedPath = encodeFileUrl(filePath);
|
||||
}
|
||||
|
||||
var exists = false;
|
||||
var fileName = filePath.split('/').pop();
|
||||
|
||||
for (var i = 0; i < folderModel.count; i++) {
|
||||
if (folderModel.get(i, "fileName") === fileName && !folderModel.get(i, "fileIsDir")) {
|
||||
exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (exists) {
|
||||
pendingFilePath = normalizedPath;
|
||||
showOverwriteConfirmation = true;
|
||||
} else {
|
||||
fileSelected(normalizedPath);
|
||||
closeRequested();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentPathChanged: {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
saveSettings();
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (selectedIndex >= 0 && folderModel && selectedIndex < folderModel.count) {
|
||||
selectedFilePath = "";
|
||||
selectedFileName = "";
|
||||
selectedFileIsDir = false;
|
||||
}
|
||||
}
|
||||
|
||||
property var steamPaths: [StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.steam/steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/.var/app/com.valvesoftware.Steam/.local/share/Steam/steamapps/workshop/content/431960", StandardPaths.writableLocation(StandardPaths.HomeLocation) + "/snap/steam/common/.local/share/Steam/steamapps/workshop/content/431960"]
|
||||
|
||||
property var quickAccessLocations: [
|
||||
{
|
||||
"name": I18n.tr("Home"),
|
||||
"path": homeDir,
|
||||
"icon": "home"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Documents"),
|
||||
"path": docsDir,
|
||||
"icon": "description"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Downloads"),
|
||||
"path": downloadDir,
|
||||
"icon": "download"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Pictures"),
|
||||
"path": picsDir,
|
||||
"icon": "image"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Music"),
|
||||
"path": musicDir,
|
||||
"icon": "music_note"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Videos"),
|
||||
"path": videosDir,
|
||||
"icon": "movie"
|
||||
},
|
||||
{
|
||||
"name": I18n.tr("Desktop"),
|
||||
"path": desktopDir,
|
||||
"icon": "computer"
|
||||
}
|
||||
]
|
||||
|
||||
FolderListModel {
|
||||
id: folderModel
|
||||
|
||||
showDirsFirst: true
|
||||
showDotAndDotDot: false
|
||||
showHidden: root.showHiddenFiles
|
||||
caseSensitive: false
|
||||
nameFilters: fileExtensions
|
||||
showFiles: true
|
||||
showDirs: true
|
||||
folder: encodeFileUrl(currentPath || homeDir)
|
||||
sortField: {
|
||||
switch (sortBy) {
|
||||
case "name":
|
||||
return FolderListModel.Name;
|
||||
case "size":
|
||||
return FolderListModel.Size;
|
||||
case "modified":
|
||||
return FolderListModel.Time;
|
||||
case "type":
|
||||
return FolderListModel.Type;
|
||||
default:
|
||||
return FolderListModel.Name;
|
||||
}
|
||||
}
|
||||
sortReversed: !sortAscending
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: keyboardController
|
||||
|
||||
property int totalItems: folderModel.count
|
||||
property int gridColumns: viewMode === "list" ? 1 : Math.max(1, actualGridColumns)
|
||||
|
||||
function handleKey(event) {
|
||||
if (event.key === Qt.Key_Escape && root.closeOnEscape) {
|
||||
closeRequested();
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F10) {
|
||||
showKeyboardHints = !showKeyboardHints;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_F1 || event.key === Qt.Key_I) {
|
||||
showFileInfo = !showFileInfo;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if ((event.modifiers & Qt.AltModifier && event.key === Qt.Key_Left) || event.key === Qt.Key_Backspace) {
|
||||
if (currentPath !== homeDir) {
|
||||
navigateUp();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!keyboardNavigationActive) {
|
||||
const isInitKey = event.key === Qt.Key_Tab || event.key === Qt.Key_Down || event.key === Qt.Key_Right || (event.key === Qt.Key_N && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_J && event.modifiers & Qt.ControlModifier) || (event.key === Qt.Key_L && event.modifiers & Qt.ControlModifier);
|
||||
|
||||
if (isInitKey) {
|
||||
keyboardNavigationActive = true;
|
||||
if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (event.key) {
|
||||
case Qt.Key_Tab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = 0;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Backtab:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = totalItems - 1;
|
||||
} else if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
} else {
|
||||
selectedIndex = totalItems - 1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_N:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_P:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_J:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_K:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_H:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (!backButtonFocused && selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_L:
|
||||
if (event.modifiers & Qt.ControlModifier) {
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
break;
|
||||
case Qt.Key_Left:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused)
|
||||
return;
|
||||
if (selectedIndex > 0) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Right:
|
||||
if (pathInputHasFocus)
|
||||
return;
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Up:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
if (gridColumns === 1) {
|
||||
selectedIndex = 0;
|
||||
} else {
|
||||
var col = selectedIndex % gridColumns;
|
||||
selectedIndex = Math.min(col, totalItems - 1);
|
||||
}
|
||||
} else if (selectedIndex >= gridColumns) {
|
||||
selectedIndex -= gridColumns;
|
||||
} else if (selectedIndex > 0 && gridColumns === 1) {
|
||||
selectedIndex--;
|
||||
} else if (currentPath !== homeDir) {
|
||||
backButtonFocused = true;
|
||||
selectedIndex = -1;
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Down:
|
||||
if (backButtonFocused) {
|
||||
backButtonFocused = false;
|
||||
selectedIndex = 0;
|
||||
} else if (gridColumns === 1) {
|
||||
if (selectedIndex < totalItems - 1) {
|
||||
selectedIndex++;
|
||||
}
|
||||
} else {
|
||||
var newIndex = selectedIndex + gridColumns;
|
||||
if (newIndex < totalItems) {
|
||||
selectedIndex = newIndex;
|
||||
} else {
|
||||
var lastRowStart = Math.floor((totalItems - 1) / gridColumns) * gridColumns;
|
||||
var col = selectedIndex % gridColumns;
|
||||
var targetIndex = lastRowStart + col;
|
||||
if (targetIndex < totalItems && targetIndex > selectedIndex) {
|
||||
selectedIndex = targetIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
case Qt.Key_Return:
|
||||
case Qt.Key_Enter:
|
||||
case Qt.Key_Space:
|
||||
if (backButtonFocused) {
|
||||
navigateUp();
|
||||
} else if (selectedIndex >= 0 && selectedIndex < totalItems) {
|
||||
root.keyboardFileSelection(selectedIndex);
|
||||
}
|
||||
event.accepted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: keyboardSelectionTimer
|
||||
|
||||
property int targetIndex: -1
|
||||
|
||||
interval: 1
|
||||
onTriggered: {
|
||||
executeKeyboardSelection(targetIndex);
|
||||
}
|
||||
}
|
||||
|
||||
focus: true
|
||||
|
||||
Keys.onPressed: event => {
|
||||
keyboardController.handleKey(event);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: 48
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: if (windowControls)
|
||||
windowControls.tryStartMove()
|
||||
onDoubleClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingL
|
||||
|
||||
DankIcon {
|
||||
name: browserIcon
|
||||
size: Theme.iconSizeLarge
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: browserTitle
|
||||
font.pixelSize: Theme.fontSizeXLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: showHiddenFiles ? "visibility_off" : "visibility"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: showHiddenFiles ? Theme.primary : Theme.surfaceText
|
||||
onClicked: showHiddenFiles = !showHiddenFiles
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: viewMode === "grid" ? "view_list" : "grid_view"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: viewMode = viewMode === "grid" ? "list" : "grid"
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: iconSizeIndex === 0 ? "photo_size_select_small" : iconSizeIndex === 1 ? "photo_size_select_large" : iconSizeIndex === 2 ? "photo_size_select_actual" : "zoom_in"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
visible: viewMode === "grid"
|
||||
onClicked: iconSizeIndex = (iconSizeIndex + 1) % iconSizes.length
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "info"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.showKeyboardHints = !root.showKeyboardHints
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
visible: windowControls?.supported ?? false
|
||||
circular: false
|
||||
iconName: windowControls?.targetWindow?.maximized ? "fullscreen_exit" : "fullscreen"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: if (windowControls)
|
||||
windowControls.tryToggleMaximize()
|
||||
}
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "close"
|
||||
iconSize: Theme.iconSize - 4
|
||||
iconColor: Theme.surfaceText
|
||||
onClicked: root.closeRequested()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
width: parent.width
|
||||
height: parent.height - 49
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.bottomMargin: root.saveMode ? 40 + Theme.spacingL * 2 : 0
|
||||
spacing: 0
|
||||
|
||||
Row {
|
||||
width: showSidebar ? 201 : 0
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
visible: showSidebar
|
||||
|
||||
FileBrowserSidebar {
|
||||
height: parent.height
|
||||
quickAccessLocations: root.quickAccessLocations
|
||||
currentPath: root.currentPath
|
||||
onLocationSelected: path => navigateTo(path)
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 1
|
||||
height: parent.height
|
||||
color: Theme.outline
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width - (showSidebar ? 201 : 0)
|
||||
height: parent.height
|
||||
spacing: 0
|
||||
|
||||
FileBrowserNavigation {
|
||||
width: parent.width
|
||||
currentPath: root.currentPath
|
||||
homeDir: root.homeDir
|
||||
backButtonFocused: root.backButtonFocused
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
showSidebar: root.showSidebar
|
||||
pathEditMode: root.pathEditMode
|
||||
onNavigateUp: root.navigateUp()
|
||||
onNavigateTo: path => root.navigateTo(path)
|
||||
onPathInputFocusChanged: hasFocus => {
|
||||
root.pathInputHasFocus = hasFocus;
|
||||
if (hasFocus) {
|
||||
root.pathEditMode = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: parent.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridContainer
|
||||
width: parent.width
|
||||
height: parent.height - 41
|
||||
clip: true
|
||||
|
||||
property real gridCellWidth: iconSizes[iconSizeIndex] + 24
|
||||
property real gridCellHeight: iconSizes[iconSizeIndex] + 56
|
||||
property real availableGridWidth: width - Theme.spacingM * 2
|
||||
property int gridColumns: Math.max(1, Math.floor(availableGridWidth / gridCellWidth))
|
||||
property real gridLeftMargin: Theme.spacingM + Math.max(0, (availableGridWidth - (gridColumns * gridCellWidth)) / 2)
|
||||
|
||||
onGridColumnsChanged: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
Component.onCompleted: {
|
||||
root.actualGridColumns = gridColumns;
|
||||
}
|
||||
|
||||
DankGridView {
|
||||
id: fileGrid
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: gridContainer.gridLeftMargin
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "grid"
|
||||
cellWidth: gridContainer.gridCellWidth
|
||||
cellHeight: gridContainer.gridCellHeight
|
||||
cacheBuffer: 260
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: gridScrollbar
|
||||
}
|
||||
|
||||
ScrollBar.horizontal: DankScrollbar {
|
||||
policy: ScrollBar.AlwaysOff
|
||||
}
|
||||
|
||||
delegate: FileBrowserGridDelegate {
|
||||
iconSizes: root.iconSizes
|
||||
iconSizeIndex: root.iconSizeIndex
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankListView {
|
||||
id: fileList
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
anchors.rightMargin: Theme.spacingM
|
||||
anchors.topMargin: Theme.spacingS
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
visible: viewMode === "list"
|
||||
spacing: Theme.spacingXXS
|
||||
model: folderModel
|
||||
currentIndex: selectedIndex
|
||||
onCurrentIndexChanged: {
|
||||
if (keyboardNavigationActive && currentIndex >= 0)
|
||||
positionViewAtIndex(currentIndex, ListView.Contain);
|
||||
}
|
||||
|
||||
ScrollBar.vertical: DankScrollbar {
|
||||
id: listScrollbar
|
||||
}
|
||||
|
||||
delegate: FileBrowserListDelegate {
|
||||
width: fileList.width
|
||||
selectedIndex: root.selectedIndex
|
||||
keyboardNavigationActive: root.keyboardNavigationActive
|
||||
onItemClicked: (index, path, name, isDir) => {
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(path, name, isDir);
|
||||
root.activateFile(path, name, isDir);
|
||||
}
|
||||
onItemSelected: (index, path, name, isDir) => {
|
||||
setSelectedFileData(path, name, isDir);
|
||||
}
|
||||
onItemContextMenuRequested: (sender, localX, localY, path, name, isDir) => {
|
||||
root.openItemContextMenu(sender, localX, localY, path, name, isDir);
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onKeyboardSelectionRequestedChanged() {
|
||||
if (root.keyboardSelectionRequested && root.keyboardSelectionIndex === index) {
|
||||
root.keyboardSelectionRequested = false;
|
||||
selectedIndex = index;
|
||||
setSelectedFileData(filePath, fileName, fileIsDir);
|
||||
root.activateFile(filePath, fileName, fileIsDir);
|
||||
}
|
||||
}
|
||||
|
||||
target: root
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSaveRow {
|
||||
id: saveRow
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
saveMode: root.saveMode
|
||||
defaultFileName: root.defaultFileName
|
||||
currentPath: root.currentPath
|
||||
onSaveRequested: filePath => handleSaveFile(filePath)
|
||||
}
|
||||
|
||||
KeyboardHints {
|
||||
id: keyboardHints
|
||||
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
showHints: root.showKeyboardHints
|
||||
}
|
||||
|
||||
FileInfo {
|
||||
id: fileInfo
|
||||
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingL
|
||||
width: 300
|
||||
showFileInfo: root.showFileInfo
|
||||
selectedIndex: root.selectedIndex
|
||||
sourceFolderModel: folderModel
|
||||
currentPath: root.currentPath
|
||||
currentFileName: root.selectedFileName
|
||||
currentFileIsDir: root.selectedFileIsDir
|
||||
currentFileExtension: {
|
||||
if (root.selectedFileIsDir || !root.selectedFileName)
|
||||
return "";
|
||||
|
||||
var lastDot = root.selectedFileName.lastIndexOf('.');
|
||||
return lastDot > 0 ? root.selectedFileName.substring(lastDot + 1).toLowerCase() : "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserSortMenu {
|
||||
id: sortMenu
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.topMargin: 120
|
||||
anchors.rightMargin: Theme.spacingL
|
||||
sortBy: root.sortBy
|
||||
sortAscending: root.sortAscending
|
||||
onSortBySelected: value => {
|
||||
root.sortBy = value;
|
||||
}
|
||||
onSortOrderSelected: ascending => {
|
||||
root.sortAscending = ascending;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserOverwriteDialog {
|
||||
anchors.fill: parent
|
||||
showDialog: showOverwriteConfirmation
|
||||
pendingFilePath: root.pendingFilePath
|
||||
onConfirmed: filePath => {
|
||||
showOverwriteConfirmation = false;
|
||||
fileSelected(filePath);
|
||||
pendingFilePath = "";
|
||||
Qt.callLater(() => root.closeRequested());
|
||||
}
|
||||
onCancelled: {
|
||||
showOverwriteConfirmation = false;
|
||||
pendingFilePath = "";
|
||||
}
|
||||
}
|
||||
|
||||
FileBrowserItemContextMenu {
|
||||
id: itemContextMenu
|
||||
parentFocusItem: root
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,252 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserGridDelegate {}
|
||||
StyledRect {
|
||||
id: delegateRoot
|
||||
|
||||
required property bool fileIsDir
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property int index
|
||||
|
||||
property bool weMode: false
|
||||
property var iconSizes: [80, 120, 160, 200]
|
||||
property int iconSizeIndex: 1
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
|
||||
signal itemClicked(int index, string path, string name, bool isDir)
|
||||
signal itemSelected(int index, string path, string name, bool isDir)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(delegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(delegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string _thumbnailSize: iconSizeIndex >= 2 ? "x-large" : "large"
|
||||
property int _thumbnailPx: iconSizeIndex >= 2 ? 512 : 256
|
||||
property string videoThumbnailPath: {
|
||||
if (!delegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + delegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/" + _thumbnailSize + "/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/" + _thumbnailSize;
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s " + _thumbnailPx + " -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, delegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
width: weMode ? 245 : iconSizes[iconSizeIndex] + 16
|
||||
height: weMode ? 205 : iconSizes[iconSizeIndex] + 48
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
|
||||
return mouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && delegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: (keyboardNavigationActive && delegateRoot.index === selectedIndex) ? 2 : 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (keyboardNavigationActive && delegateRoot.index === selectedIndex)
|
||||
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (keyboardNavigationActive && selectedIndex === delegateRoot.index)
|
||||
itemSelected(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: weMode ? 225 : (iconSizes[iconSizeIndex] - 8)
|
||||
height: weMode ? 165 : (iconSizes[iconSizeIndex] - 8)
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
anchors.margins: 2
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: gridPreviewImage
|
||||
anchors.fill: parent
|
||||
property var weExtensions: [".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tga", ".jxl", ".avif", ".heif", ".exr"]
|
||||
property int weExtIndex: 0
|
||||
property string imagePath: {
|
||||
if (weMode && delegateRoot.fileIsDir)
|
||||
return delegateRoot.filePath + "/preview" + weExtensions[weExtIndex];
|
||||
if (_videoThumb)
|
||||
return _videoThumb;
|
||||
return "";
|
||||
}
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
onStatusChanged: {
|
||||
if (status !== Image.Error)
|
||||
return;
|
||||
if (weMode && delegateRoot.fileIsDir) {
|
||||
if (weExtIndex < weExtensions.length - 1) {
|
||||
weExtIndex++;
|
||||
} else {
|
||||
imagePath = "";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (_videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
sourceSize.height: weMode ? 225 : iconSizes[iconSizeIndex]
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && ((!delegateRoot.fileIsDir && isVideo) || (weMode && delegateRoot.fileIsDir))
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !delegateRoot.fileIsDir && isImage ? delegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !delegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: delegateRoot.fileIsDir ? "folder" : getIconForFile(delegateRoot.fileName)
|
||||
size: iconSizes[iconSizeIndex] * 0.45
|
||||
color: delegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: (!delegateRoot.fileIsDir && !isImage && !(isVideo && gridPreviewImage.status === Image.Ready)) || (delegateRoot.fileIsDir && !weMode)
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: delegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: delegateRoot.width - Theme.spacingM
|
||||
elide: Text.ElideRight
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
maximumLineCount: 2
|
||||
wrapMode: Text.Wrap
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(delegateRoot.index, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(delegateRoot, mouse.x, mouse.y, delegateRoot.filePath, delegateRoot.fileName, delegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,153 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserItemContextMenu {}
|
||||
Popup {
|
||||
id: root
|
||||
|
||||
property string filePath: ""
|
||||
property string fileName: ""
|
||||
property bool fileIsDir: false
|
||||
property var parentFocusItem: null
|
||||
|
||||
signal trashed
|
||||
signal menuClosed
|
||||
|
||||
readonly property var menuItems: [
|
||||
{
|
||||
text: I18n.tr("Move to Trash"),
|
||||
icon: "delete",
|
||||
action: trashItem,
|
||||
enabled: filePath.length > 0,
|
||||
dangerous: true
|
||||
},
|
||||
{
|
||||
text: I18n.tr("Copy path"),
|
||||
icon: "content_copy",
|
||||
action: copyPath,
|
||||
enabled: filePath.length > 0
|
||||
}
|
||||
]
|
||||
|
||||
function showAt(parentItem, localX, localY, path, name, isDir) {
|
||||
if (!parentItem)
|
||||
return;
|
||||
parent = parentItem;
|
||||
filePath = path || "";
|
||||
fileName = name || "";
|
||||
fileIsDir = !!isDir;
|
||||
x = Math.max(0, Math.min(parentItem.width - width, localX));
|
||||
y = Math.max(0, Math.min(parentItem.height - height, localY));
|
||||
open();
|
||||
}
|
||||
|
||||
function trashItem() {
|
||||
if (!filePath)
|
||||
return;
|
||||
TrashService.trashPath(filePath, ok => {
|
||||
if (ok)
|
||||
root.trashed();
|
||||
});
|
||||
close();
|
||||
}
|
||||
|
||||
function copyPath() {
|
||||
if (!filePath)
|
||||
return;
|
||||
Quickshell.execDetached(["dms", "cl", "copy", filePath]);
|
||||
close();
|
||||
}
|
||||
|
||||
width: 220
|
||||
height: menuColumn.implicitHeight + Theme.spacingS * 2
|
||||
padding: 0
|
||||
modal: false
|
||||
closePolicy: Popup.CloseOnEscape
|
||||
|
||||
onClosed: {
|
||||
closePolicy = Popup.CloseOnEscape;
|
||||
menuClosed();
|
||||
if (parentFocusItem)
|
||||
Qt.callLater(() => parentFocusItem.forceActiveFocus());
|
||||
}
|
||||
|
||||
onOpened: outsideClickTimer.start()
|
||||
|
||||
Timer {
|
||||
id: outsideClickTimer
|
||||
interval: 100
|
||||
onTriggered: root.closePolicy = Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
||||
}
|
||||
|
||||
background: Rectangle {
|
||||
color: "transparent"
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
color: Theme.floatingSurface
|
||||
radius: Theme.cornerRadius
|
||||
border.color: BlurService.borderColor
|
||||
border.width: BlurService.borderWidth
|
||||
|
||||
Column {
|
||||
id: menuColumn
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: 1
|
||||
|
||||
Repeater {
|
||||
model: root.menuItems
|
||||
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
opacity: modelData.enabled ? 1 : 0.5
|
||||
color: {
|
||||
if (!modelData.enabled || !area.containsMouse)
|
||||
return "transparent";
|
||||
if (modelData.dangerous)
|
||||
return Theme.errorHover;
|
||||
return BlurService.hoverColor(Theme.widgetBaseHoverColor);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.left: parent.left
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.right: parent.right
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
name: modelData.icon
|
||||
size: 16
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
}
|
||||
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: modelData.text
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: modelData.dangerous && area.containsMouse && modelData.enabled ? Theme.error : Theme.surfaceText
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: area
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: modelData.enabled
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: modelData.action()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,256 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import Quickshell.Widgets
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserListDelegate {}
|
||||
StyledRect {
|
||||
id: listDelegateRoot
|
||||
|
||||
required property bool fileIsDir
|
||||
required property string filePath
|
||||
required property string fileName
|
||||
required property int index
|
||||
required property var fileModified
|
||||
required property int fileSize
|
||||
|
||||
property int selectedIndex: -1
|
||||
property bool keyboardNavigationActive: false
|
||||
|
||||
signal itemClicked(int index, string path, string name, bool isDir)
|
||||
signal itemSelected(int index, string path, string name, bool isDir)
|
||||
signal itemContextMenuRequested(var sender, real localX, real localY, string path, string name, bool isDir)
|
||||
|
||||
function getFileExtension(fileName) {
|
||||
const parts = fileName.split('.');
|
||||
if (parts.length > 1) {
|
||||
return parts[parts.length - 1].toLowerCase();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function determineFileType(fileName) {
|
||||
const ext = getFileExtension(fileName);
|
||||
|
||||
const imageExts = ["png", "jpg", "jpeg", "gif", "bmp", "webp", "svg", "ico", "jxl", "avif", "heif", "exr"];
|
||||
if (imageExts.includes(ext)) {
|
||||
return "image";
|
||||
}
|
||||
|
||||
const videoExts = ["mp4", "mkv", "avi", "mov", "webm", "flv", "wmv", "m4v"];
|
||||
if (videoExts.includes(ext)) {
|
||||
return "video";
|
||||
}
|
||||
|
||||
const audioExts = ["mp3", "wav", "flac", "ogg", "m4a", "aac", "wma"];
|
||||
if (audioExts.includes(ext)) {
|
||||
return "audio";
|
||||
}
|
||||
|
||||
const codeExts = ["js", "ts", "jsx", "tsx", "py", "go", "rs", "c", "cpp", "h", "java", "kt", "swift", "rb", "php", "html", "css", "scss", "json", "xml", "yaml", "yml", "toml", "sh", "bash", "zsh", "fish", "qml", "vue", "svelte"];
|
||||
if (codeExts.includes(ext)) {
|
||||
return "code";
|
||||
}
|
||||
|
||||
const docExts = ["txt", "md", "pdf", "doc", "docx", "odt", "rtf"];
|
||||
if (docExts.includes(ext)) {
|
||||
return "document";
|
||||
}
|
||||
|
||||
const archiveExts = ["zip", "tar", "gz", "bz2", "xz", "7z", "rar"];
|
||||
if (archiveExts.includes(ext)) {
|
||||
return "archive";
|
||||
}
|
||||
|
||||
if (!ext || fileName.indexOf('.') === -1) {
|
||||
return "binary";
|
||||
}
|
||||
|
||||
return "file";
|
||||
}
|
||||
|
||||
function isImageFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "image";
|
||||
}
|
||||
|
||||
function isVideoFile(fileName) {
|
||||
if (!fileName) {
|
||||
return false;
|
||||
}
|
||||
return determineFileType(fileName) === "video";
|
||||
}
|
||||
|
||||
property bool isImage: isImageFile(listDelegateRoot.fileName)
|
||||
property bool isVideo: isVideoFile(listDelegateRoot.fileName)
|
||||
|
||||
property string _xdgCacheHome: Paths.strip(Paths.xdgCache)
|
||||
property string videoThumbnailPath: {
|
||||
if (!listDelegateRoot.fileIsDir && isVideo) {
|
||||
const hash = Qt.md5("file://" + listDelegateRoot.filePath);
|
||||
return _xdgCacheHome + "/thumbnails/normal/" + hash + ".png";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
property string _videoThumb: ""
|
||||
property bool _thumbGenAttempted: false
|
||||
|
||||
// Probe the thumbnail optimistically; Image.Error triggers generation
|
||||
onVideoThumbnailPathChanged: {
|
||||
_thumbGenAttempted = false;
|
||||
_videoThumb = videoThumbnailPath;
|
||||
}
|
||||
|
||||
function generateVideoThumbnail() {
|
||||
if (_thumbGenAttempted)
|
||||
return;
|
||||
_thumbGenAttempted = true;
|
||||
_videoThumb = "";
|
||||
const thumbPath = videoThumbnailPath;
|
||||
const thumbDir = _xdgCacheHome + "/thumbnails/normal";
|
||||
const script = "mkdir -p \"$1\" && ffmpegthumbnailer -i \"$2\" -o \"$3\" -s 128 -f";
|
||||
Proc.runCommand(null, ["sh", "-c", script, "thumb", thumbDir, listDelegateRoot.filePath, thumbPath], function (output, exitCode) {
|
||||
if (exitCode === 0)
|
||||
_videoThumb = thumbPath;
|
||||
});
|
||||
}
|
||||
|
||||
function getIconForFile(fileName) {
|
||||
const lowerName = fileName.toLowerCase();
|
||||
if (lowerName.startsWith("dockerfile")) {
|
||||
return "docker";
|
||||
}
|
||||
const ext = fileName.split('.').pop();
|
||||
return ext || "";
|
||||
}
|
||||
|
||||
function formatFileSize(size) {
|
||||
if (size < 1024)
|
||||
return size + " B";
|
||||
if (size < 1024 * 1024)
|
||||
return (size / 1024).toFixed(1) + " KB";
|
||||
if (size < 1024 * 1024 * 1024)
|
||||
return (size / (1024 * 1024)).toFixed(1) + " MB";
|
||||
return (size / (1024 * 1024 * 1024)).toFixed(1) + " GB";
|
||||
}
|
||||
|
||||
height: 44
|
||||
radius: Theme.cornerRadius
|
||||
color: {
|
||||
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
|
||||
return Theme.surfacePressed;
|
||||
return listMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainerHigh, 0);
|
||||
}
|
||||
border.color: keyboardNavigationActive && listDelegateRoot.index === selectedIndex ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: (keyboardNavigationActive && listDelegateRoot.index === selectedIndex) ? 2 : 0
|
||||
|
||||
Component.onCompleted: {
|
||||
if (keyboardNavigationActive && listDelegateRoot.index === selectedIndex)
|
||||
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
onSelectedIndexChanged: {
|
||||
if (keyboardNavigationActive && selectedIndex === listDelegateRoot.index)
|
||||
itemSelected(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
Item {
|
||||
width: 28
|
||||
height: 28
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
ClippingRectangle {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: "transparent"
|
||||
|
||||
Image {
|
||||
id: listPreviewImage
|
||||
anchors.fill: parent
|
||||
property string imagePath: _videoThumb
|
||||
source: imagePath ? "file://" + imagePath.split('/').map(s => encodeURIComponent(s)).join('/') : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
sourceSize.width: 32
|
||||
sourceSize.height: 32
|
||||
asynchronous: true
|
||||
visible: status === Image.Ready && !listDelegateRoot.fileIsDir && isVideo
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error && _videoThumb)
|
||||
generateVideoThumbnail();
|
||||
}
|
||||
}
|
||||
|
||||
CachingImage {
|
||||
anchors.fill: parent
|
||||
imagePath: !listDelegateRoot.fileIsDir && isImage ? listDelegateRoot.filePath : ""
|
||||
maxCacheSize: 256
|
||||
animate: false
|
||||
visible: !listDelegateRoot.fileIsDir && isImage
|
||||
}
|
||||
}
|
||||
|
||||
DankNFIcon {
|
||||
anchors.centerIn: parent
|
||||
name: listDelegateRoot.fileIsDir ? "folder" : getIconForFile(listDelegateRoot.fileName)
|
||||
size: Theme.iconSize - 2
|
||||
color: listDelegateRoot.fileIsDir ? Theme.primary : Theme.surfaceText
|
||||
visible: listDelegateRoot.fileIsDir || (!isImage && !(isVideo && listPreviewImage.status === Image.Ready))
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileName || ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width - 280
|
||||
elide: Text.ElideRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
maximumLineCount: 1
|
||||
clip: true
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: listDelegateRoot.fileIsDir ? "" : formatFileSize(listDelegateRoot.fileSize)
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 70
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: Qt.formatDateTime(listDelegateRoot.fileModified, "MMM d, yyyy h:mm AP")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: 140
|
||||
horizontalAlignment: Text.AlignRight
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: listMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
onClicked: mouse => {
|
||||
switch (mouse.button) {
|
||||
case Qt.LeftButton:
|
||||
itemClicked(listDelegateRoot.index, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
case Qt.RightButton:
|
||||
itemContextMenuRequested(listDelegateRoot, mouse.x, mouse.y, listDelegateRoot.filePath, listDelegateRoot.fileName, listDelegateRoot.fileIsDir);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,100 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserModal {}
|
||||
FloatingWindow {
|
||||
id: fileBrowserModal
|
||||
|
||||
property bool disablePopupTransparency: true
|
||||
property string browserTitle: "Select File"
|
||||
property string browserIcon: "folder_open"
|
||||
property string browserType: "generic"
|
||||
property var fileExtensions: ["*.*"]
|
||||
property alias filterExtensions: fileBrowserModal.fileExtensions
|
||||
property bool showHiddenFiles: false
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property var parentModal: null
|
||||
parentWindow: parentModal
|
||||
property bool shouldHaveFocus: visible
|
||||
property bool allowFocusOverride: false
|
||||
property bool shouldBeVisible: visible
|
||||
property bool allowStacking: true
|
||||
|
||||
signal fileSelected(string path)
|
||||
signal dialogClosed
|
||||
|
||||
function open() {
|
||||
visible = true;
|
||||
}
|
||||
|
||||
function close() {
|
||||
visible = false;
|
||||
}
|
||||
|
||||
objectName: "fileBrowserModal"
|
||||
title: "Files - " + browserTitle
|
||||
minimumSize: Qt.size(500, 400)
|
||||
implicitWidth: 800
|
||||
implicitHeight: 600
|
||||
color: Theme.surfaceContainer
|
||||
visible: false
|
||||
|
||||
onClosed: close()
|
||||
|
||||
onVisibleChanged: {
|
||||
if (visible) {
|
||||
if (parentModal && "shouldHaveFocus" in parentModal) {
|
||||
parentModal.shouldHaveFocus = false;
|
||||
parentModal.allowFocusOverride = true;
|
||||
}
|
||||
Qt.callLater(() => {
|
||||
if (content) {
|
||||
content.reset();
|
||||
content.forceActiveFocus();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (parentModal && "allowFocusOverride" in parentModal) {
|
||||
parentModal.allowFocusOverride = false;
|
||||
parentModal.shouldHaveFocus = Qt.binding(() => parentModal.shouldBeVisible);
|
||||
}
|
||||
dialogClosed();
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: contentLoader
|
||||
anchors.fill: parent
|
||||
active: fileBrowserModal.visible
|
||||
sourceComponent: FileBrowserContent {
|
||||
id: content
|
||||
anchors.fill: parent
|
||||
focus: true
|
||||
closeOnEscape: false
|
||||
windowControls: fileBrowserModal.windowControlsRef
|
||||
|
||||
browserTitle: fileBrowserModal.browserTitle
|
||||
browserIcon: fileBrowserModal.browserIcon
|
||||
browserType: fileBrowserModal.browserType
|
||||
fileExtensions: fileBrowserModal.fileExtensions
|
||||
showHiddenFiles: fileBrowserModal.showHiddenFiles
|
||||
saveMode: fileBrowserModal.saveMode
|
||||
defaultFileName: fileBrowserModal.defaultFileName
|
||||
|
||||
Component.onCompleted: initialize()
|
||||
|
||||
onFileSelected: path => fileBrowserModal.fileSelected(path)
|
||||
onCloseRequested: fileBrowserModal.close()
|
||||
}
|
||||
}
|
||||
|
||||
property alias content: contentLoader.item
|
||||
property alias windowControlsRef: windowControls
|
||||
|
||||
FloatingWindowControls {
|
||||
id: windowControls
|
||||
targetWindow: fileBrowserModal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,130 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserNavigation {}
|
||||
Row {
|
||||
id: navigation
|
||||
|
||||
property string currentPath: ""
|
||||
property string homeDir: ""
|
||||
property bool backButtonFocused: false
|
||||
property bool keyboardNavigationActive: false
|
||||
property bool showSidebar: true
|
||||
property bool pathEditMode: false
|
||||
property bool pathInputHasFocus: false
|
||||
|
||||
signal navigateUp
|
||||
signal navigateTo(string path)
|
||||
signal pathInputFocusChanged(bool hasFocus)
|
||||
|
||||
height: 40
|
||||
leftPadding: Theme.spacingM
|
||||
rightPadding: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
StyledRect {
|
||||
width: 32
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: (backButtonMouseArea.containsMouse || (backButtonFocused && keyboardNavigationActive)) && currentPath !== homeDir ? Theme.surfaceVariant : Theme.withAlpha(Theme.surfaceVariant, 0)
|
||||
opacity: currentPath !== homeDir ? 1 : 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankIcon {
|
||||
anchors.centerIn: parent
|
||||
name: "arrow_back"
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.surfaceText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: backButtonMouseArea
|
||||
|
||||
anchors.fill: parent
|
||||
hoverEnabled: currentPath !== homeDir
|
||||
cursorShape: currentPath !== homeDir ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
enabled: currentPath !== homeDir
|
||||
onClicked: navigation.navigateUp()
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: Math.max(0, (parent?.width ?? 0) - 40 - Theme.spacingS - (showSidebar ? 0 : 80))
|
||||
height: 32
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
StyledRect {
|
||||
anchors.fill: parent
|
||||
radius: Theme.cornerRadius
|
||||
color: pathEditMode ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.withAlpha(Theme.surfaceContainer, 0)
|
||||
border.color: pathEditMode ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
|
||||
border.width: pathEditMode ? 1 : 0
|
||||
visible: !pathEditMode
|
||||
|
||||
StyledText {
|
||||
id: pathDisplay
|
||||
text: currentPath.replace("file://", "")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
anchors.rightMargin: Theme.spacingS
|
||||
elide: Text.ElideMiddle
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
maximumLineCount: 1
|
||||
wrapMode: Text.NoWrap
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.IBeamCursor
|
||||
onClicked: {
|
||||
pathEditMode = true;
|
||||
pathInput.text = currentPath.replace("file://", "");
|
||||
Qt.callLater(() => pathInput.forceActiveFocus());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DankTextField {
|
||||
id: pathInput
|
||||
anchors.fill: parent
|
||||
visible: pathEditMode
|
||||
topPadding: Theme.spacingXS
|
||||
bottomPadding: Theme.spacingXS
|
||||
onAccepted: {
|
||||
const newPath = text.trim();
|
||||
if (newPath !== "") {
|
||||
navigation.navigateTo(newPath);
|
||||
}
|
||||
pathEditMode = false;
|
||||
}
|
||||
Keys.onEscapePressed: {
|
||||
pathEditMode = false;
|
||||
}
|
||||
Keys.onDownPressed: {
|
||||
pathEditMode = false;
|
||||
}
|
||||
onActiveFocusChanged: {
|
||||
navigation.pathInputFocusChanged(activeFocus);
|
||||
if (!activeFocus && pathEditMode) {
|
||||
pathEditMode = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
spacing: Theme.spacingXS
|
||||
visible: !showSidebar
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
DankActionButton {
|
||||
circular: false
|
||||
iconName: "sort"
|
||||
iconSize: Theme.iconSize - 6
|
||||
iconColor: Theme.surfaceText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,127 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserOverwriteDialog {}
|
||||
Item {
|
||||
id: overwriteDialog
|
||||
|
||||
property bool showDialog: false
|
||||
property string pendingFilePath: ""
|
||||
|
||||
signal confirmed(string filePath)
|
||||
signal cancelled
|
||||
|
||||
visible: showDialog
|
||||
focus: showDialog
|
||||
|
||||
Keys.onEscapePressed: {
|
||||
cancelled();
|
||||
}
|
||||
|
||||
Keys.onReturnPressed: {
|
||||
confirmed(pendingFilePath);
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Theme.shadowStrong
|
||||
opacity: 0.8
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
anchors.centerIn: parent
|
||||
width: 400
|
||||
height: 160
|
||||
color: Theme.surfaceContainer
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
|
||||
Column {
|
||||
anchors.centerIn: parent
|
||||
width: parent.width - Theme.spacingL * 2
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("File Already Exists")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("A file with this name already exists. Do you want to overwrite it?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledRect {
|
||||
width: 80
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: cancelArea.containsMouse ? Qt.lighter(Theme.surfaceVariant, 1.2) : Theme.surfaceVariant
|
||||
border.color: Theme.outline
|
||||
border.width: 1
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Cancel")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: cancelArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
cancelled();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: 90
|
||||
height: 36
|
||||
radius: Theme.cornerRadius
|
||||
color: overwriteArea.containsMouse ? Qt.darker(Theme.primary, 1.1) : Theme.primary
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Overwrite")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.background
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: overwriteArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
confirmed(pendingFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,75 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserSaveRow {}
|
||||
Row {
|
||||
id: saveRow
|
||||
|
||||
property bool saveMode: false
|
||||
property string defaultFileName: ""
|
||||
property string currentPath: ""
|
||||
property alias fileName: fileNameInput.text
|
||||
|
||||
signal saveRequested(string filePath)
|
||||
|
||||
height: saveMode ? 40 : 0
|
||||
visible: saveMode
|
||||
spacing: Theme.spacingM
|
||||
|
||||
DankTextField {
|
||||
id: fileNameInput
|
||||
|
||||
width: parent.width - saveButton.width - Theme.spacingM
|
||||
height: 40
|
||||
text: defaultFileName
|
||||
placeholderText: I18n.tr("Enter filename...")
|
||||
ignoreLeftRightKeys: false
|
||||
focus: saveMode
|
||||
topPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingS
|
||||
Component.onCompleted: {
|
||||
if (saveMode)
|
||||
Qt.callLater(() => {
|
||||
forceActiveFocus();
|
||||
});
|
||||
}
|
||||
onAccepted: {
|
||||
if (text.trim() !== "") {
|
||||
var basePath = currentPath.replace(/^file:\/\//, '');
|
||||
var fullPath = basePath + "/" + text.trim();
|
||||
fullPath = fullPath.replace(/\/+/g, '/');
|
||||
saveRequested(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
id: saveButton
|
||||
|
||||
width: 80
|
||||
height: 40
|
||||
color: fileNameInput.text.trim() !== "" ? Theme.primary : Theme.surfaceVariant
|
||||
radius: Theme.cornerRadius
|
||||
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: I18n.tr("Save")
|
||||
color: fileNameInput.text.trim() !== "" ? Theme.primaryText : Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
}
|
||||
|
||||
StateLayer {
|
||||
stateColor: Theme.primary
|
||||
cornerRadius: Theme.cornerRadius
|
||||
enabled: fileNameInput.text.trim() !== ""
|
||||
onClicked: {
|
||||
if (fileNameInput.text.trim() !== "") {
|
||||
var basePath = currentPath.replace(/^file:\/\//, '');
|
||||
var fullPath = basePath + "/" + fileNameInput.text.trim();
|
||||
fullPath = fullPath.replace(/\/+/g, '/');
|
||||
saveRequested(fullPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,70 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserSidebar {}
|
||||
StyledRect {
|
||||
id: sidebar
|
||||
|
||||
property var quickAccessLocations: []
|
||||
property string currentPath: ""
|
||||
signal locationSelected(string path)
|
||||
|
||||
width: 200
|
||||
color: Theme.nestedSurface
|
||||
clip: true
|
||||
|
||||
Column {
|
||||
anchors.fill: parent
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Quick Access")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
leftPadding: Theme.spacingS
|
||||
bottomPadding: Theme.spacingXS
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: quickAccessLocations
|
||||
|
||||
StyledRect {
|
||||
width: parent?.width ?? 0
|
||||
height: 38
|
||||
radius: Theme.cornerRadius
|
||||
color: quickAccessMouseArea.containsMouse ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : (currentPath === modelData?.path ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingM
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: modelData?.icon ?? ""
|
||||
size: Theme.iconSize - 2
|
||||
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: currentPath === modelData?.path ? Theme.primary : Theme.surfaceText
|
||||
font.weight: currentPath === modelData?.path ? Font.Medium : Font.Normal
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: quickAccessMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: locationSelected(modelData?.path ?? "")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,189 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileBrowserSortMenu {}
|
||||
StyledRect {
|
||||
id: sortMenu
|
||||
|
||||
property string sortBy: "name"
|
||||
property bool sortAscending: true
|
||||
property color surfaceColor: Theme.surfaceContainer
|
||||
|
||||
signal sortBySelected(string value)
|
||||
signal sortOrderSelected(bool ascending)
|
||||
|
||||
width: 200
|
||||
height: sortColumn.height + Theme.spacingM * 2
|
||||
color: surfaceColor
|
||||
radius: Theme.cornerRadius
|
||||
border.color: Theme.outlineMedium
|
||||
border.width: 1
|
||||
visible: false
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
id: sortColumn
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.top: parent.top
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: "Sort By"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
Repeater {
|
||||
model: [
|
||||
{
|
||||
"name": "Name",
|
||||
"value": "name"
|
||||
},
|
||||
{
|
||||
"name": "Size",
|
||||
"value": "size"
|
||||
},
|
||||
{
|
||||
"name": "Modified",
|
||||
"value": "modified"
|
||||
},
|
||||
{
|
||||
"name": "Type",
|
||||
"value": "type"
|
||||
}
|
||||
]
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: sortMouseArea.containsMouse ? Theme.surfaceVariant : (sortBy === modelData?.value ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: sortBy === modelData?.value ? "check" : ""
|
||||
size: Theme.iconSizeSmall
|
||||
color: Theme.primary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: sortBy === modelData?.value
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: modelData?.name ?? ""
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: sortBy === modelData?.value ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: sortMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortBySelected(modelData?.value ?? "name");
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn.width
|
||||
height: 1
|
||||
color: Theme.outline
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Order"
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
font.weight: Font.Medium
|
||||
topPadding: Theme.spacingXS
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: ascMouseArea.containsMouse ? Theme.surfaceVariant : (sortAscending ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_upward"
|
||||
size: Theme.iconSizeSmall
|
||||
color: sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Ascending"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: ascMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortOrderSelected(true);
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledRect {
|
||||
width: sortColumn?.width ?? 0
|
||||
height: 32
|
||||
radius: Theme.cornerRadius
|
||||
color: descMouseArea.containsMouse ? Theme.surfaceVariant : (!sortAscending ? Theme.surfacePressed : Theme.withAlpha(Theme.surfacePressed, 0))
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: Theme.spacingS
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "arrow_downward"
|
||||
size: Theme.iconSizeSmall
|
||||
color: !sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: "Descending"
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: !sortAscending ? Theme.primary : Theme.surfaceText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: descMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: {
|
||||
sortMenu.sortOrderSelected(false);
|
||||
sortMenu.visible = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,236 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.FileInfo {}
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showFileInfo: false
|
||||
property int selectedIndex: -1
|
||||
property var sourceFolderModel: null
|
||||
property string currentPath: ""
|
||||
|
||||
height: 200
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.secondary
|
||||
border.width: 2
|
||||
opacity: showFileInfo ? 1 : 0
|
||||
z: 100
|
||||
|
||||
onShowFileInfoChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fileStatProcess
|
||||
command: ["stat", "-c", "%y|%A|%s|%n", selectedFilePath]
|
||||
property string selectedFilePath: ""
|
||||
property var fileStats: null
|
||||
running: false
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text && text.trim()) {
|
||||
const parts = text.trim().split('|');
|
||||
if (parts.length >= 4) {
|
||||
fileStatProcess.fileStats = {
|
||||
"modifiedTime": parts[0],
|
||||
"permissions": parts[1],
|
||||
"size": parseInt(parts[2]) || 0,
|
||||
"fullPath": parts[3]
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onExited: function (exitCode) {}
|
||||
}
|
||||
|
||||
property string currentFileName: ""
|
||||
property bool currentFileIsDir: false
|
||||
property string currentFileExtension: ""
|
||||
|
||||
onCurrentFileNameChanged: {
|
||||
if (showFileInfo && currentFileName && currentPath) {
|
||||
const fullPath = currentPath + "/" + currentFileName;
|
||||
if (fullPath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = fullPath;
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateFileInfo(filePath, fileName, isDirectory) {
|
||||
if (filePath && filePath !== fileStatProcess.selectedFilePath) {
|
||||
fileStatProcess.selectedFilePath = filePath;
|
||||
currentFileName = fileName || "";
|
||||
currentFileIsDir = isDirectory || false;
|
||||
|
||||
let ext = "";
|
||||
if (!isDirectory && fileName) {
|
||||
const lastDot = fileName.lastIndexOf('.');
|
||||
if (lastDot > 0) {
|
||||
ext = fileName.substring(lastDot + 1).toLowerCase();
|
||||
}
|
||||
}
|
||||
currentFileExtension = ext;
|
||||
|
||||
if (showFileInfo) {
|
||||
fileStatProcess.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var currentFileDisplayData: {
|
||||
if (selectedIndex < 0 || !sourceFolderModel) {
|
||||
return {
|
||||
"exists": false,
|
||||
"name": "No selection",
|
||||
"type": "",
|
||||
"size": "",
|
||||
"modified": "",
|
||||
"permissions": "",
|
||||
"extension": "",
|
||||
"position": "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
const hasValidFile = currentFileName !== "";
|
||||
return {
|
||||
"exists": hasValidFile,
|
||||
"name": hasValidFile ? currentFileName : "Loading...",
|
||||
"type": currentFileIsDir ? "Directory" : "File",
|
||||
"size": fileStatProcess.fileStats ? formatFileSize(fileStatProcess.fileStats.size) : "Calculating...",
|
||||
"modified": fileStatProcess.fileStats ? formatDateTime(fileStatProcess.fileStats.modifiedTime) : "Loading...",
|
||||
"permissions": fileStatProcess.fileStats ? fileStatProcess.fileStats.permissions : "Loading...",
|
||||
"extension": currentFileExtension,
|
||||
"position": sourceFolderModel ? ((selectedIndex + 1) + " of " + sourceFolderModel.count) : "N/A"
|
||||
};
|
||||
}
|
||||
|
||||
Column {
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
Row {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankIcon {
|
||||
name: "info"
|
||||
size: Theme.iconSize
|
||||
color: Theme.secondary
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("File Information")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
width: parent.width
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.name
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
elide: Text.ElideMiddle
|
||||
wrapMode: Text.NoWrap
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.type + (currentFileDisplayData.extension ? " (." + currentFileDisplayData.extension + ")" : "")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.size
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
visible: currentFileDisplayData.exists && !currentFileIsDir
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.modified
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
elide: Text.ElideRight
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.permissions
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
visible: currentFileDisplayData.exists
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: currentFileDisplayData.position
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("F1/I: Toggle • F10: Help")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceTextMedium
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingM
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
function formatFileSize(bytes) {
|
||||
if (bytes === 0 || !bytes) {
|
||||
return "0 B";
|
||||
}
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
function formatDateTime(dateTimeString) {
|
||||
if (!dateTimeString) {
|
||||
return "Unknown";
|
||||
}
|
||||
const parts = dateTimeString.split(' ');
|
||||
if (parts.length >= 2) {
|
||||
return parts[0] + " " + parts[1].split('.')[0];
|
||||
}
|
||||
return dateTimeString;
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,50 @@
|
||||
import qs.DankCommon.Modals.FileBrowser as DankCommon
|
||||
import QtQuick
|
||||
import qs.Common
|
||||
import qs.Widgets
|
||||
|
||||
DankCommon.KeyboardHints {}
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool showHints: false
|
||||
|
||||
height: 80
|
||||
radius: Theme.cornerRadius
|
||||
color: Theme.withAlpha(Theme.surfaceContainer, 0.95)
|
||||
border.color: Theme.primary
|
||||
border.width: 2
|
||||
opacity: showHints ? 1 : 0
|
||||
z: 100
|
||||
|
||||
Column {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
anchors.margins: Theme.spacingS
|
||||
spacing: Theme.spacingXXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Tab/Shift+Tab: Nav • ←→↑↓: Grid Nav • Enter/Space: Select")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Alt+←/Backspace: Back • F1/I: File Info • F10: Help • Esc: Close")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceText
|
||||
width: parent.width
|
||||
wrapMode: Text.WordWrap
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: Theme.shortDuration
|
||||
easing.type: Theme.standardEasing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ FocusScope {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Authentication failed - try again")
|
||||
text: I18n.tr("Authentication failed, please try again")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.error
|
||||
width: parent.width
|
||||
|
||||
@@ -27,7 +27,7 @@ DankModal {
|
||||
property real holdProgress: 0
|
||||
property bool showHoldHint: false
|
||||
property bool holdFromKeyboard: false
|
||||
property string pendingKeyAction: ""
|
||||
property string committedAction: ""
|
||||
|
||||
readonly property bool needsConfirmation: SettingsData.powerActionConfirm
|
||||
readonly property int holdDurationMs: SettingsData.powerActionHoldDuration * 1000
|
||||
@@ -39,10 +39,16 @@ DankModal {
|
||||
return action !== "lock" && action !== "restart";
|
||||
}
|
||||
|
||||
function actionWakesOnKeyRelease(action) {
|
||||
return action === "suspend" || action === "hibernate";
|
||||
}
|
||||
|
||||
function startHold(action, actionIndex) {
|
||||
if (committedAction !== "")
|
||||
return;
|
||||
if (!needsConfirmation || !actionNeedsConfirm(action)) {
|
||||
if (holdFromKeyboard) {
|
||||
pendingKeyAction = action;
|
||||
if (holdFromKeyboard && actionWakesOnKeyRelease(action)) {
|
||||
commitAction(action, actionIndex);
|
||||
return;
|
||||
}
|
||||
executeAction(action);
|
||||
@@ -56,7 +62,6 @@ DankModal {
|
||||
}
|
||||
|
||||
function cancelHold() {
|
||||
pendingKeyAction = "";
|
||||
if (holdAction === "")
|
||||
return;
|
||||
const wasHolding = holdProgress > 0;
|
||||
@@ -76,8 +81,8 @@ DankModal {
|
||||
return;
|
||||
}
|
||||
holdTimer.stop();
|
||||
if (holdFromKeyboard) {
|
||||
pendingKeyAction = holdAction;
|
||||
if (holdFromKeyboard && actionWakesOnKeyRelease(holdAction)) {
|
||||
commitAction(holdAction, holdActionIndex);
|
||||
return;
|
||||
}
|
||||
const action = holdAction;
|
||||
@@ -87,6 +92,25 @@ DankModal {
|
||||
executeAction(action);
|
||||
}
|
||||
|
||||
function commitAction(action, actionIndex) {
|
||||
holdTimer.stop();
|
||||
holdAction = "";
|
||||
holdActionIndex = actionIndex;
|
||||
committedAction = action;
|
||||
commitFallbackTimer.restart();
|
||||
}
|
||||
|
||||
function executeCommittedAction() {
|
||||
if (committedAction === "")
|
||||
return;
|
||||
commitFallbackTimer.stop();
|
||||
const action = committedAction;
|
||||
committedAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
executeAction(action);
|
||||
}
|
||||
|
||||
signal switchUserRequested
|
||||
|
||||
function executeAction(action) {
|
||||
@@ -128,6 +152,12 @@ DankModal {
|
||||
onTriggered: root.showHoldHint = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: commitFallbackTimer
|
||||
interval: 5000
|
||||
onTriggered: root.executeCommittedAction()
|
||||
}
|
||||
|
||||
function openCentered() {
|
||||
parentBounds = Qt.rect(0, 0, 0, 0);
|
||||
parentScreen = null;
|
||||
@@ -286,7 +316,8 @@ DankModal {
|
||||
holdProgress = 0;
|
||||
showHoldHint = false;
|
||||
holdFromKeyboard = false;
|
||||
pendingKeyAction = "";
|
||||
committedAction = "";
|
||||
commitFallbackTimer.stop();
|
||||
updateVisibleActions();
|
||||
const defaultIndex = getDefaultActionIndex();
|
||||
selectedIndex = defaultIndex;
|
||||
@@ -309,6 +340,10 @@ DankModal {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (committedAction !== "") {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
holdFromKeyboard = true;
|
||||
if (SettingsData.powerMenuGridLayout) {
|
||||
handleGridNavigation(event, true);
|
||||
@@ -321,6 +356,11 @@ DankModal {
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (committedAction !== "") {
|
||||
event.accepted = true;
|
||||
executeCommittedAction();
|
||||
return;
|
||||
}
|
||||
if (SettingsData.powerMenuGridLayout) {
|
||||
handleGridNavigation(event, false);
|
||||
} else {
|
||||
@@ -331,16 +371,7 @@ DankModal {
|
||||
function handleListNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (pendingKeyAction !== "") {
|
||||
const action = pendingKeyAction;
|
||||
pendingKeyAction = "";
|
||||
holdAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
executeAction(action);
|
||||
} else {
|
||||
cancelHold();
|
||||
}
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
@@ -434,16 +465,7 @@ DankModal {
|
||||
function handleGridNavigation(event, isPressed) {
|
||||
if (!isPressed) {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter || event.key === Qt.Key_R || event.key === Qt.Key_X || event.key === Qt.Key_L || event.key === Qt.Key_S || event.key === Qt.Key_H || event.key === Qt.Key_D || (event.key === Qt.Key_P && !(event.modifiers & Qt.ControlModifier))) {
|
||||
if (pendingKeyAction !== "") {
|
||||
const action = pendingKeyAction;
|
||||
pendingKeyAction = "";
|
||||
holdAction = "";
|
||||
holdActionIndex = -1;
|
||||
holdProgress = 0;
|
||||
executeAction(action);
|
||||
} else {
|
||||
cancelHold();
|
||||
}
|
||||
cancelHold();
|
||||
event.accepted = true;
|
||||
}
|
||||
return;
|
||||
@@ -812,6 +834,7 @@ DankModal {
|
||||
|
||||
Row {
|
||||
id: hintRow
|
||||
readonly property bool selectedNeedsHold: root.actionNeedsConfirm(root.getActionAtIndex(root.selectedIndex))
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.bottomMargin: Theme.spacingS
|
||||
@@ -826,7 +849,13 @@ DankModal {
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
name: root.showHoldHint ? "warning" : "touch_app"
|
||||
name: {
|
||||
if (root.showHoldHint)
|
||||
return "warning";
|
||||
if (!hintRow.selectedNeedsHold)
|
||||
return "bolt";
|
||||
return "touch_app";
|
||||
}
|
||||
size: Theme.fontSizeSmall
|
||||
color: root.showHoldHint ? Theme.warning : Theme.surfaceTextSecondary
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
@@ -836,8 +865,12 @@ DankModal {
|
||||
readonly property real totalMs: SettingsData.powerActionHoldDuration * 1000
|
||||
readonly property int remainingMs: Math.ceil(totalMs * (1 - root.holdProgress))
|
||||
text: {
|
||||
if (root.committedAction !== "")
|
||||
return I18n.tr("Release to confirm");
|
||||
if (root.showHoldHint)
|
||||
return I18n.tr("Hold longer to confirm");
|
||||
if (!hintRow.selectedNeedsHold)
|
||||
return I18n.tr("Activates immediately");
|
||||
if (root.holdProgress > 0) {
|
||||
if (totalMs < 1000)
|
||||
return I18n.tr("Hold to confirm (%1 ms)").arg(remainingMs);
|
||||
|
||||
@@ -494,7 +494,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Processes", "process count label in footer") + ":"
|
||||
text: I18n.tr("Processes:", "process count label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
@@ -511,7 +511,7 @@ FloatingWindow {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Uptime", "uptime label in footer") + ":"
|
||||
text: I18n.tr("Uptime:", "uptime label in footer")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ PluginComponent {
|
||||
ccWidgetPrimaryText: I18n.tr("Printers")
|
||||
ccWidgetSecondaryText: {
|
||||
if (CupsService.cupsAvailable && CupsService.getPrintersNum() > 0) {
|
||||
return I18n.tr("Printers") + ": " + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs") + ": " + CupsService.getTotalJobsNum();
|
||||
return I18n.tr("Printers: ") + CupsService.getPrintersNum() + " - " + I18n.tr("Jobs: ") + CupsService.getTotalJobsNum();
|
||||
} else {
|
||||
if (!CupsService.cupsAvailable) {
|
||||
return I18n.tr("Print Server not available");
|
||||
|
||||
@@ -18,7 +18,7 @@ PluginComponent {
|
||||
ccWidgetPrimaryText: I18n.tr("VPN")
|
||||
ccWidgetSecondaryText: {
|
||||
if (vpnActivating)
|
||||
return I18n.tr("Connecting...");
|
||||
return I18n.tr("Connecting…");
|
||||
if (!vpnActivated)
|
||||
return I18n.tr("Disconnected");
|
||||
const names = DMSNetworkService.activeNames || [];
|
||||
|
||||
@@ -77,6 +77,12 @@ DankPopout {
|
||||
|
||||
readonly property color _containerBg: Theme.nestedSurface
|
||||
|
||||
// Defer open one tick so screen-change geometry settles before the surface
|
||||
// maps; a synchronous open churns the surface and loses the blur on a switch.
|
||||
function present() {
|
||||
Qt.callLater(open);
|
||||
}
|
||||
|
||||
function openWithSection(section) {
|
||||
StateUtils.openWithSection(root, section);
|
||||
}
|
||||
|
||||
@@ -166,7 +166,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: scanButton.isDiscovering ? I18n.tr("Scanning...") : I18n.tr("Scan")
|
||||
text: scanButton.isDiscovering ? I18n.tr("Scanning") : I18n.tr("Scan")
|
||||
color: scanButton.adapterEnabled ? Theme.primary : Theme.surfaceVariantText
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
font.weight: Font.Medium
|
||||
|
||||
@@ -624,7 +624,7 @@ Rectangle {
|
||||
spacing: Theme.spacingXS
|
||||
|
||||
StyledText {
|
||||
text: wifiDelegate.isConnecting ? I18n.tr("Connecting...") + " \u2022" : (wifiDelegate.isConnected ? I18n.tr("Connected") + " \u2022" : (modelData.secured ? I18n.tr("Secured") + " \u2022" : I18n.tr("Open", "network security type", true) + " \u2022"))
|
||||
text: wifiDelegate.isConnecting ? I18n.tr("Connecting...") + " \u2022" : (wifiDelegate.isConnected ? I18n.tr("Connected") + " \u2022" : (modelData.secured ? I18n.tr("Secured") + " \u2022" : I18n.tr("Open") + " \u2022"))
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
color: wifiDelegate.isConnecting ? Theme.warning : Theme.surfaceVariantText
|
||||
}
|
||||
|
||||
@@ -63,8 +63,8 @@ BasePill {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onPressed: mouse => root.triggerRipple(this, mouse.x, mouse.y)
|
||||
onClicked: function (mouse) {
|
||||
onPressed: function (mouse) {
|
||||
root.triggerRipple(this, mouse.x, mouse.y);
|
||||
switch (mouse.button) {
|
||||
case Qt.RightButton:
|
||||
openContextMenu();
|
||||
|
||||
@@ -7,9 +7,6 @@ import qs.Widgets
|
||||
BasePill {
|
||||
id: root
|
||||
|
||||
property color idleColor: Theme.widgetTextColor
|
||||
property color inhibitColor: Theme.primary
|
||||
|
||||
content: Component {
|
||||
Item {
|
||||
implicitWidth: icon.width
|
||||
@@ -20,7 +17,7 @@ BasePill {
|
||||
anchors.centerIn: parent
|
||||
name: SessionService.idleInhibited ? "motion_sensor_active" : "motion_sensor_idle"
|
||||
size: Theme.barIconSize(root.barThickness, -4, root.barConfig?.maximizeWidgetIcons, root.barConfig?.iconScale)
|
||||
color: SessionService.idleInhibited ? inhibitColor : idleColor
|
||||
color: Theme.widgetTextColor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1250,13 +1250,8 @@ BasePill {
|
||||
|
||||
if (root.isVerticalOrientation) {
|
||||
const edge = root.axis?.edge;
|
||||
if (edge === "left") {
|
||||
const targetX = overflowMenu.anchorPos.x;
|
||||
return Math.max(left, Math.min(right, targetX));
|
||||
} else {
|
||||
const targetX = overflowMenu.anchorPos.x - alignedWidth;
|
||||
return Math.max(left, Math.min(right, targetX));
|
||||
}
|
||||
const targetX = edge === "left" ? overflowMenu.anchorPos.x : overflowMenu.anchorPos.x - alignedWidth;
|
||||
return Math.max(10, Math.min(overflowMenu.width - alignedWidth - 10, targetX));
|
||||
} else {
|
||||
const want = overflowMenu.anchorPos.x - alignedWidth / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
@@ -1271,13 +1266,8 @@ BasePill {
|
||||
const want = overflowMenu.anchorPos.y - alignedHeight / 2;
|
||||
return Math.max(top, Math.min(bottom, want));
|
||||
} else {
|
||||
if (root.isAtBottom) {
|
||||
const targetY = overflowMenu.anchorPos.y - alignedHeight;
|
||||
return Math.max(top, Math.min(bottom, targetY));
|
||||
} else {
|
||||
const targetY = overflowMenu.anchorPos.y;
|
||||
return Math.max(top, Math.min(bottom, targetY));
|
||||
}
|
||||
const targetY = root.isAtBottom ? overflowMenu.anchorPos.y - alignedHeight : overflowMenu.anchorPos.y;
|
||||
return Math.max(10, Math.min(overflowMenu.height - alignedHeight - 10, targetY));
|
||||
}
|
||||
})(), overflowMenu.dpr)
|
||||
|
||||
@@ -1770,13 +1760,8 @@ BasePill {
|
||||
|
||||
if (menuRoot.isVertical) {
|
||||
const edge = menuRoot.axis?.edge;
|
||||
if (edge === "left") {
|
||||
const targetX = menuWindow.anchorPos.x;
|
||||
return Math.max(left, Math.min(right, targetX));
|
||||
} else {
|
||||
const targetX = menuWindow.anchorPos.x - alignedWidth;
|
||||
return Math.max(left, Math.min(right, targetX));
|
||||
}
|
||||
const targetX = edge === "left" ? menuWindow.anchorPos.x : menuWindow.anchorPos.x - alignedWidth;
|
||||
return Math.max(10, Math.min(menuWindow.width - alignedWidth - 10, targetX));
|
||||
} else {
|
||||
const want = menuWindow.anchorPos.x - alignedWidth / 2;
|
||||
return Math.max(left, Math.min(right, want));
|
||||
@@ -1791,13 +1776,8 @@ BasePill {
|
||||
const want = menuWindow.anchorPos.y - alignedHeight / 2;
|
||||
return Math.max(top, Math.min(bottom, want));
|
||||
} else {
|
||||
if (menuRoot.isAtBottom) {
|
||||
const targetY = menuWindow.anchorPos.y - alignedHeight;
|
||||
return Math.max(top, Math.min(bottom, targetY));
|
||||
} else {
|
||||
const targetY = menuWindow.anchorPos.y;
|
||||
return Math.max(top, Math.min(bottom, targetY));
|
||||
}
|
||||
const targetY = menuRoot.isAtBottom ? menuWindow.anchorPos.y - alignedHeight : menuWindow.anchorPos.y;
|
||||
return Math.max(10, Math.min(menuWindow.height - alignedHeight - 10, targetY));
|
||||
}
|
||||
})(), menuWindow.dpr)
|
||||
|
||||
|
||||
@@ -329,7 +329,7 @@ Item {
|
||||
spacing: Theme.spacingS
|
||||
|
||||
DankButton {
|
||||
text: root.saving ? I18n.tr("Saving...") : I18n.tr("Save")
|
||||
text: root.saving ? I18n.tr("Saving…") : I18n.tr("Save")
|
||||
iconName: "check"
|
||||
buttonHeight: 32
|
||||
backgroundColor: Theme.primary
|
||||
|
||||
@@ -1079,7 +1079,7 @@ Rectangle {
|
||||
|
||||
Text {
|
||||
text: I18n.tr("Add a task...", "placeholder in the new-task input field")
|
||||
color: Theme.onSurface_38
|
||||
color: Theme.outlineButton
|
||||
visible: taskInput.text.length === 0
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
@@ -19,6 +18,7 @@ Card {
|
||||
anchors.centerIn: parent
|
||||
spacing: Theme.spacingS
|
||||
visible: !WeatherService.weather.available
|
||||
z: 1
|
||||
|
||||
DankSpinner {
|
||||
size: 24
|
||||
@@ -42,9 +42,9 @@ Card {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
DankButton {
|
||||
text: I18n.tr("Refresh")
|
||||
flat: true
|
||||
buttonHeight: 32
|
||||
visible: !WeatherService.weather.loading
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
onClicked: WeatherService.forceRefresh()
|
||||
|
||||
@@ -141,52 +141,27 @@ Item {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
DankActionButton {
|
||||
id: refreshButtonTwo
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.4)
|
||||
anchors.top: parent.top
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
smoothTransform: isRefreshing
|
||||
|
||||
property bool isRefreshing: false
|
||||
iconName: isRefreshing ? "" : "refresh"
|
||||
iconColor: Theme.withAlpha(Theme.surfaceText, 0.4)
|
||||
tooltipText: I18n.tr("Refresh Weather")
|
||||
tooltipSide: "left"
|
||||
enabled: !isRefreshing
|
||||
|
||||
MouseArea {
|
||||
id: refreshButtonMouseAreaTwo
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
enabled: parent.enabled
|
||||
property bool isRefreshing: false
|
||||
|
||||
Timer {
|
||||
id: hoverDelayTwo
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
refreshButtonTooltipTwo.show(I18n.tr("Refresh Weather"), refreshButtonTwo, 0, 0, "left");
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
hoverDelayTwo.restart();
|
||||
}
|
||||
|
||||
onExited: {
|
||||
hoverDelayTwo.stop();
|
||||
refreshButtonTooltipTwo.hide();
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
refreshButtonTwo.isRefreshing = true;
|
||||
WeatherService.forceRefresh();
|
||||
refreshTimerTwo.restart();
|
||||
}
|
||||
onClicked: {
|
||||
isRefreshing = true;
|
||||
WeatherService.forceRefresh();
|
||||
refreshTimerTwo.restart();
|
||||
}
|
||||
|
||||
DankTooltipV2 {
|
||||
id: refreshButtonTooltipTwo
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
size: refreshButtonTwo.iconSize
|
||||
visible: refreshButtonTwo.isRefreshing
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -194,14 +169,6 @@ Item {
|
||||
interval: 2000
|
||||
onTriggered: refreshButtonTwo.isRefreshing = false
|
||||
}
|
||||
|
||||
RotationAnimator on rotation {
|
||||
running: refreshButtonTwo.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -864,52 +831,28 @@ Item {
|
||||
onClicked: SessionData.setWeatherHourlyDetailed(!SessionData.weatherHourlyDetailed)
|
||||
}
|
||||
|
||||
DankIcon {
|
||||
DankActionButton {
|
||||
id: refreshButton
|
||||
name: "refresh"
|
||||
size: Theme.iconSize - 4
|
||||
color: Theme.withAlpha(Theme.surfaceText, 0.4)
|
||||
anchors.right: parent.right
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
smoothTransform: isRefreshing
|
||||
|
||||
property bool isRefreshing: false
|
||||
iconName: isRefreshing ? "" : "refresh"
|
||||
iconColor: Theme.withAlpha(Theme.surfaceText, 0.4)
|
||||
tooltipText: I18n.tr("Refresh Weather")
|
||||
tooltipSide: "left"
|
||||
enabled: !isRefreshing
|
||||
|
||||
MouseArea {
|
||||
id: refreshButtonMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: parent.enabled ? Qt.PointingHandCursor : Qt.ForbiddenCursor
|
||||
enabled: parent.enabled
|
||||
property bool isRefreshing: false
|
||||
|
||||
Timer {
|
||||
id: hoverDelay
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
refreshButtonTooltip.show(I18n.tr("Refresh Weather"), refreshButton, 0, 0, "left");
|
||||
}
|
||||
}
|
||||
|
||||
onEntered: {
|
||||
hoverDelay.restart();
|
||||
}
|
||||
|
||||
onExited: {
|
||||
hoverDelay.stop();
|
||||
refreshButtonTooltip.hide();
|
||||
}
|
||||
|
||||
onClicked: {
|
||||
refreshButton.isRefreshing = true;
|
||||
WeatherService.forceRefresh();
|
||||
refreshTimer.restart();
|
||||
}
|
||||
onClicked: {
|
||||
isRefreshing = true;
|
||||
WeatherService.forceRefresh();
|
||||
refreshTimer.restart();
|
||||
}
|
||||
|
||||
DankTooltipV2 {
|
||||
id: refreshButtonTooltip
|
||||
DankSpinner {
|
||||
anchors.centerIn: parent
|
||||
size: refreshButton.iconSize
|
||||
visible: refreshButton.isRefreshing
|
||||
}
|
||||
|
||||
Timer {
|
||||
@@ -917,14 +860,6 @@ Item {
|
||||
interval: 2000
|
||||
onTriggered: refreshButton.isRefreshing = false
|
||||
}
|
||||
|
||||
RotationAnimator on rotation {
|
||||
running: refreshButton.isRefreshing
|
||||
from: 0
|
||||
to: 360
|
||||
duration: 1000
|
||||
loops: Animation.Infinite
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import qs.Common
|
||||
import qs.Services
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
@@ -65,10 +66,23 @@ PanelWindow {
|
||||
}
|
||||
|
||||
function cancelFade() {
|
||||
dismiss();
|
||||
fadeCancelled();
|
||||
}
|
||||
|
||||
function dismiss() {
|
||||
fadeSeq.stop();
|
||||
fadeOverlay.opacity = 0.0;
|
||||
active = false;
|
||||
fadeCancelled();
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: IdleService
|
||||
function onIsShellLockedChanged() {
|
||||
if (IdleService.isShellLocked)
|
||||
return;
|
||||
root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
|
||||
@@ -10,6 +10,8 @@ import qs.Services
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
readonly property var log: Log.scoped("Lock")
|
||||
|
||||
property string sharedPasswordBuffer: ""
|
||||
property bool shouldLock: false
|
||||
|
||||
@@ -21,26 +23,13 @@ Scope {
|
||||
|
||||
onShouldLockChanged: {
|
||||
IdleService.isShellLocked = shouldLock;
|
||||
if (shouldLock && lockPowerOffArmed) {
|
||||
lockStateCheck.restart();
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockStateCheck
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (sessionLock.locked && lockPowerOffArmed) {
|
||||
pendingLock = false;
|
||||
lockPowerOffArmed = false;
|
||||
beginPowerOff();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property bool lockInitiatedLocally: false
|
||||
property bool pendingLock: false
|
||||
readonly property int maxLockRetries: 3
|
||||
property int lockRetryAttempts: 0
|
||||
property bool lockRetryPending: false
|
||||
property bool lockPowerOffArmed: false
|
||||
property bool lockWakeAllowed: false
|
||||
property bool customLockerSpawned: false
|
||||
@@ -116,6 +105,24 @@ Scope {
|
||||
return true;
|
||||
}
|
||||
|
||||
function resetLockRetry() {
|
||||
lockRetryAttempts = 0;
|
||||
lockRetryPending = false;
|
||||
lockRetryTimer.stop();
|
||||
}
|
||||
|
||||
function handleLockLost() {
|
||||
if (lockRetryAttempts >= maxLockRetries) {
|
||||
log.error("Compositor refused session lock", maxLockRetries, "times - resetting lock state");
|
||||
forceReset();
|
||||
return;
|
||||
}
|
||||
lockRetryAttempts++;
|
||||
lockRetryPending = true;
|
||||
lockRetryTimer.restart();
|
||||
log.warn("Session lock lost while lock requested - retry", lockRetryAttempts, "/", maxLockRetries);
|
||||
}
|
||||
|
||||
function lock() {
|
||||
if (SettingsData.customPowerActionLock?.length > 0) {
|
||||
spawnCustomLocker();
|
||||
@@ -124,6 +131,7 @@ Scope {
|
||||
if (shouldLock || pendingLock)
|
||||
return;
|
||||
|
||||
resetLockRetry();
|
||||
lockInitiatedLocally = true;
|
||||
lockPowerOffArmed = powerOffOnLock;
|
||||
|
||||
@@ -139,7 +147,7 @@ Scope {
|
||||
|
||||
function lockAndOutputsOff() {
|
||||
IdleService.lockPowerOffRequested = true;
|
||||
if (sessionLock.locked) {
|
||||
if (sessionLock.secure) {
|
||||
beginPowerOff();
|
||||
return;
|
||||
}
|
||||
@@ -150,6 +158,7 @@ Scope {
|
||||
function unlock() {
|
||||
if (!shouldLock)
|
||||
return;
|
||||
resetLockRetry();
|
||||
lockInitiatedLocally = false;
|
||||
notifyLoginctl(false);
|
||||
shouldLock = false;
|
||||
@@ -160,6 +169,7 @@ Scope {
|
||||
pendingLock = false;
|
||||
shouldLock = false;
|
||||
customLockerSpawned = false;
|
||||
resetLockRetry();
|
||||
resetPowerOffFade();
|
||||
IdleService.lockPowerOffRequested = false;
|
||||
}
|
||||
@@ -198,6 +208,12 @@ Scope {
|
||||
shouldLock = false;
|
||||
}
|
||||
|
||||
function onSessionResumed() {
|
||||
if (!shouldLock || sessionLock.locked)
|
||||
return;
|
||||
resumeRelockTimer.restart();
|
||||
}
|
||||
|
||||
function onLoginctlStateChanged() {
|
||||
if (SessionService.active && pendingLock) {
|
||||
pendingLock = false;
|
||||
@@ -239,7 +255,7 @@ Scope {
|
||||
WlSessionLock {
|
||||
id: sessionLock
|
||||
|
||||
locked: shouldLock
|
||||
locked: shouldLock && !lockRetryPending
|
||||
|
||||
WlSessionLockSurface {
|
||||
id: lockSurface
|
||||
@@ -312,16 +328,23 @@ Scope {
|
||||
Connections {
|
||||
target: sessionLock
|
||||
|
||||
function onLockedChanged() {
|
||||
notifyLockedHint(sessionLock.locked);
|
||||
if (sessionLock.locked) {
|
||||
pendingLock = false;
|
||||
if (lockPowerOffArmed && powerOffOnLock)
|
||||
beginPowerOff();
|
||||
lockPowerOffArmed = false;
|
||||
function onSecureChanged() {
|
||||
notifyLockedHint(sessionLock.secure);
|
||||
if (!sessionLock.secure)
|
||||
return;
|
||||
}
|
||||
IdleService.dismissFadeToLock();
|
||||
lockRetryAttempts = 0;
|
||||
pendingLock = false;
|
||||
if (lockPowerOffArmed && powerOffOnLock)
|
||||
beginPowerOff();
|
||||
lockPowerOffArmed = false;
|
||||
}
|
||||
|
||||
function onLockedChanged() {
|
||||
if (sessionLock.locked)
|
||||
return;
|
||||
if (shouldLock && (IdleService.monitorsOff || lockPowerOffArmed || IdleService.lockPowerOffRequested))
|
||||
return;
|
||||
lockWakeAllowed = false;
|
||||
resetPowerOffFade();
|
||||
if (IdleService.monitorsOff && powerOffOnLock) {
|
||||
@@ -329,6 +352,8 @@ Scope {
|
||||
CompositorService.powerOnMonitors();
|
||||
}
|
||||
IdleService.lockPowerOffRequested = false;
|
||||
if (shouldLock && !lockRetryPending)
|
||||
handleLockLost();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,13 +385,15 @@ Scope {
|
||||
}
|
||||
|
||||
function isLocked(): bool {
|
||||
return sessionLock.locked;
|
||||
return sessionLock.secure;
|
||||
}
|
||||
|
||||
function status(): string {
|
||||
return JSON.stringify({
|
||||
shouldLock: root.shouldLock,
|
||||
sessionLockLocked: sessionLock.locked,
|
||||
sessionLockSecure: sessionLock.secure,
|
||||
lockRetryAttempts: root.lockRetryAttempts,
|
||||
lockInitiatedLocally: root.lockInitiatedLocally,
|
||||
pendingLock: root.pendingLock,
|
||||
loginctlLocked: SessionService.locked,
|
||||
@@ -395,6 +422,29 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: lockRetryTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: root.lockRetryPending = false
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: resumeRelockTimer
|
||||
interval: 1000
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!root.shouldLock || sessionLock.locked)
|
||||
return;
|
||||
if (IdleService.monitorsOff || root.lockPowerOffArmed || IdleService.lockPowerOffRequested)
|
||||
return;
|
||||
root.log.warn("Session lock dead after resume - re-locking");
|
||||
root.resetLockRetry();
|
||||
root.lockRetryPending = true;
|
||||
root.lockRetryPending = false;
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: dpmsReapplyTimer
|
||||
interval: 100
|
||||
@@ -407,7 +457,7 @@ Scope {
|
||||
interval: 200
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
if (!sessionLock.locked)
|
||||
if (!sessionLock.secure)
|
||||
return;
|
||||
if (!powerOffOnLock)
|
||||
return;
|
||||
@@ -426,7 +476,7 @@ Scope {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
enabled: sessionLock.locked
|
||||
enabled: sessionLock.secure
|
||||
hoverEnabled: enabled
|
||||
onPressed: lockWakeDebounce.restart()
|
||||
onPositionChanged: lockWakeDebounce.restart()
|
||||
@@ -435,10 +485,10 @@ Scope {
|
||||
|
||||
FocusScope {
|
||||
anchors.fill: parent
|
||||
focus: sessionLock.locked
|
||||
focus: sessionLock.secure
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (!sessionLock.locked)
|
||||
if (!sessionLock.secure)
|
||||
return;
|
||||
lockWakeDebounce.restart();
|
||||
}
|
||||
|
||||
@@ -692,14 +692,14 @@ Item {
|
||||
spacing: Theme.spacingM
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Unsaved changes")
|
||||
text: I18n.tr("Unsaved Changes")
|
||||
font.pixelSize: Theme.fontSizeLarge
|
||||
color: Theme.surfaceText
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
text: root.pendingAction === "new" ? I18n.tr("You have unsaved changes. Save before creating a new file?") : root.pendingAction.startsWith("close_tab_") ? I18n.tr("You have unsaved changes. Save before closing this tab?") : root.pendingAction === "load_file" || root.pendingAction === "open" ? I18n.tr("You have unsaved changes. Save before opening a file?") : I18n.tr("You have unsaved changes. Save before continuing?")
|
||||
font.pixelSize: Theme.fontSizeMedium
|
||||
color: Theme.surfaceTextMedium
|
||||
width: parent.width
|
||||
|
||||
@@ -61,6 +61,11 @@ DankPopout {
|
||||
notificationHistoryVisible = !notificationHistoryVisible;
|
||||
}
|
||||
|
||||
// Re-open without toggling the flag (used when retargeting to another monitor).
|
||||
function present() {
|
||||
openSized();
|
||||
}
|
||||
|
||||
function openSized() {
|
||||
if (!notificationHistoryVisible)
|
||||
return;
|
||||
|
||||
@@ -172,7 +172,7 @@ Rectangle {
|
||||
}
|
||||
|
||||
StyledText {
|
||||
text: I18n.tr("Timeouts")
|
||||
text: I18n.tr("Notification Timeouts")
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
font.weight: Font.Medium
|
||||
color: Theme.surfaceVariantText
|
||||
@@ -182,6 +182,7 @@ Rectangle {
|
||||
id: lowTimeoutDropdown
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
text: I18n.tr("Low Priority")
|
||||
description: I18n.tr("Timeout for low priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutLow)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
@@ -198,6 +199,7 @@ Rectangle {
|
||||
id: normalTimeoutDropdown
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
text: I18n.tr("Normal Priority")
|
||||
description: I18n.tr("Timeout for normal priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutNormal)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
@@ -214,6 +216,7 @@ Rectangle {
|
||||
id: criticalTimeoutDropdown
|
||||
transientSurfaceTracker: root.transientSurfaceTracker
|
||||
text: I18n.tr("Critical Priority")
|
||||
description: I18n.tr("Timeout for critical priority notifications")
|
||||
currentValue: getTimeoutText(SettingsData.notificationTimeoutCritical)
|
||||
options: timeoutOptions.map(opt => opt.text)
|
||||
onValueChanged: value => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
import qs.Common
|
||||
import qs.DankCommon.Common as DankCommon
|
||||
import qs.Services
|
||||
import qs.Widgets
|
||||
|
||||
@@ -177,9 +176,14 @@ Item {
|
||||
text: "DANK LINUX"
|
||||
font.pixelSize: parent.compactLogo ? 32 : 48
|
||||
font.weight: Font.Bold
|
||||
font.family: DankCommon.Fonts.sans
|
||||
font.family: interFont.name
|
||||
color: Theme.surfaceText
|
||||
antialiasing: true
|
||||
|
||||
FontLoader {
|
||||
id: interFont
|
||||
source: Qt.resolvedUrl("../../assets/fonts/inter/InterVariable.ttf")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,9 +48,8 @@ done
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "battery_charging_full"
|
||||
title: I18n.tr("Status")
|
||||
title: I18n.tr("Battery Status")
|
||||
settingKey: "batteryStatusCard"
|
||||
tags: ["battery", "status", "charge", "health"]
|
||||
|
||||
Column {
|
||||
width: parent.width - Theme.spacingM * 2
|
||||
@@ -168,9 +167,8 @@ done
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "tune"
|
||||
title: I18n.tr("Protection")
|
||||
title: I18n.tr("Battery Protection")
|
||||
settingKey: "batteryProtection"
|
||||
tags: ["battery", "protection", "charge", "limit"]
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "batteryChargeLimit"
|
||||
@@ -232,9 +230,8 @@ done
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "notifications"
|
||||
title: I18n.tr("Alerts")
|
||||
title: I18n.tr("Battery Alerts")
|
||||
settingKey: "batteryAlerts"
|
||||
tags: ["battery", "alerts", "low", "warning"]
|
||||
|
||||
SettingsSliderRow {
|
||||
settingKey: "batteryLowThreshold"
|
||||
@@ -335,6 +332,7 @@ done
|
||||
SettingsDropdownRow {
|
||||
settingKey: "acProfileName"
|
||||
text: I18n.tr("Profile when Plugged In (AC)")
|
||||
description: I18n.tr("Power profile to use when AC power is connected.")
|
||||
options: [I18n.tr("Don't Change"), Theme.getPowerProfileLabel(0), Theme.getPowerProfileLabel(1), Theme.getPowerProfileLabel(2)]
|
||||
currentValue: {
|
||||
const val = SettingsData.acProfileName;
|
||||
@@ -352,6 +350,7 @@ done
|
||||
SettingsDropdownRow {
|
||||
settingKey: "batteryProfileName"
|
||||
text: I18n.tr("Profile when on Battery")
|
||||
description: I18n.tr("Power profile to use when running on battery power.")
|
||||
options: [I18n.tr("Don't Change"), Theme.getPowerProfileLabel(0), Theme.getPowerProfileLabel(1), Theme.getPowerProfileLabel(2)]
|
||||
currentValue: {
|
||||
const val = SettingsData.batteryProfileName;
|
||||
|
||||
@@ -153,7 +153,7 @@ Item {
|
||||
]
|
||||
|
||||
readonly property var entryActionKeys: ["copy", "paste", "pin", "edit", "delete"]
|
||||
readonly property var entryActionLabels: [I18n.tr("Copy"), I18n.tr("Paste"), I18n.tr("Pin", "pin item action"), I18n.tr("Edit"), I18n.tr("Delete")]
|
||||
readonly property var entryActionLabels: [I18n.tr("Copy"), I18n.tr("Paste"), I18n.tr("Pin"), I18n.tr("Edit"), I18n.tr("Delete")]
|
||||
|
||||
function getMaxHistoryText(value) {
|
||||
if (value <= 0)
|
||||
@@ -341,6 +341,7 @@ Item {
|
||||
tags: ["clipboard", "entry", "size", "limit"]
|
||||
settingKey: "maxEntrySize"
|
||||
text: I18n.tr("Maximum Entry Size")
|
||||
description: I18n.tr("Maximum size per clipboard entry")
|
||||
options: root.maxEntrySizeOptions.map(opt => opt.text)
|
||||
|
||||
Component.onCompleted: {
|
||||
|
||||
@@ -94,7 +94,7 @@ Item {
|
||||
|
||||
function fixLayoutInclude() {
|
||||
if (readOnly) {
|
||||
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "hyprland-migration");
|
||||
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing layout settings."), "dms setup", "hyprland-migration");
|
||||
return;
|
||||
}
|
||||
const paths = getLayoutConfigPaths();
|
||||
@@ -208,7 +208,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
StyledText {
|
||||
text: {
|
||||
if (warningBox.showLegacy)
|
||||
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings.");
|
||||
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing layout settings.");
|
||||
if (warningBox.showSetup)
|
||||
return I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/layout");
|
||||
return "";
|
||||
@@ -270,7 +270,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
tags: ["niri", "layout", "gaps", "radius", "window", "border"]
|
||||
title: I18n.tr("%1 Layout Overrides").arg("niri")
|
||||
title: I18n.tr("Niri Layout Overrides")
|
||||
settingKey: "niriLayout"
|
||||
iconName: "layers"
|
||||
visible: CompositorService.isNiri
|
||||
@@ -279,7 +279,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
tags: ["niri", "gaps", "override", "unmanaged"]
|
||||
settingKey: "niriLayoutGapsMode"
|
||||
text: I18n.tr("Gaps")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your %1 config").arg("niri")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your niri config")
|
||||
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
|
||||
currentIndex: {
|
||||
if (SettingsData.niriLayoutGapsOverride === -2)
|
||||
@@ -399,7 +399,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
tags: ["hyprland", "layout", "gaps", "radius", "window", "border", "rounding"]
|
||||
title: I18n.tr("%1 Layout Overrides").arg("Hyprland")
|
||||
title: I18n.tr("Hyprland Layout Overrides")
|
||||
settingKey: "hyprlandLayout"
|
||||
iconName: "crop_square"
|
||||
visible: CompositorService.isHyprland
|
||||
@@ -408,7 +408,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
tags: ["hyprland", "gaps", "override", "inner", "outer", "unmanaged"]
|
||||
settingKey: "hyprlandLayoutGapsMode"
|
||||
text: I18n.tr("Gaps")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your %1 config").arg("Hyprland")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your Hyprland config")
|
||||
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
|
||||
currentIndex: {
|
||||
if (SettingsData.hyprlandLayoutGapsOverride === -2)
|
||||
@@ -492,6 +492,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
tags: ["hyprland", "border", "override"]
|
||||
settingKey: "hyprlandLayoutBorderSizeEnabled"
|
||||
text: I18n.tr("Override Border Size")
|
||||
description: I18n.tr("Use custom border size")
|
||||
checked: SettingsData.hyprlandLayoutBorderSize >= 0
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
@@ -550,7 +551,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
tags: ["mangowc", "mango", "dwl", "layout", "gaps", "radius", "window", "border"]
|
||||
title: I18n.tr("%1 Layout Overrides").arg("MangoWC")
|
||||
title: I18n.tr("MangoWC Layout Overrides")
|
||||
settingKey: "mangoLayout"
|
||||
iconName: "crop_square"
|
||||
visible: CompositorService.isMango
|
||||
@@ -559,7 +560,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
tags: ["mangowc", "mango", "gaps", "override", "inner", "outer", "unmanaged"]
|
||||
settingKey: "mangoLayoutGapsMode"
|
||||
text: I18n.tr("Gaps")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your %1 config").arg("MangoWC")
|
||||
description: I18n.tr("Auto matches bar spacing; Off leaves gaps to your MangoWC config")
|
||||
model: [I18n.tr("Auto"), I18n.tr("Custom"), I18n.tr("Off")]
|
||||
currentIndex: {
|
||||
if (SettingsData.mangoLayoutGapsOverride === -2)
|
||||
@@ -643,6 +644,7 @@ awk '$1 == "xray" { print FILENAME ":" FNR; exit }' $files 2>/dev/null`;
|
||||
tags: ["mangowc", "mango", "border", "override"]
|
||||
settingKey: "mangoLayoutBorderSizeEnabled"
|
||||
text: I18n.tr("Override Border Size")
|
||||
description: I18n.tr("Use custom border size")
|
||||
checked: SettingsData.mangoLayoutBorderSize >= 0
|
||||
onToggled: checked => {
|
||||
if (checked) {
|
||||
|
||||
@@ -266,7 +266,7 @@ Item {
|
||||
|
||||
SettingsCard {
|
||||
iconName: "dashboard"
|
||||
title: I18n.tr("Configurations")
|
||||
title: I18n.tr("Bar Configurations")
|
||||
settingKey: "barConfigurations"
|
||||
visible: !dankBarTab.appearanceOnly
|
||||
|
||||
@@ -490,7 +490,7 @@ Item {
|
||||
DankButtonGroup {
|
||||
id: positionButtonGroup
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
model: [I18n.tr("Top", "screen edge position"), I18n.tr("Bottom", "screen edge position"), I18n.tr("Left", "screen edge position"), I18n.tr("Right", "screen edge position")]
|
||||
model: [I18n.tr("Top"), I18n.tr("Bottom"), I18n.tr("Left"), I18n.tr("Right")]
|
||||
currentIndex: {
|
||||
selectedBarId;
|
||||
const config = SettingsData.getBarConfig(selectedBarId);
|
||||
@@ -820,6 +820,7 @@ Item {
|
||||
id: barTransparencySlider
|
||||
visible: !SettingsData.frameEnabled
|
||||
text: I18n.tr("Bar Opacity")
|
||||
description: I18n.tr("Controls opacity of the bar background")
|
||||
value: (selectedBarConfig?.transparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -842,6 +843,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
id: widgetTransparencySlider
|
||||
text: I18n.tr("Widget Opacity")
|
||||
description: I18n.tr("Controls opacity of widget backgrounds")
|
||||
value: (selectedBarConfig?.widgetTransparency ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1461,6 +1463,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
id: borderOpacitySlider
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Controls opacity of the border")
|
||||
value: (selectedBarConfig?.borderOpacity ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1555,6 +1558,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
id: widgetOutlineOpacitySlider
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Controls opacity of the widget outline")
|
||||
value: (selectedBarConfig?.widgetOutlineOpacity ?? 1.0) * 100
|
||||
minimum: 0
|
||||
maximum: 100
|
||||
@@ -1663,6 +1667,7 @@ Item {
|
||||
SettingsSliderRow {
|
||||
visible: shadowCard.shadowActive
|
||||
text: I18n.tr("Opacity")
|
||||
description: I18n.tr("Controls opacity of the shadow layer")
|
||||
minimum: 10
|
||||
maximum: 100
|
||||
unit: "%"
|
||||
|
||||
@@ -312,6 +312,7 @@ Item {
|
||||
text: I18n.tr("Calendar", "Calendar")
|
||||
category: root.appCategory.Calendar
|
||||
tags: ["calendar", "events"]
|
||||
description: I18n.tr("Manages calendar events", "Manages calendar events")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,6 +330,7 @@ Item {
|
||||
text: I18n.tr("PDF Reader", "PDF Reader")
|
||||
category: root.appCategory.PDFReader
|
||||
tags: ["pdf", "reader"]
|
||||
description: I18n.tr("For reading PDF files", "For reading PDF files")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -339,11 +341,13 @@ Item {
|
||||
text: I18n.tr("Image Viewer", "Image Viewer")
|
||||
category: root.appCategory.ImageViewer
|
||||
tags: ["image", "viewer"]
|
||||
description: I18n.tr("Opens image files", "Opens image files")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("Video Player", "Video Player")
|
||||
category: root.appCategory.VideoPlayer
|
||||
tags: ["video", "player"]
|
||||
description: I18n.tr("Plays video files", "Plays video files")
|
||||
}
|
||||
AppSelector {
|
||||
text: I18n.tr("Music Player", "Music Player")
|
||||
|
||||
@@ -264,24 +264,6 @@ Singleton {
|
||||
callback(true);
|
||||
}
|
||||
|
||||
function publishActiveProfileModes() {
|
||||
const compositor = CompositorService.compositor;
|
||||
const profileId = SettingsData.getActiveDisplayProfile(compositor);
|
||||
const profile = profileId ? validatedProfiles[profileId] : null;
|
||||
const outputs = profile?.outputs || {};
|
||||
const modes = {};
|
||||
|
||||
for (const outputId in outputs) {
|
||||
const mode = outputs[outputId]?.mode;
|
||||
if (mode)
|
||||
modes[outputId] = {
|
||||
"mode": mode
|
||||
};
|
||||
}
|
||||
|
||||
SettingsData.setActiveDisplayProfileModes(compositor, modes);
|
||||
}
|
||||
|
||||
function generateProfileId() {
|
||||
return "profile_" + Date.now() + "_" + Math.random().toString(36).slice(2, 9);
|
||||
}
|
||||
@@ -380,7 +362,6 @@ Singleton {
|
||||
};
|
||||
validatedProfiles = updated;
|
||||
matchedProfile = findMatchingProfile();
|
||||
publishActiveProfileModes();
|
||||
profileSaved(profileId, profileName);
|
||||
});
|
||||
});
|
||||
@@ -562,7 +543,6 @@ Singleton {
|
||||
};
|
||||
const onWriteSuccess = () => {
|
||||
SettingsData.setActiveDisplayProfile(CompositorService.compositor, configId);
|
||||
publishActiveProfileModes();
|
||||
if (isManual) {
|
||||
profilesLoading = false;
|
||||
profileActivated(configId, profileName);
|
||||
@@ -606,7 +586,6 @@ Singleton {
|
||||
writeMonitorsJson(data, null);
|
||||
validatedProfiles = validated;
|
||||
matchedProfile = findMatchingProfile();
|
||||
publishActiveProfileModes();
|
||||
if (!profilesReady) {
|
||||
profilesReady = true;
|
||||
applyAutoConfig();
|
||||
@@ -654,7 +633,6 @@ Singleton {
|
||||
currentOutputSet = buildCurrentOutputSet();
|
||||
matchedProfile = findMatchingProfile();
|
||||
SettingsData.setActiveDisplayProfile(CompositorService.compositor, id);
|
||||
publishActiveProfileModes();
|
||||
profileSaved(id, profileName);
|
||||
});
|
||||
});
|
||||
@@ -700,7 +678,6 @@ Singleton {
|
||||
delete updated[profileId];
|
||||
validatedProfiles = updated;
|
||||
matchedProfile = findMatchingProfile();
|
||||
publishActiveProfileModes();
|
||||
profileDeleted(profileId);
|
||||
});
|
||||
});
|
||||
@@ -913,14 +890,6 @@ Singleton {
|
||||
target: CompositorService
|
||||
function onCompositorChanged() {
|
||||
root.checkIncludeStatus();
|
||||
root.publishActiveProfileModes();
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: SettingsData
|
||||
function onActiveDisplayProfileChanged() {
|
||||
root.publishActiveProfileModes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1549,7 +1518,7 @@ Singleton {
|
||||
}
|
||||
|
||||
function showHyprlandReadOnlyWarning() {
|
||||
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings."), "dms setup", "display-config");
|
||||
ToastService.showWarning(I18n.tr("Hyprland conf mode"), I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing display settings."), "dms setup", "display-config");
|
||||
}
|
||||
|
||||
function buildOutputsMap() {
|
||||
@@ -2282,16 +2251,7 @@ Singleton {
|
||||
"name": match.entry.name || "",
|
||||
"outputs": outputConfigs
|
||||
};
|
||||
writeMonitorsJson(data, success => {
|
||||
if (!success || !profileId)
|
||||
return;
|
||||
const updated = JSON.parse(JSON.stringify(validatedProfiles));
|
||||
if (updated[profileId]) {
|
||||
updated[profileId].outputs = outputConfigs;
|
||||
validatedProfiles = updated;
|
||||
publishActiveProfileModes();
|
||||
}
|
||||
});
|
||||
writeMonitorsJson(data, null);
|
||||
});
|
||||
|
||||
clearPendingChanges();
|
||||
@@ -2679,7 +2639,7 @@ Singleton {
|
||||
function getTransformLabel(transform) {
|
||||
switch (transform) {
|
||||
case "Normal":
|
||||
return I18n.tr("Normal", "display rotation option", true);
|
||||
return I18n.tr("Normal");
|
||||
case "90":
|
||||
return I18n.tr("90°");
|
||||
case "180":
|
||||
@@ -2695,12 +2655,12 @@ Singleton {
|
||||
case "Flipped270":
|
||||
return I18n.tr("Flipped 270°");
|
||||
default:
|
||||
return I18n.tr("Normal", "display rotation option", true);
|
||||
return I18n.tr("Normal");
|
||||
}
|
||||
}
|
||||
|
||||
function getTransformValue(label) {
|
||||
if (label === I18n.tr("Normal", "display rotation option", true))
|
||||
if (label === I18n.tr("Normal"))
|
||||
return "Normal";
|
||||
if (label === I18n.tr("90°"))
|
||||
return "90";
|
||||
|
||||
@@ -60,9 +60,9 @@ StyledRect {
|
||||
StyledText {
|
||||
text: {
|
||||
if (root.showLegacy)
|
||||
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before changing these settings.");
|
||||
return I18n.tr("This install is still using hyprland.conf. Run dms setup to migrate before editing display settings.");
|
||||
if (root.showSetup)
|
||||
return I18n.tr("Click 'Setup' to create %1 and add include to your compositor config.").arg("dms/outputs");
|
||||
return I18n.tr("Click 'Setup' to create the outputs config and add include to your compositor config.");
|
||||
return "";
|
||||
}
|
||||
font.pixelSize: Theme.fontSizeSmall
|
||||
|
||||
@@ -93,21 +93,21 @@ Column {
|
||||
|
||||
currentValue: {
|
||||
if (!hotCornersData)
|
||||
return I18n.tr("Inherit", "inherit from global setting");
|
||||
return I18n.tr("Inherit");
|
||||
if (hotCornersData.off)
|
||||
return I18n.tr("Off");
|
||||
const corners = hotCornersData.corners || [];
|
||||
if (corners.length === 0)
|
||||
return I18n.tr("Inherit", "inherit from global setting");
|
||||
return I18n.tr("Inherit");
|
||||
if (corners.length === 4)
|
||||
return I18n.tr("All");
|
||||
return I18n.tr("Select...");
|
||||
}
|
||||
options: [I18n.tr("Inherit", "inherit from global setting"), I18n.tr("Off"), I18n.tr("All"), I18n.tr("Select...")]
|
||||
options: [I18n.tr("Inherit"), I18n.tr("Off"), I18n.tr("All"), I18n.tr("Select...")]
|
||||
|
||||
onValueChanged: value => {
|
||||
switch (value) {
|
||||
case I18n.tr("Inherit", "inherit from global setting"):
|
||||
case I18n.tr("Inherit"):
|
||||
DisplayConfigState.setNiriSetting(root.outputData, root.outputName, "hotCorners", null);
|
||||
break;
|
||||
case I18n.tr("Off"):
|
||||
@@ -232,7 +232,7 @@ Column {
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
height: 40
|
||||
placeholderText: I18n.tr("Inherit", "inherit from global setting")
|
||||
placeholderText: I18n.tr("Inherit")
|
||||
enabled: !settingsColumn.isDisabled
|
||||
text: {
|
||||
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
|
||||
@@ -270,7 +270,7 @@ Column {
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
height: 40
|
||||
placeholderText: I18n.tr("Inherit", "inherit from global setting")
|
||||
placeholderText: I18n.tr("Inherit")
|
||||
enabled: !settingsColumn.isDisabled
|
||||
text: {
|
||||
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
|
||||
@@ -321,7 +321,7 @@ Column {
|
||||
DankTextField {
|
||||
width: parent.width
|
||||
height: 40
|
||||
placeholderText: I18n.tr("Inherit", "inherit from global setting")
|
||||
placeholderText: I18n.tr("Inherit")
|
||||
enabled: !settingsColumn.isDisabled
|
||||
text: {
|
||||
const layout = DisplayConfigState.getNiriSetting(root.outputData, root.outputName, "layout", null);
|
||||
|
||||
@@ -308,7 +308,7 @@ StyledRect {
|
||||
return DisplayConfigState.getTransformLabel(pendingTransform);
|
||||
return DisplayConfigState.getTransformLabel(root.outputData?.logical?.transform ?? "Normal");
|
||||
}
|
||||
options: [I18n.tr("Normal", "display rotation option", true), I18n.tr("90°"), I18n.tr("180°"), I18n.tr("270°"), I18n.tr("Flipped"), I18n.tr("Flipped 90°"), I18n.tr("Flipped 180°"), I18n.tr("Flipped 270°")]
|
||||
options: [I18n.tr("Normal"), I18n.tr("90°"), I18n.tr("180°"), I18n.tr("270°"), I18n.tr("Flipped"), I18n.tr("Flipped 90°"), I18n.tr("Flipped 180°"), I18n.tr("Flipped 270°")]
|
||||
onValueChanged: value => DisplayConfigState.setPendingChange(root.outputName, "transform", DisplayConfigState.getTransformValue(value))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,6 +399,7 @@ Item {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("All displays")
|
||||
description: I18n.tr("Show on all connected displays")
|
||||
checked: {
|
||||
var prefs = root.getScreenPreferences(parent.componentId);
|
||||
return prefs.includes("all") || (typeof prefs[0] === "string" && prefs[0] === "all");
|
||||
@@ -419,6 +420,7 @@ Item {
|
||||
DankToggle {
|
||||
width: parent.width
|
||||
text: I18n.tr("Focused Monitor Only")
|
||||
description: I18n.tr("Show notifications only on the currently focused monitor")
|
||||
visible: parent.componentId === "notifications"
|
||||
checked: SettingsData.notificationFocusedMonitor
|
||||
onToggled: checked => SettingsData.set("notificationFocusedMonitor", checked)
|
||||
|
||||
@@ -37,7 +37,7 @@ Item {
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "dock_to_bottom"
|
||||
title: I18n.tr("Visibility")
|
||||
title: I18n.tr("Dock Visibility")
|
||||
settingKey: "dockVisibility"
|
||||
|
||||
SettingsToggleRow {
|
||||
@@ -228,6 +228,7 @@ Item {
|
||||
settingKey: "dockShowOverflowBadge"
|
||||
tags: ["dock", "overflow", "badge", "count", "indicator"]
|
||||
text: I18n.tr("Show Overflow Badge Count")
|
||||
description: I18n.tr("Displays count when overflow is active")
|
||||
checked: SettingsData.dockShowOverflowBadge
|
||||
onToggled: checked => SettingsData.set("dockShowOverflowBadge", checked)
|
||||
}
|
||||
@@ -460,7 +461,7 @@ Item {
|
||||
if (!PopoutService.colorPickerModal)
|
||||
return;
|
||||
PopoutService.colorPickerModal.selectedColor = SettingsData.dockLauncherLogoColorOverride;
|
||||
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Launcher Logo Color");
|
||||
PopoutService.colorPickerModal.pickerTitle = I18n.tr("Choose Dock Launcher Logo Color");
|
||||
PopoutService.colorPickerModal.onColorSelectedCallback = function (selectedColor) {
|
||||
SettingsData.set("dockLauncherLogoColorOverride", selectedColor);
|
||||
};
|
||||
@@ -682,6 +683,7 @@ Item {
|
||||
|
||||
SettingsButtonGroupRow {
|
||||
text: I18n.tr("Border Color")
|
||||
description: I18n.tr("Choose the border accent color")
|
||||
visible: SettingsData.dockBorderEnabled
|
||||
model: [I18n.tr("Surface", "color option"), I18n.tr("Secondary", "color option"), I18n.tr("Primary", "color option")]
|
||||
buttonPadding: Theme.spacingS
|
||||
|
||||
@@ -31,7 +31,7 @@ Item {
|
||||
SettingsCard {
|
||||
width: parent.width
|
||||
iconName: "frame_source"
|
||||
title: I18n.tr("General")
|
||||
title: I18n.tr("Frame")
|
||||
settingKey: "frameEnabled"
|
||||
|
||||
SettingsToggleRow {
|
||||
|
||||
@@ -122,6 +122,7 @@ Item {
|
||||
tags: ["gamma", "day", "temperature", "kelvin", "color"]
|
||||
width: parent.width - parent.leftPadding - parent.rightPadding
|
||||
text: I18n.tr("Day Temperature")
|
||||
description: I18n.tr("Color temperature for day time")
|
||||
minimum: SessionData.nightModeTemperature
|
||||
maximum: 10000
|
||||
step: 100
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user