1
0
mirror of https://github.com/AvengeMedia/DankMaterialShell.git synced 2026-08-01 19:18:28 -04:00

Squashed commit of the following:

commit 990d86d481
Author: bbedward <bbedward@gmail.com>
Date:   Sat Jul 18 10:43:22 2026 -0400

    flake: update-common

commit 526cb157fd
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 17:56:40 2026 -0400

    i18n: update sync scrirpt for dank-qml-common

commit 92ba96d9f9
Author: bbedward <bbedward@gmail.com>
Date:   Thu Jul 16 09:24:37 2026 -0400

    qs: integrate with dank-qml-common
This commit is contained in:
bbedward
2026-07-18 10:44:31 -04:00
parent 5dfd875b9e
commit 235f0668b8
128 changed files with 577 additions and 14706 deletions
+56
View File
@@ -0,0 +1,56 @@
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 }}
+2
View File
@@ -27,6 +27,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
+2
View File
@@ -10,6 +10,8 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v6
with:
submodules: recursive
- name: Install flatpak
run: sudo apt update && sudo apt install -y flatpak
+20
View File
@@ -42,6 +42,7 @@ jobs:
with:
ref: ${{ inputs.tag }}
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
@@ -191,11 +192,13 @@ 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
@@ -252,6 +255,7 @@ 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
@@ -299,6 +303,22 @@ 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/
+2
View File
@@ -36,6 +36,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Check for updates
id: check
@@ -199,6 +200,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Wait before OBS upload
run: sleep 3
+2
View File
@@ -36,6 +36,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Install dependencies
run: |
@@ -94,6 +95,7 @@ jobs:
uses: actions/checkout@v6
with:
fetch-depth: 0
submodules: recursive
- name: Set up Go
uses: actions/setup-go@v6
+3 -2
View File
@@ -83,6 +83,7 @@ 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"
@@ -225,7 +226,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 }}/archive/refs/tags/${ARCHIVE_TAG}.tar.gz"
curl -fsSL -o "$TARBALL" "https://github.com/${{ github.repository }}/releases/download/${ARCHIVE_TAG}/dms-source.tar.gz"
RELEASE_CHECKSUM="$(sha256sum "$TARBALL" | cut -d' ' -f1)"
rm -f "$TARBALL"
@@ -246,7 +247,7 @@ jobs:
# into $wrksrc (create_wrksrc=yes handles the rest).
SRC_CACHE="hostdir/sources/dms-git-${GIT_VER}"
mkdir -p "$SRC_CACHE"
tar -czf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
tar -czhf "${SRC_CACHE}/dms-git-${GIT_VER}.tar.gz" \
--exclude=void-packages \
--exclude=r2-repo \
--exclude=.git \
+3
View File
@@ -0,0 +1,3 @@
[submodule "dank-qml-common"]
path = dank-qml-common
url = https://github.com/AvengeMedia/dank-qml-common.git
+28
View File
@@ -6,6 +6,14 @@ 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
@@ -28,6 +36,24 @@ 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.
@@ -104,6 +130,8 @@ 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)
+8 -1
View File
@@ -35,6 +35,12 @@ 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)..."
@@ -43,8 +49,9 @@ 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 -r $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@cp -rL $(SHELL_DIR)/* $(SHELL_INSTALL_DIR)/
@rm -rf $(SHELL_INSTALL_DIR)/.git* $(SHELL_INSTALL_DIR)/.github
@echo "Shell files installed"
+20 -1
View File
@@ -250,6 +250,10 @@ 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()
@@ -291,6 +295,10 @@ 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
}
@@ -320,10 +328,21 @@ 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)
@@ -342,7 +361,7 @@ func offerReclone(dmsPath string) bool {
}
fmt.Println("Cloning fresh copy...")
cloneCmd := exec.Command("git", "clone", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd := exec.Command("git", "clone", "--recurse-submodules", "https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil {
+15 -2
View File
@@ -520,7 +520,7 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
Progress: 0.90,
Step: "Cloning DankMaterialShell...",
IsComplete: false,
CommandInfo: "git clone https://github.com/AvengeMedia/DankMaterialShell.git",
CommandInfo: "git clone --recurse-submodules 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",
cloneCmd := exec.CommandContext(ctx, "git", "clone", "--recurse-submodules",
"https://github.com/AvengeMedia/DankMaterialShell.git", dmsPath)
if err := cloneCmd.Run(); err != nil {
return fmt.Errorf("failed to clone DankMaterialShell: %w", err)
@@ -553,6 +553,8 @@ 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
@@ -591,6 +593,8 @@ func (m *ManualPackageInstaller) installDankMaterialShell(ctx context.Context, v
return nil
}
m.syncDMSSubmodules(ctx, dmsPath)
m.log("DankMaterialShell updated successfully (git variant)")
return nil
}
@@ -609,10 +613,19 @@ 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...")
+1
Submodule dank-qml-common added at 5942bc1c60
+1 -1
View File
@@ -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 -r quickshell/* debian/dms-git/usr/share/quickshell/dms/; \
cp -rL 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; \
+1 -1
View File
@@ -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 cf - --exclude=debian . ) | \
( cd $$SOURCE_DIR && tar chf - --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 && \
+1 -1
View File
@@ -3,7 +3,7 @@
<service name="download_url">
<param name="protocol">https</param>
<param name="host">github.com</param>
<param name="path">/AvengeMedia/DankMaterialShell/archive/refs/tags/v1.2.3.tar.gz</param>
<param name="path">/AvengeMedia/DankMaterialShell/releases/download/v1.2.3/dms-source.tar.gz</param>
<param name="filename">dms-source.tar.gz</param>
</service>
<!-- Download amd64 binary -->
+1 -1
View File
@@ -63,7 +63,7 @@ override_dh_auto_install:
mv "$$SOURCE_DIR" DankMaterialShell-$(UPSTREAM_VERSION); \
fi
if [ -d DankMaterialShell-$(UPSTREAM_VERSION) ]; then \
cp -r DankMaterialShell-$(UPSTREAM_VERSION)/quickshell/* debian/dms/usr/share/quickshell/dms/; \
cp -rL 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; \
+19 -19
View File
@@ -216,7 +216,7 @@ update_debian_dms_service() {
return 0
fi
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-source\.tar\.gz|/releases/download/v${base_version}/dms-source.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 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
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
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 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar -czhf "$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 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar -czhf "$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 --branch "${GIT_REVISION:-master}" "$GIT_URL" "$SOURCE_DIR" 2>/dev/null ||
git clone --depth 1 "$GIT_URL" "$SOURCE_DIR" 2>/dev/null; then
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
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 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$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 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$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 -cJf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$EXPECTED_DIR"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$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 -cJf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cJhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
elif [[ "$SOURCE0" == *.tar.bz2 ]]; then
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -cjhf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
else
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czf "$WORK_DIR/$SOURCE0" "$DIR_NAME"
tar --sort=name --mtime='2000-01-01 00:00:00' --owner=0 --group=0 -czhf "$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)"
+3 -3
View File
@@ -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/archive/refs/tags/v${VERSION}.tar.gz"; then
if wget -O dms-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.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/archive/refs/tags/v${VERSION}.tar.gz"; then
if wget -O dms-greeter-source.tar.gz "https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${VERSION}/dms-source.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 "https://github.com/$GIT_REPO.git" "$TEMP_CLONE"; then
if git clone --recurse-submodules --shallow-submodules "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)
+1 -1
View File
@@ -60,7 +60,7 @@ override_dh_auto_install:
# Install QML files from git clone
mkdir -p debian/dms-git/usr/share/quickshell/dms
cp -r dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
cp -rL dms-git-repo/* debian/dms-git/usr/share/quickshell/dms/
# Remove unnecessary directories
rm -rf debian/dms-git/usr/share/quickshell/dms/core
+1 -1
View File
@@ -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 -r DankMaterialShell-$(BASE_VERSION)/quickshell/* debian/dms-greeter/usr/share/quickshell/dms-greeter/
cp -rL 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 \
+1 -1
View File
@@ -63,7 +63,7 @@ override_dh_auto_install:
# Install QML files from source tarball
mkdir -p debian/dms/usr/share/quickshell/dms
cp -r DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
cp -rL DankMaterialShell-$(BASE_VERSION)/* debian/dms/usr/share/quickshell/dms/
# Remove unnecessary directories
rm -rf debian/dms/usr/share/quickshell/dms/core
+1 -1
View File
@@ -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/archive/refs/tags/v${version}.tar.gz"
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
depends="greetd quickshell acl-progs dbus elogind pam_rundir mesa-dri"
+1 -1
View File
@@ -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/archive/refs/tags/v${version}.tar.gz"
distfiles="https://github.com/AvengeMedia/DankMaterialShell/releases/download/v${version}/dms-source.tar.gz"
checksum=14f2678d6a15223ba069d1b8c8a21a5564b735d190c231f62ed44fd8bf48677c
# Optional feature deps are listed in distro/void/README.md.
Generated
+17
View File
@@ -1,5 +1,21 @@
{
"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": {
@@ -34,6 +50,7 @@
},
"root": {
"inputs": {
"dank-qml-common": "dank-qml-common",
"flake-compat": "flake-compat",
"nixpkgs": "nixpkgs"
}
+9
View File
@@ -7,12 +7,17 @@
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
@@ -125,6 +130,10 @@
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
+5 -1
View File
@@ -141,13 +141,15 @@ 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
── Common/ # Shared resources
│ └── DankCommon/ # Symlink to ../dank-qml-common/DankCommon
├── distro/ # Distribution packaging
│ ├── arch/ # AUR packages
│ ├── fedora/ # RPM specs
@@ -490,6 +492,8 @@ 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:
+5 -60
View File
@@ -1,66 +1,11 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.DankCommon.Common as DankCommon
Singleton {
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 {}
}
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
}
+2 -8
View File
@@ -1,9 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Common as DankCommon
// Reusable NumberAnimation wrapper
NumberAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
DankCommon.DankAnim {}
+2 -8
View File
@@ -1,9 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Common as DankCommon
// Reusable ColorAnimation wrapper
ColorAnimation {
duration: Theme.expressiveDurations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Theme.expressiveCurves.standard
}
DankCommon.DankColorAnim {}
+2 -63
View File
@@ -1,64 +1,3 @@
import QtQuick
import Quickshell.Io
import qs.DankCommon.Common as DankCommon
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++;
}
}
DankCommon.DankSocket {}
+2 -53
View File
@@ -1,54 +1,3 @@
pragma ComponentBehavior: Bound
import qs.DankCommon.Common as DankCommon
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)
}
}
DankCommon.ElevationShadow {}
+73 -12
View File
@@ -25,6 +25,7 @@ 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: ({
@@ -32,8 +33,11 @@ Singleton {
})
property var translations: ({})
property bool translationsLoaded: false
property var commonTranslations: ({})
property bool commonTranslationsLoaded: false
property url _selectedPath: ""
property url _commonSelectedPath: ""
FolderListModel {
id: dir
@@ -48,6 +52,18 @@ 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
@@ -69,6 +85,22 @@ 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);
@@ -117,29 +149,58 @@ 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 || !translations)
return term;
if (context && translations[context] && translations[context][term])
return translations[context][term];
if (translations[term] && translations[term][term])
return translations[term][term];
for (const c in translations) {
if (translations[c] && translations[c][term])
return translations[c][term];
if (translationsLoaded) {
const hit = _lookup(translations, term, context);
if (hit)
return hit;
}
if (commonTranslationsLoaded) {
const hit = _lookup(commonTranslations, term, context);
if (hit)
return hit;
}
return term;
}
function trContext(context, term) {
if (!translationsLoaded || !translations)
return term;
if (translations[context] && translations[context][term])
if (translationsLoaded && translations[context] && translations[context][term])
return translations[context][term];
if (commonTranslationsLoaded && commonTranslations[context] && commonTranslations[context][term])
return commonTranslations[context][term];
return term;
}
}
+6 -65
View File
@@ -1,72 +1,13 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import qs.Common
import qs.DankCommon.Common as DankCommon
// Reusable ListView/GridView transitions
Singleton {
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
}
}
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
}
+8
View File
@@ -108,6 +108,14 @@ 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 "";
+4 -148
View File
@@ -1,157 +1,13 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import qs.Services
import qs.DankCommon.Common as DankCommon
Singleton {
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: ({})
readonly property int noTimeout: DankCommon.Proc.noTimeout
readonly property string dmsBin: DankCommon.Proc.dmsBin
function 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 {}
}
DankCommon.Proc.runCommand(id, command, callback, debounceMs, timeoutMs);
}
}
+17 -4
View File
@@ -6,6 +6,7 @@ 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
@@ -1230,16 +1231,28 @@ Singleton {
property string fontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.getEffectiveFontFamily();
return resolvedFontFamily(GreetdSettings.getEffectiveFontFamily());
}
return typeof SettingsData !== "undefined" ? SettingsData.fontFamily : "Inter Variable";
return typeof SettingsData !== "undefined" ? resolvedFontFamily(SettingsData.fontFamily) : DankCommon.Fonts.sans;
}
property string monoFontFamily: {
if (typeof SessionData !== "undefined" && SessionData.isGreeterMode && typeof GreetdSettings !== "undefined") {
return GreetdSettings.monoFontFamily;
return resolvedMonoFontFamily(GreetdSettings.monoFontFamily);
}
return typeof SettingsData !== "undefined" ? SettingsData.monoFontFamily : "Fira Code";
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;
}
property int fontWeight: {
File diff suppressed because it is too large Load Diff
+1
View File
@@ -0,0 +1 @@
../dank-qml-common/DankCommon
@@ -1,935 +1,3 @@
import Qt.labs.folderlistmodel
import QtCore
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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
}
}
DankCommon.FileBrowserContent {}
@@ -1,252 +1,3 @@
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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;
}
}
}
}
DankCommon.FileBrowserGridDelegate {}
@@ -1,153 +1,3 @@
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Services
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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()
}
}
}
}
}
}
DankCommon.FileBrowserItemContextMenu {}
@@ -1,256 +1,3 @@
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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;
}
}
}
}
DankCommon.FileBrowserListDelegate {}
@@ -1,100 +1,3 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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
}
}
DankCommon.FileBrowserModal {}
@@ -1,130 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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
}
}
}
DankCommon.FileBrowserNavigation {}
@@ -1,127 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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);
}
}
}
}
}
}
}
DankCommon.FileBrowserOverwriteDialog {}
@@ -1,75 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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);
}
}
}
}
}
DankCommon.FileBrowserSaveRow {}
@@ -1,70 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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 ?? "")
}
}
}
}
}
DankCommon.FileBrowserSidebar {}
@@ -1,189 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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;
}
}
}
}
}
DankCommon.FileBrowserSortMenu {}
+2 -235
View File
@@ -1,236 +1,3 @@
import QtQuick
import Quickshell.Io
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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
}
}
}
DankCommon.FileInfo {}
@@ -1,50 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Modals.FileBrowser as DankCommon
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
}
}
}
DankCommon.KeyboardHints {}
+2 -6
View File
@@ -1,6 +1,7 @@
import QtQuick
import QtQuick.Effects
import qs.Common
import qs.DankCommon.Common as DankCommon
import qs.Services
import qs.Widgets
@@ -176,14 +177,9 @@ Item {
text: "DANK LINUX"
font.pixelSize: parent.compactLogo ? 32 : 48
font.weight: Font.Bold
font.family: interFont.name
font.family: DankCommon.Fonts.sans
color: Theme.surfaceText
antialiasing: true
FontLoader {
id: interFont
source: Qt.resolvedUrl("../../assets/fonts/inter/InterVariable.ttf")
}
}
}
+2
View File
@@ -35,6 +35,8 @@ Plugins let you add custom widgets to DankBar and Control Center. They're discov
- Supports left, center, and right sections
- Supports any dankbar position (top/left/right/bottom)
Many widgets are implemented in the shared [dank-qml-common](https://github.com/AvengeMedia/dank-qml-common) library and re-exported by DMS. Plugins should keep importing `qs.Common`, `qs.Services`, `qs.Widgets`, and `qs.Modules.Plugins` — these remain the supported plugin API and are unaffected by where a widget is implemented.
## Plugin Structure
Each plugin must be a directory in `$CONFIGPATH/DankMaterialShell/plugins/` containing:
@@ -1,54 +0,0 @@
#version 450
// Standalone rounded rect with border and M3 elevation shadow as one SDF.
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float borderWidth;
vec4 rectPx; // rounded rect in item px: x, y, w, h
vec4 cornerRadius; // topLeft, topRight, bottomRight, bottomLeft
vec4 fillColor; // straight (non-premultiplied) rgba
vec4 borderColor; // straight rgba
vec4 shadowColor; // straight rgba; a = 0 disables both shadow terms
vec4 shadowParam; // key: x = blur px, y = spread px, z,w = offset px
vec4 ambientParam; // ambient: x = blur px, y = spread px, z = alpha
} ubuf;
float sdRoundBox4(vec2 p, vec2 c, vec2 hs, vec4 r) {
p -= c;
float rr = (p.x >= 0.0) ? (p.y >= 0.0 ? r.z : r.y) : (p.y >= 0.0 ? r.w : r.x);
rr = min(rr, min(hs.x, hs.y));
vec2 q = abs(p) - hs + rr;
return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - rr;
}
float rectDist(vec2 px) {
vec2 hs = ubuf.rectPx.zw * 0.5;
return sdRoundBox4(px, ubuf.rectPx.xy + hs, hs, ubuf.cornerRadius);
}
void main() {
vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx);
float d = rectDist(px);
float fw = max(fwidth(d), 1e-4);
float cov = 1.0 - smoothstep(-fw, fw, d);
float covInner = 1.0 - smoothstep(-fw, fw, d + ubuf.borderWidth);
vec4 col = vec4(ubuf.fillColor.rgb, 1.0) * (ubuf.fillColor.a * covInner)
+ vec4(ubuf.borderColor.rgb, 1.0) * (ubuf.borderColor.a * max(0.0, cov - covInner));
if (ubuf.shadowColor.a > 0.0) {
float dk = rectDist(px - ubuf.shadowParam.zw) - ubuf.shadowParam.y;
float bk = max(ubuf.shadowParam.x, fw);
float covK = 1.0 - smoothstep(-bk, bk, dk);
float ba = max(ubuf.ambientParam.x, fw);
float covA = 1.0 - smoothstep(-ba, ba, d - ubuf.ambientParam.y);
float sh = 1.0 - (1.0 - covK * ubuf.shadowColor.a) * (1.0 - covA * ubuf.ambientParam.z);
col += vec4(ubuf.shadowColor.rgb, 1.0) * (sh * (1.0 - cov));
}
fragColor = col * ubuf.qt_Opacity;
}
-61
View File
@@ -1,61 +0,0 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float cornerRadiusPx;
float rippleCenterX;
float rippleCenterY;
float rippleRadius;
float rippleOpacity;
float offsetX;
float offsetY;
float parentWidth;
float parentHeight;
vec4 rippleCol;
} ubuf;
float sdRoundRect(vec2 p, vec2 b, float r) {
vec2 q = abs(p) - (b - vec2(r));
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
void main() {
if (ubuf.rippleOpacity <= 0.0 || ubuf.rippleRadius <= 0.0) {
fragColor = vec4(0.0);
return;
}
vec2 px = qt_TexCoord0 * vec2(ubuf.widthPx, ubuf.heightPx) + vec2(ubuf.offsetX, ubuf.offsetY);
float circleD = length(px - vec2(ubuf.rippleCenterX, ubuf.rippleCenterY)) - ubuf.rippleRadius;
float aaCircle = max(fwidth(circleD), 0.001);
float circleMask = 1.0 - smoothstep(-aaCircle, aaCircle, circleD);
if (circleMask <= 0.0) {
fragColor = vec4(0.0);
return;
}
float rrMask = 1.0;
if (ubuf.cornerRadiusPx > 0.0) {
vec2 halfSize = vec2(ubuf.parentWidth, ubuf.parentHeight) * 0.5;
float r = clamp(ubuf.cornerRadiusPx, 0.0, min(halfSize.x, halfSize.y));
float rrD = sdRoundRect(px - halfSize, halfSize, r);
float aaRR = max(fwidth(rrD), 0.001);
rrMask = 1.0 - smoothstep(-aaRR, aaRR, rrD);
}
float mask = circleMask * rrMask;
if (mask <= 0.0) {
fragColor = vec4(0.0);
return;
}
float a = ubuf.rippleCol.a * ubuf.rippleOpacity * mask * ubuf.qt_Opacity;
fragColor = vec4(ubuf.rippleCol.rgb * a, a);
}
-115
View File
@@ -1,115 +0,0 @@
#version 450
layout(location = 0) in vec2 qt_TexCoord0;
layout(location = 0) out vec4 fragColor;
layout(std140, binding = 0) uniform buf {
mat4 qt_Matrix;
float qt_Opacity;
float widthPx;
float heightPx;
float value;
float actualValue;
float phase;
float ampPx;
float wavelengthPx;
float lineWidthPx;
float showActual;
vec4 fillColor;
vec4 trackColor;
vec4 playheadColor;
vec4 actualColor;
} ubuf;
const float TAU = 6.28318530718;
const float AA = 0.75; // pixel-space antialias band
// Signed distance to a rounded box centered at the origin.
float sdRoundBar(vec2 p, vec2 halfSize, float r) {
vec2 q = abs(p) - halfSize + vec2(r);
return length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
}
// Composite a straight-alpha color over a premultiplied accumulator.
vec4 blendOver(vec4 dst, vec3 rgb, float a) {
return vec4(rgb * a + dst.rgb * (1.0 - a), a + dst.a * (1.0 - a));
}
void main() {
float w = ubuf.widthPx;
float h = ubuf.heightPx;
vec2 px = vec2(qt_TexCoord0.x * w, qt_TexCoord0.y * h);
float mid = h * 0.5;
float halfW = ubuf.lineWidthPx * 0.5;
float k = TAU / max(ubuf.wavelengthPx, 1e-3);
float playX = clamp(ubuf.value, 0.0, 1.0) * w;
float actualX = clamp(ubuf.actualValue, 0.0, 1.0) * w;
bool seeking = ubuf.showActual > 0.5;
float loX = min(playX, actualX);
float hiX = max(playX, actualX);
float fillEnd = seeking ? loX : playX; // filled progress ends here
float actStart = seeking ? loX : playX; // seek-preview segment
float actEnd = seeking ? hiX : playX;
float trackStart = seeking ? hiX : playX; // unplayed remainder
// Perpendicular distance to the animated sine stroke.
float ang = k * px.x + ubuf.phase;
float wy = mid + ubuf.ampPx * sin(ang);
float slope = ubuf.ampPx * k * cos(ang);
float dWave = abs(px.y - wy) / sqrt(1.0 + slope * slope);
float aaW = AA;
float waveStroke = 1.0 - smoothstep(halfW - aaW, halfW + aaW, dWave);
// Straight remainder line.
float dLine = abs(px.y - mid);
float aaL = AA;
float lineStroke = 1.0 - smoothstep(halfW - aaL, halfW + aaL, dLine);
vec4 col = vec4(0.0);
// 1. Track (unplayed remainder), to the right of the progress head.
{
float m = lineStroke * step(trackStart, px.x);
col = blendOver(col, ubuf.trackColor.rgb, ubuf.trackColor.a * m);
}
// 2. Seek-preview segment (only while seeking).
if (seeking) {
float m = waveStroke * step(actStart, px.x) * step(px.x, actEnd);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 3. Filled progress wave.
{
float m = waveStroke * step(halfW, px.x) * step(px.x, fillEnd);
// Rounded start cap.
float capS = length(px - vec2(halfW, mid + ubuf.ampPx * sin(k * halfW + ubuf.phase))) - halfW;
float capM = 1.0 - smoothstep(-aaW, aaW, capS);
m = max(m, capM * step(halfW - 1.0, px.x));
col = blendOver(col, ubuf.fillColor.rgb, ubuf.fillColor.a * m);
}
// 4. Actual-position marker (only while seeking).
if (seeking) {
float amH = max(ubuf.lineWidthPx + 4.0, 10.0);
float d = sdRoundBar(px - vec2(actualX, mid), vec2(1.0, amH * 0.5), 1.0);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.actualColor.rgb, ubuf.actualColor.a * m);
}
// 5. Playhead pill (on top).
{
float phW = 3.5;
float phH = max(ubuf.lineWidthPx + 12.0, 16.0);
float d = sdRoundBar(px - vec2(playX, mid), vec2(phW * 0.5, phH * 0.5), phW * 0.5);
float aa = AA;
float m = 1.0 - smoothstep(-aa, aa, d);
col = blendOver(col, ubuf.playheadColor.rgb, ubuf.playheadColor.a * m);
}
fragColor = col * ubuf.qt_Opacity;
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -128
View File
@@ -1,129 +1,3 @@
import QtQuick
import Quickshell.Widgets
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
required property string iconValue
required property int iconSize
property string fallbackText: "A"
property color iconColor: Theme.surfaceText
property color colorOverride: "transparent"
property real brightnessOverride: 0.0
property real contrastOverride: 0.0
property real saturationOverride: 0.0
property color fallbackBackgroundColor: Theme.surfaceLight
property color fallbackTextColor: Theme.primary
property real materialIconSizeAdjustment: Theme.spacingM
property real unicodeIconScale: 0.7
property real fallbackTextScale: 0.4
property real iconMargins: 0
property real fallbackLeftMargin: 0
property real fallbackRightMargin: 0
property real fallbackTopMargin: 0
property real fallbackBottomMargin: 0
readonly property bool isMaterial: iconValue && iconValue.startsWith("material:")
readonly property bool isUnicode: iconValue && iconValue.startsWith("unicode:")
readonly property bool isSvgCorner: iconValue && iconValue.startsWith("svg+corner:")
readonly property bool isSvg: iconValue && !isSvgCorner && iconValue.startsWith("svg:")
readonly property bool isImage: iconValue && iconValue.startsWith("image:")
readonly property bool hasColorOverride: colorOverride.a > 0
readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
readonly property string imagePath: isImage ? iconValue.substring(6) : ""
readonly property string svgSource: {
if (isSvgCorner) {
const parts = iconValue.substring(11).split("|");
return parts[0] || "";
}
if (isSvg)
return iconValue.substring(4);
return "";
}
readonly property string svgCornerIcon: isSvgCorner ? (iconValue.substring(11).split("|")[1] || "") : ""
readonly property bool hasSpecialPrefix: isMaterial || isUnicode || isSvg || isSvgCorner || isImage
readonly property string iconPath: {
if (hasSpecialPrefix || !iconValue)
return "";
return Paths.resolveIconPath(iconValue);
}
visible: iconValue !== undefined && iconValue !== ""
DankIcon {
anchors.centerIn: parent
name: root.materialName
size: root.iconSize - root.materialIconSizeAdjustment
color: root.hasColorOverride ? root.colorOverride : root.iconColor
visible: root.isMaterial
}
StyledText {
anchors.centerIn: parent
text: root.unicodeChar
font.pixelSize: root.iconSize * root.unicodeIconScale
color: root.hasColorOverride ? root.colorOverride : root.iconColor
visible: root.isUnicode
}
DankSVGIcon {
anchors.centerIn: parent
source: root.svgSource
size: root.iconSize
cornerIcon: root.svgCornerIcon
colorOverride: root.colorOverride
brightnessOverride: root.brightnessOverride
contrastOverride: root.contrastOverride
saturationOverride: root.saturationOverride
visible: root.isSvg || root.isSvgCorner
}
CachingImage {
id: cachingImg
anchors.fill: parent
imagePath: root.imagePath
maxCacheSize: root.iconSize * 2
animate: false
visible: root.isImage && status === Image.Ready
}
Loader {
id: iconImgLoader
anchors.fill: parent
anchors.margins: root.iconMargins
active: !root.hasSpecialPrefix && root.iconPath !== ""
sourceComponent: IconImage {
anchors.fill: parent
source: root.iconPath
backer.sourceSize: Qt.size(root.iconSize * 2, root.iconSize * 2)
mipmap: true
asynchronous: true
visible: status === Image.Ready
}
}
Rectangle {
id: fallbackRect
anchors.fill: parent
anchors.leftMargin: root.fallbackLeftMargin
anchors.rightMargin: root.fallbackRightMargin
anchors.topMargin: root.fallbackTopMargin
anchors.bottomMargin: root.fallbackBottomMargin
visible: !root.hasSpecialPrefix && (root.iconPath === "" || !iconImgLoader.item || iconImgLoader.item.status !== Image.Ready)
color: root.fallbackBackgroundColor
radius: Theme.cornerRadius
border.width: 0
border.color: Theme.primarySelected
StyledText {
anchors.centerIn: parent
text: root.fallbackText
font.pixelSize: root.iconSize * root.fallbackTextScale
color: root.fallbackTextColor
font.weight: Font.Bold
}
}
}
DankCommon.AppIconRenderer {}
+2 -131
View File
@@ -1,132 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string imagePath: ""
property int maxCacheSize: 512
property int status: isAnimated ? animatedImg.status : staticImg.status
property int fillMode: Image.PreserveAspectCrop
// AnimatedImage decodes full-size on the GUI thread and is never cached;
// disable for thumbnail grids
property bool animate: true
property bool _fromCache: false
readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
readonly property bool isAnimated: {
if (!animate || !imagePath)
return false;
const lower = imagePath.toLowerCase();
return lower.endsWith(".gif") || lower.endsWith(".webp");
}
readonly property string normalizedPath: {
if (!imagePath)
return "";
if (isRemoteUrl)
return imagePath;
if (imagePath.startsWith("file://"))
return imagePath.substring(7);
return imagePath;
}
function djb2Hash(str) {
if (!str)
return "";
let hash = 5381;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) + hash) + str.charCodeAt(i);
hash = hash & 0x7FFFFFFF;
}
return hash.toString(16).padStart(8, '0');
}
readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
readonly property string cacheFileName: imageHash && !isRemoteUrl && !isAnimated ? `${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
readonly property string cachePath: cacheFileName ? `${Paths.stringify(Paths.imagecache)}/${cacheFileName}` : ""
readonly property string encodedImagePath: {
if (!normalizedPath)
return "";
if (isRemoteUrl)
return normalizedPath;
return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
}
AnimatedImage {
id: animatedImg
anchors.fill: parent
visible: root.isAnimated
asynchronous: true
fillMode: root.fillMode
source: root.isAnimated ? root.imagePath : ""
playing: visible && status === AnimatedImage.Ready
}
Image {
id: staticImg
anchors.fill: parent
visible: !root.isAnimated
asynchronous: true
fillMode: root.fillMode
sourceSize.width: root.maxCacheSize
sourceSize.height: root.maxCacheSize
smooth: true
onStatusChanged: {
switch (status) {
case Image.Error:
if (!root._fromCache)
return;
root._fromCache = false;
source = root.encodedImagePath;
return;
case Image.Ready:
if (root._fromCache || root.isRemoteUrl || !root.cachePath)
return;
if (!visible || width <= 0 || height <= 0 || !Window.window?.visible)
return;
const grabPath = root.cachePath;
grabToImage(res => {
res.saveToFile(grabPath);
});
return;
}
}
}
// Derives everything from a local snapshot of imagePath: sibling property
// bindings (isRemoteUrl, encodedImagePath, ...) are still stale when
// onImagePathChanged runs, so reading them here routes remote URLs down
// the local-file branch on the first path change
function resolveSource() {
const path = imagePath;
if (!path) {
_fromCache = false;
staticImg.source = "";
return;
}
const lower = path.toLowerCase();
if (animate && (lower.endsWith(".gif") || lower.endsWith(".webp")))
return;
if (path.startsWith("http://") || path.startsWith("https://")) {
_fromCache = false;
staticImg.source = path;
return;
}
const stripped = path.startsWith("file://") ? path.substring(7) : path;
const encoded = "file://" + stripped.split('/').map(s => encodeURIComponent(s)).join('/');
const hash = djb2Hash(stripped);
if (!hash) {
_fromCache = false;
staticImg.source = encoded;
return;
}
// Cache-first; a miss errors and falls back to encodedImagePath
_fromCache = true;
staticImg.source = `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png`;
}
onImagePathChanged: resolveSource()
// During creation onImagePathChanged fires before sibling properties (maxCacheSize) initialize
onCachePathChanged: resolveSource()
}
DankCommon.CachingImage {}
+2 -44
View File
@@ -1,45 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
StyledRect {
id: root
property string iconName: ""
property int iconSize: Theme.iconSize - 4
property color iconColor: Theme.surfaceText
property color backgroundColor: "transparent"
property bool circular: true
property int buttonSize: 32
property var tooltipText: null
property string tooltipSide: "bottom"
readonly property alias pressed: stateLayer.pressed
signal clicked
signal entered
signal exited
width: buttonSize
height: buttonSize
radius: Theme.cornerRadius
color: backgroundColor
DankIcon {
anchors.centerIn: parent
name: root.iconName
size: root.iconSize
color: root.iconColor
}
StateLayer {
id: stateLayer
disabled: !root.enabled
stateColor: Theme.primary
cornerRadius: root.radius
onClicked: root.clicked()
onEntered: root.entered()
onExited: root.exited()
tooltipText: root.tooltipText
tooltipSide: root.tooltipSide
}
}
DankCommon.DankActionButton {}
+2 -27
View File
@@ -1,28 +1,3 @@
import QtQuick
import qs.DankCommon.Widgets as DankCommon
SequentialAnimation {
id: root
property Item target
property real minOpacity: 0.3
property int pulseDuration: 600
loops: Animation.Infinite
NumberAnimation {
target: root.target
property: "opacity"
to: root.minOpacity
duration: root.pulseDuration
easing.type: Easing.InOutQuad
}
NumberAnimation {
target: root.target
property: "opacity"
to: 1.0
duration: root.pulseDuration
easing.type: Easing.InOutQuad
}
onStopped: if (root.target) root.target.opacity = 1.0
}
DankCommon.DankBlink {}
+2 -98
View File
@@ -1,99 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string text: ""
property string iconName: ""
property int iconSize: Theme.iconSizeSmall
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property color backgroundColor: Theme.buttonBg
property color textColor: Theme.buttonText
property int buttonHeight: 40
property int horizontalPadding: Theme.spacingL
property bool enableScaleAnimation: false
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
signal clicked
width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
height: buttonHeight
radius: Theme.cornerRadius
color: backgroundColor
opacity: enabled ? 1 : 0.4
scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0
Behavior on scale {
enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: 100
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
radius: parent.radius
color: {
if (pressed)
return Theme.withAlpha(root.textColor, 0.20);
if (hovered)
return Theme.withAlpha(root.textColor, 0.12);
return Theme.withAlpha(root.textColor, 0);
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: rippleLayer
rippleColor: root.textColor
cornerRadius: root.radius
enableRipple: root.enableRipple
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingS
DankIcon {
name: root.iconName
size: root.iconSize
color: root.textColor
visible: root.iconName !== ""
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.text
font.pixelSize: Theme.fontSizeMedium
font.weight: Font.Medium
color: root.textColor
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: root.enabled
onPressed: mouse => {
if (root.enableRipple)
rippleLayer.trigger(mouse.x, mouse.y);
}
onClicked: root.clicked()
}
}
DankCommon.DankButton {}
+2 -270
View File
@@ -1,271 +1,3 @@
import QtQuick
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Row {
id: root
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property var model: []
property int currentIndex: -1
property string selectionMode: "single"
property bool multiSelect: selectionMode === "multi"
property var initialSelection: []
property var currentSelection: initialSelection
property bool checkEnabled: true
property string size: "medium"
property int buttonHeight: size === "small" ? 32 : 40
property int minButtonWidth: size === "small" ? 56 : 64
property int buttonPadding: size === "small" ? Theme.spacingM : Theme.spacingL
property int checkIconSize: size === "small" ? Theme.iconSizeSmall - 2 : Theme.iconSizeSmall
property int textSize: size === "small" ? Theme.fontSizeSmall : Theme.fontSizeMedium
property bool userInteracted: false
property bool usePopupTransparency: !checkParentDisablesTransparency()
property real maximumWidth: -1
readonly property real _segmentCap: {
const count = model?.length ?? 0;
if (maximumWidth <= 0 || count === 0)
return -1;
return (maximumWidth - spacing * (count - 1)) / count - 4;
}
signal selectionChanged(int index, bool selected)
signal animationCompleted
spacing: Theme.spacingXS
Timer {
id: animationTimer
interval: Theme.shortDuration
onTriggered: {
root.userInteracted = false;
root.animationCompleted();
}
}
function isSelected(index) {
if (multiSelect) {
return repeater.itemAt(index)?.selected || false;
}
return index === currentIndex;
}
function selectItem(index) {
userInteracted = true;
if (multiSelect) {
const modelValue = model[index];
let newSelection = [...currentSelection];
const isCurrentlySelected = newSelection.includes(modelValue);
if (isCurrentlySelected) {
newSelection = newSelection.filter(item => item !== modelValue);
} else {
newSelection.push(modelValue);
}
currentSelection = newSelection;
selectionChanged(index, !isCurrentlySelected);
animationTimer.restart();
} else {
const oldIndex = currentIndex;
selectionChanged(index, true);
if (oldIndex !== index && oldIndex >= 0) {
selectionChanged(oldIndex, false);
}
animationTimer.restart();
}
}
Repeater {
id: repeater
model: ScriptModel {
values: root.model
}
delegate: Rectangle {
id: segment
property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property bool isFirst: index === 0
property bool isLast: index === repeater.count - 1
property bool visualFirst: I18n.isRtl ? isLast : isFirst
property bool visualLast: I18n.isRtl ? isFirst : isLast
property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
readonly property real contentNaturalWidth: (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0) + buttonText.implicitWidth
width: {
const natural = Math.max(contentNaturalWidth + root.buttonPadding * 2, root.minButtonWidth);
const capped = root._segmentCap > 0 ? Math.min(natural, Math.max(root._segmentCap, root.minButtonWidth)) : natural;
return capped + (selected ? 4 : 0);
}
height: root.buttonHeight
color: selected ? Theme.buttonBg : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency) : Theme.surfaceVariant)
border.color: "transparent"
border.width: 0
topLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
topRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
bottomRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
Behavior on width {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on topRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomLeftRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on bottomRightRadius {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on color {
enabled: root.userInteracted
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
topLeftRadius: parent.topLeftRadius
bottomLeftRadius: parent.bottomLeftRadius
topRightRadius: parent.topRightRadius
bottomRightRadius: parent.bottomRightRadius
color: {
if (pressed)
return selected ? Theme.buttonPressed : Theme.surfaceTextHover;
if (hovered)
return selected ? Theme.buttonHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: segmentRipple
cornerRadius: Theme.cornerRadius
rippleColor: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
}
Item {
id: contentItem
anchors.centerIn: parent
implicitWidth: contentRow.implicitWidth
implicitHeight: contentRow.implicitHeight
Row {
id: contentRow
spacing: Theme.spacingS
DankIcon {
id: checkIcon
name: "check"
size: root.checkIconSize
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
visible: root.checkEnabled && segment.selected
opacity: segment.selected ? 1 : 0
scale: segment.selected ? 1 : 0.6
anchors.verticalCenter: parent.verticalCenter
Behavior on opacity {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on scale {
enabled: root.userInteracted
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
}
}
StyledText {
id: buttonText
readonly property real capAvailable: {
if (root._segmentCap <= 0)
return -1;
const cap = Math.max(root._segmentCap, root.minButtonWidth);
return Math.max(0, cap - root.buttonPadding * 2 - (checkIcon.visible ? checkIcon.width + contentRow.spacing : 0));
}
text: typeof modelData === "string" ? modelData : modelData.text || ""
font.pixelSize: root.textSize
font.weight: segment.selected ? Font.Medium : Font.Normal
color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
width: capAvailable < 0 ? implicitWidth : Math.min(implicitWidth, capAvailable)
maximumLineCount: 1
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => segmentRipple.trigger(mouse.x, mouse.y)
onClicked: root.selectItem(index)
}
}
}
}
DankCommon.DankButtonGroup {}
+2 -130
View File
@@ -1,131 +1,3 @@
import QtQuick
import QtQuick.Window
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string imageSource: ""
property string fallbackIcon: "notifications"
property string fallbackText: ""
property bool cacheImages: true
property bool hasImage: imageSource !== ""
readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
readonly property bool probeSettled: probe.status === Image.Ready || probe.status === Image.Error
readonly property var activeImage: {
if (isAnimated)
return probe;
if (staticImage.status === Image.Ready)
return staticImage;
if (probe.status === Image.Ready && probe.source !== "")
return probe;
return staticImage;
}
property int imageStatus: activeImage.status
signal imageSaved(string filePath)
property string _pendingSavePath: ""
property var _attachedWindow: root.Window.window
on_AttachedWindowChanged: {
if (_attachedWindow && _pendingSavePath !== "") {
Qt.callLater(function () {
if (root._pendingSavePath !== "") {
let path = root._pendingSavePath;
root._pendingSavePath = "";
root.saveImageToFile(path);
}
});
}
}
function saveImageToFile(filePath) {
if (activeImage.status !== Image.Ready)
return false;
if (!activeImage.Window.window) {
_pendingSavePath = filePath;
return true;
}
activeImage.grabToImage(function (result) {
if (result && result.saveToFile(filePath)) {
root.imageSaved(filePath);
}
});
return true;
}
radius: width / 2
color: Theme.primaryHover
border.color: "transparent"
border.width: 0
ClippingRectangle {
anchors.fill: parent
anchors.margins: 2
radius: Math.min(width, height) / 2
color: "transparent"
// Probes as AnimatedImage to read frameCount; retires once staticImage is ready.
AnimatedImage {
id: probe
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === probe && probe.status === Image.Ready && root.imageSource !== ""
source: root.shouldProbe && (root.isAnimated || staticImage.status !== Image.Ready) ? root.imageSource : ""
}
// Takes over once the probe settles on a non-animated image, then latches.
Image {
id: staticImage
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
smooth: true
mipmap: true
cache: root.cacheImages
visible: root.activeImage === staticImage && staticImage.status === Image.Ready && root.imageSource !== ""
sourceSize.width: Math.max(width * 2, 128)
sourceSize.height: Math.max(height * 2, 128)
source: {
if (!root.shouldProbe)
return root.imageSource;
if ((root.probeSettled && !root.isAnimated) || staticImage.status !== Image.Null)
return root.imageSource;
return "";
}
}
}
AppIconRenderer {
anchors.centerIn: parent
width: Math.round(parent.width * 0.75)
height: width
visible: (root.activeImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
iconValue: root.fallbackIcon
iconSize: width
iconColor: Theme.surfaceVariantText
materialIconSizeAdjustment: 0
fallbackText: root.fallbackText
fallbackBackgroundColor: "transparent"
fallbackTextColor: Theme.surfaceVariantText
}
StyledText {
anchors.centerIn: parent
visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
text: root.fallbackText
font.pixelSize: Math.max(12, parent.width * 0.5)
font.weight: Font.Bold
color: Theme.surfaceVariantText
}
}
DankCommon.DankCircularImage {}
+2 -132
View File
@@ -1,133 +1,3 @@
import QtQuick
import QtQuick.Layouts
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
ColumnLayout {
id: root
required property string title
property string description: ""
property bool expanded: false
property bool showBackground: false
property alias headerColor: headerRect.color
signal toggleRequested
spacing: Theme.spacingS
Layout.fillWidth: true
Rectangle {
id: headerRect
Layout.fillWidth: true
Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48)
radius: Theme.cornerRadius
color: "transparent"
RowLayout {
id: titleRow
anchors.fill: parent
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
spacing: Theme.spacingM
StyledText {
text: root.title
font.pixelSize: Theme.fontSizeLarge
font.weight: Font.Medium
Layout.fillWidth: true
}
DankIcon {
name: "expand_more"
size: Theme.iconSizeSmall
rotation: root.expanded ? 180 : 0
Behavior on rotation {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
}
StateLayer {
anchors.fill: parent
onClicked: {
root.toggleRequested();
root.expanded = !root.expanded;
}
}
}
default property alias content: contentColumn.data
Item {
id: contentWrapper
Layout.fillWidth: true
Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0
clip: true
Behavior on Layout.preferredHeight {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankAnim {
duration: Theme.expressiveDurations.expressiveDefaultSpatial
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
Rectangle {
id: backgroundRect
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.surfaceContainer
opacity: root.showBackground && root.expanded ? 1.0 : 0.0
visible: root.showBackground
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
ColumnLayout {
id: contentColumn
anchors.left: parent.left
anchors.right: parent.right
y: Theme.spacingS
anchors.leftMargin: Theme.paddingM
anchors.rightMargin: Theme.paddingM
anchors.bottomMargin: Theme.spacingS
spacing: Theme.spacingS
opacity: root.expanded ? 1.0 : 0.0
Behavior on opacity {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
NumberAnimation {
easing.type: Easing.BezierSpline
duration: Theme.shortDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
StyledText {
id: descriptionText
Layout.fillWidth: true
Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0
Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0
visible: root.description !== ""
text: root.description
color: Theme.surfaceTextSecondary
font.pixelSize: Theme.fontSizeSmall
wrapMode: Text.Wrap
}
}
}
}
DankCommon.DankCollapsibleSection {}
+2 -61
View File
@@ -1,62 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
// Premultiplied-alpha color tween: bind `to`, read `value`. Plain
// ColorAnimation lerps raw RGBA and flashes when the endpoints differ
// in both color and alpha (translucent <-> opaque).
QtObject {
id: root
property color to
property bool animated: true
property int duration: Theme.mediumDuration
property int easingType: Theme.emphasizedEasing
property color _from: to
property color _target: to
property real _mix: 1
property bool _ready: false
readonly property color value: {
const from = _from;
const target = _target;
const alpha = from.a + (target.a - from.a) * _mix;
if (alpha <= 0)
return Qt.rgba(target.r, target.g, target.b, 0);
const mix = (a, b) => (a * from.a + (b * target.a - a * from.a) * _mix) / alpha;
return Qt.rgba(mix(from.r, target.r), mix(from.g, target.g), mix(from.b, target.b), alpha);
}
readonly property NumberAnimation _anim: NumberAnimation {
target: root
property: "_mix"
from: 0
to: 1
duration: root.duration
easing.type: root.easingType
}
onToChanged: {
if (!_ready || !animated) {
_anim.stop();
_from = to;
_target = to;
_mix = 1;
return;
}
if (Qt.colorEqual(to, _target))
return;
const current = value;
_anim.stop();
_from = current;
_target = to;
_mix = 0;
_anim.restart();
}
Component.onCompleted: {
_from = to;
_target = to;
_ready = true;
}
}
DankCommon.DankColorAnimation {}
+2 -44
View File
@@ -1,45 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property color swatchColor: "transparent"
property color ringColor: Theme.outline
property real minPreviewAlpha: 0.4
readonly property bool translucent: swatchColor.a > 0 && swatchColor.a < 1
readonly property color displayColor: translucent ? Theme.withAlpha(swatchColor, Math.max(swatchColor.a, minPreviewAlpha)) : swatchColor
Loader {
anchors.fill: parent
active: root.translucent
sourceComponent: Component {
Canvas {
onPaint: {
const ctx = getContext("2d");
ctx.reset();
ctx.beginPath();
ctx.arc(width / 2, height / 2, width / 2, 0, 2 * Math.PI);
ctx.clip();
const s = Math.max(2, Math.round(width / 4));
for (let y = 0; y < height; y += s) {
for (let x = 0; x < width; x += s) {
ctx.fillStyle = (((x / s) + (y / s)) % 2 === 0) ? "#ffffff" : "#bdbdbd";
ctx.fillRect(x, y, s, s);
}
}
}
onVisibleChanged: if (visible)
requestPaint()
}
}
}
Rectangle {
anchors.fill: parent
radius: width / 2
color: root.displayColor
border.color: root.ringColor
border.width: 1
}
}
DankCommon.DankColorSwatch {}
+2 -554
View File
@@ -1,555 +1,3 @@
import "../Common/fzf.js" as Fzf
import QtQuick
import QtQuick.Controls
import Quickshell
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property string text: ""
property string description: ""
property string currentValue: ""
property var options: []
property var optionIcons: []
property bool enableFuzzySearch: false
property var optionIconMap: ({})
property var optionColorMap: ({})
function rebuildIconMap() {
const map = {};
for (let i = 0; i < options.length; i++) {
if (optionIcons.length > i)
map[options[i]] = optionIcons[i];
}
optionIconMap = map;
}
onOptionsChanged: rebuildIconMap()
onOptionIconsChanged: rebuildIconMap()
property int popupWidthOffset: 0
property int maxPopupHeight: 400
property bool openUpwards: false
property int popupWidth: 0
property bool alignPopupRight: false
property int dropdownWidth: 200
property bool compactMode: text === "" && description === ""
property bool showTrigger: true
property Item popupAnchorItem: null
property bool addHorizontalPadding: false
property string emptyText: ""
property bool usePopupTransparency: !checkParentDisablesTransparency()
property var transientSurfaceTracker: null
signal valueChanged(string value)
property bool menuOpen: false
onMenuOpenChanged: transientSurfaceTracker?.setActive(root, menuOpen, null)
function closeDropdownMenu() {
if (!root.menuOpen && !dropdownMenu.opened && !dropdownMenu.visible)
return;
root.menuOpen = false;
dropdownMenu.close();
}
function positionDropdownMenu() {
let currentIndex = root.options.indexOf(root.currentValue);
listView.positionViewAtIndex(currentIndex >= 0 ? currentIndex : 0, ListView.Beginning);
const anchorItem = root.popupAnchorItem || dropdown;
const pos = anchorItem.mapToItem(Overlay.overlay, 0, 0);
const popupW = dropdownMenu.width;
const popupH = dropdownMenu.height;
const overlayH = Overlay.overlay.height;
const goUp = root.openUpwards || pos.y + anchorItem.height + popupH + 4 > overlayH;
dropdownMenu.x = root.alignPopupRight ? pos.x + anchorItem.width - popupW : pos.x - (root.popupWidthOffset / 2);
dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + anchorItem.height + 4;
}
function showDropdownMenu() {
if (root.options.length === 0)
return;
if (root.menuOpen)
return;
root.menuOpen = true;
dropdownMenu.open();
positionDropdownMenu();
if (root.enableFuzzySearch)
searchField.forceActiveFocus();
}
function openDropdownMenu() {
if (root.menuOpen) {
closeDropdownMenu();
return;
}
showDropdownMenu();
}
function resetSearch() {
searchField.text = "";
dropdownMenu.fzfFinder = null;
dropdownMenu.searchQuery = "";
dropdownMenu.selectedIndex = -1;
}
width: !showTrigger ? 0 : (compactMode ? dropdownWidth : parent.width)
implicitHeight: !showTrigger ? 0 : (compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM))
Component.onDestruction: {
transientSurfaceTracker?.unregister(root);
if (root.menuOpen || dropdownMenu.opened || dropdownMenu.visible)
dropdownMenu.close();
}
Connections {
target: root.transientSurfaceTracker
ignoreUnknownSignals: true
function onCloseRequested() {
root.closeDropdownMenu();
}
}
Column {
id: labelColumn
anchors.left: parent.left
anchors.right: dropdown.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
anchors.rightMargin: Theme.spacingL
spacing: Theme.spacingXS
visible: !root.compactMode && root.showTrigger
StyledText {
text: root.text
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
font.weight: Font.Medium
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: root.description
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceVariantText
visible: description.length > 0
wrapMode: Text.WordWrap
width: parent.width
horizontalAlignment: Text.AlignLeft
}
}
Rectangle {
id: dropdown
visible: root.showTrigger
width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
height: 40
anchors.right: parent.right
anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.surfaceContainer)
border.color: dropdownMenu.visible ? Theme.primary : Theme.outlineHeavy
border.width: dropdownMenu.visible ? 2 : 1
MouseArea {
id: dropdownArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: root.openDropdownMenu()
}
Row {
id: contentRow
anchors.left: parent.left
anchors.right: expandIcon.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingS
spacing: Theme.spacingS
DankColorSwatch {
id: triggerSwatch
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: root.optionColorMap[root.currentValue] !== undefined
swatchColor: visible ? root.optionColorMap[root.currentValue] : "transparent"
}
DankIcon {
id: triggerIcon
name: root.optionIconMap[root.currentValue] ?? ""
size: 18
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: root.currentValue !== "" ? root.currentValue : root.emptyText
font.pixelSize: Theme.fontSizeMedium
color: root.currentValue !== "" ? Theme.surfaceText : Theme.outline
width: contentRow.width - (triggerSwatch.visible ? triggerSwatch.width + contentRow.spacing : 0) - (triggerIcon.visible ? triggerIcon.width + contentRow.spacing : 0)
elide: Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
}
DankIcon {
id: expandIcon
name: dropdownMenu.visible ? "expand_less" : "expand_more"
size: 20
color: Theme.surfaceText
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: Theme.spacingS
Behavior on rotation {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
Popup {
id: dropdownMenu
property string searchQuery: ""
property var filteredOptions: {
if (!root.enableFuzzySearch || searchQuery.length === 0)
return root.options;
if (!fzfFinder)
return root.options;
return fzfFinder.find(searchQuery).map(r => r.item);
}
property int selectedIndex: -1
property var fzfFinder: null
function initFinder() {
fzfFinder = new Fzf.Finder(root.options, {
"selector": option => option,
"limit": 50,
"casing": "case-insensitive",
"sort": true,
"tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
});
}
function selectNext() {
if (filteredOptions.length === 0)
return;
selectedIndex = (selectedIndex + 1) % filteredOptions.length;
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
}
function selectPrevious() {
if (filteredOptions.length === 0)
return;
selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1;
listView.positionViewAtIndex(selectedIndex, ListView.Contain);
}
function selectCurrent() {
if (selectedIndex < 0 || selectedIndex >= filteredOptions.length)
return;
root.currentValue = filteredOptions[selectedIndex];
root.valueChanged(filteredOptions[selectedIndex]);
close();
}
onOpened: {
root.menuOpen = true;
selectedIndex = -1;
if (searchField.text.length > 0) {
initFinder();
searchQuery = searchField.text;
} else {
fzfFinder = null;
searchQuery = "";
}
}
onClosed: root.menuOpen = false
parent: root.Overlay.overlay
width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
height: {
let h = root.enableFuzzySearch ? 54 : 0;
if (root.options.length === 0 && root.emptyText !== "")
h += 32;
else
h += Math.min(filteredOptions.length, 10) * 36;
return Math.min(root.maxPopupHeight, h + 16);
}
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
enter: Transition {
NumberAnimation {
property: "scale"
from: 0.9
to: 1
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
exit: Transition {
NumberAnimation {
property: "scale"
from: 1
to: 0.9
duration: Theme.shortDuration
easing.type: Theme.emphasizedEasing
}
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: contentSurface
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
color: Theme.withAlpha(Theme.surfaceContainer, 1)
border.color: Theme.primary
border.width: 2
radius: Theme.cornerRadius
ElevationShadow {
id: shadowLayer
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: contentSurface.radius
targetColor: contentSurface.color
borderColor: contentSurface.border.color
borderWidth: contentSurface.border.width
shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
}
Column {
anchors.fill: parent
anchors.margins: Theme.spacingS
Rectangle {
id: searchContainer
width: parent.width
height: 42
visible: root.enableFuzzySearch
radius: Theme.cornerRadius
color: root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
DankTextField {
id: searchField
anchors.fill: parent
anchors.margins: 1
placeholderText: I18n.tr("Search...")
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
onTextChanged: searchDebounce.restart()
Keys.onDownPressed: dropdownMenu.selectNext()
Keys.onUpPressed: dropdownMenu.selectPrevious()
Keys.onReturnPressed: dropdownMenu.selectCurrent()
Keys.onEnterPressed: dropdownMenu.selectCurrent()
Keys.onPressed: event => {
if (!(event.modifiers & Qt.ControlModifier))
return;
switch (event.key) {
case Qt.Key_N:
case Qt.Key_J:
dropdownMenu.selectNext();
event.accepted = true;
break;
case Qt.Key_P:
case Qt.Key_K:
dropdownMenu.selectPrevious();
event.accepted = true;
break;
}
}
Timer {
id: searchDebounce
interval: 50
onTriggered: {
if (!dropdownMenu.fzfFinder)
dropdownMenu.initFinder();
dropdownMenu.searchQuery = searchField.text;
dropdownMenu.selectedIndex = -1;
}
}
}
}
Item {
width: 1
height: Theme.spacingXS
visible: root.enableFuzzySearch
}
Item {
width: parent.width
height: 32
visible: root.options.length === 0 && root.emptyText !== ""
StyledText {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
text: root.emptyText
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
horizontalAlignment: Text.AlignLeft
}
}
DankListView {
id: listView
width: parent.width
height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) - (root.options.length === 0 && root.emptyText !== "" ? 32 : 0)
clip: true
visible: root.options.length > 0
model: ScriptModel {
values: dropdownMenu.filteredOptions
}
spacing: Theme.spacingXXS
interactive: true
flickDeceleration: 1500
maximumFlickVelocity: 2000
boundsBehavior: Flickable.DragAndOvershootBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
delegate: Rectangle {
id: delegateRoot
required property var modelData
required property int index
property bool isSelected: dropdownMenu.selectedIndex === index
property bool isCurrentValue: root.currentValue === modelData
property string iconName: root.optionIconMap[modelData] ?? ""
property var swatchColor: root.optionColorMap[modelData]
width: ListView.view.width
height: 32
radius: Theme.cornerRadius
color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : Theme.withAlpha(Theme.primaryHoverLight, 0)
Row {
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingS
DankColorSwatch {
id: optionSwatch
width: 16
height: 16
anchors.verticalCenter: parent.verticalCenter
visible: delegateRoot.swatchColor !== undefined
swatchColor: visible ? delegateRoot.swatchColor : Theme.withAlpha(delegateRoot.swatchColor, 0)
ringColor: delegateRoot.isCurrentValue ? Theme.primary : Theme.outline
}
DankIcon {
name: delegateRoot.iconName
size: 18
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
visible: name !== ""
}
StyledText {
anchors.verticalCenter: parent.verticalCenter
text: delegateRoot.modelData
font.pixelSize: Theme.fontSizeMedium
color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
font.weight: delegateRoot.isCurrentValue ? Font.Medium : Font.Normal
width: root.popupWidth > 0 ? undefined : (delegateRoot.width - parent.x - Theme.spacingS * 2 - (optionSwatch.visible ? optionSwatch.width + parent.spacing : 0))
elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
wrapMode: Text.NoWrap
horizontalAlignment: Text.AlignLeft
}
}
MouseArea {
id: optionArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.currentValue = delegateRoot.modelData;
root.valueChanged(delegateRoot.modelData);
root.closeDropdownMenu();
}
}
}
}
}
}
}
}
DankCommon.DankDropdown {}
+2 -107
View File
@@ -1,108 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Flow {
id: root
property var model: []
property int currentIndex: 0
property int chipHeight: 32
property int chipPadding: Theme.spacingM
property bool showCheck: true
property bool showCounts: true
signal selectionChanged(int index)
spacing: Theme.spacingS
width: parent ? parent.width : 400
Repeater {
model: root.model
Rectangle {
id: chip
required property var modelData
required property int index
property bool selected: index === root.currentIndex
property bool hovered: mouseArea.containsMouse
property bool pressed: mouseArea.pressed
property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
property int count: typeof modelData === "object" ? (modelData.count || 0) : 0
property bool showCount: root.showCounts && count > 0
width: contentRow.implicitWidth + root.chipPadding * 2
height: root.chipHeight
radius: height / 2
color: selected ? Theme.primary : Theme.surfaceVariant
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Rectangle {
anchors.fill: parent
radius: parent.radius
color: {
if (pressed)
return chip.selected ? Theme.primaryPressed : Theme.surfaceTextHover;
if (hovered)
return chip.selected ? Theme.primaryHover : Theme.surfaceTextHover;
return "transparent";
}
Behavior on color {
ColorAnimation {
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: chipRipple
cornerRadius: chip.radius
rippleColor: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
}
Row {
id: contentRow
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: "check"
size: 16
anchors.verticalCenter: parent.verticalCenter
color: Theme.primaryText
visible: root.showCheck && chip.selected
}
StyledText {
text: chip.label + (chip.showCount ? " (" + chip.count + ")" : "")
font.pixelSize: Theme.fontSizeSmall
font.weight: chip.selected ? Font.Medium : Font.Normal
color: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y)
onClicked: {
root.currentIndex = chip.index;
root.selectionChanged(chip.index);
}
}
}
}
}
DankCommon.DankFilterChips {}
+2 -178
View File
@@ -1,179 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
Flickable {
id: flickable
property alias verticalScrollBar: vbar
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
property bool _scrollBarActive: false
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
WheelHandler {
id: wheelHandler
property real touchpadSpeed: Scroll.touchpadSpeed
property real momentumRetention: Scroll.momentumRetention
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
flickable.isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
let newY = flickable.contentY + scrollAmount;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
flickable.isMomentumActive = false;
velocitySamples = [];
momentum = 0;
flickable.momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
flickable.isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
flickable.momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = flickable.contentY - delta;
newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
if (flickable.flicking) {
flickable.cancelFlick();
}
flickable.contentY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
flickable.momentumVelocity = 0;
}
}
}
}
onMovementStarted: {
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: vbar.hideTimer.restart()
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = flickable.contentY - flickable.momentumVelocity * dt;
const maxY = Math.max(0, flickable.contentHeight - flickable.height);
if (newY < 0 || newY > maxY) {
flickable.contentY = newY < 0 ? 0 : maxY;
running = false;
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
return;
}
flickable.contentY = newY;
flickable.momentumVelocity *= Math.pow(flickable.friction, dt / 0.016);
if (Math.abs(flickable.momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
flickable.isMomentumActive = false;
flickable.momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
}
}
DankCommon.DankFlickable {}
+2 -174
View File
@@ -1,175 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
GridView {
id: gridView
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
onMovementStarted: {
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: vbar.hideTimer.restart()
WheelHandler {
id: wheelHandler
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real touchpadSpeed: Scroll.touchpadSpeed
property real momentumRetention: Scroll.momentumRetention
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
let newY = contentY + scrollAmount;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 120 * cellHeight * 1.2;
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = contentY - delta;
newY = Math.max(0, Math.min(contentHeight - height, newY));
if (flicking) {
cancelFlick();
}
contentY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
}
}
}
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = contentY - momentumVelocity * dt;
const maxY = Math.max(0, contentHeight - height);
if (newY < 0 || newY > maxY) {
contentY = newY < 0 ? 0 : maxY;
running = false;
isMomentumActive = false;
momentumVelocity = 0;
return;
}
contentY = newY;
momentumVelocity *= Math.pow(friction, dt / 0.016);
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
}
}
DankCommon.DankGridView {}
+2 -71
View File
@@ -1,72 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property alias name: icon.text
property alias size: icon.font.pixelSize
property alias color: icon.color
property bool filled: false
property real fill: filled ? 1.0 : 0.0
property int grade: Theme.isLightMode ? 0 : -25
property int weight: filled ? 500 : 400
property bool smoothTransform: false
implicitWidth: Math.round(size)
implicitHeight: Math.round(size)
signal rotationCompleted
FontLoader {
id: materialSymbolsFont
source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
}
StyledText {
id: icon
anchors.fill: parent
font.family: materialSymbolsFont.name
font.pixelSize: Math.round(Theme.fontSizeMedium)
font.weight: root.weight
font.hintingPreference: Font.PreferNoHinting
color: Theme.surfaceText
verticalAlignment: Text.AlignVCenter
horizontalAlignment: Text.AlignHCenter
renderType: root.smoothTransform ? Text.QtRendering : Text.NativeRendering
font.variableAxes: {
"FILL": root.fill.toFixed(1),
"GRAD": root.grade,
"opsz": 24,
"wght": root.weight
}
Behavior on font.weight {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Behavior on fill {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Timer {
id: rotationTimer
interval: 16
repeat: false
onTriggered: root.rotationCompleted()
}
onRotationChanged: {
rotationTimer.restart();
}
}
DankCommon.DankIcon {}
+2 -258
View File
@@ -1,259 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property string currentIcon: ""
property string iconType: "icon"
signal iconSelected(string iconName, string iconType)
width: 240
height: 32
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: iconPopup.visible ? Theme.primary : Theme.outline
border.width: 1
property var iconCategories: [
{
"name": I18n.tr("Numbers"),
"icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
},
{
"name": I18n.tr("Workspace"),
"icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
},
{
"name": I18n.tr("Development"),
"icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
},
{
"name": I18n.tr("Communication"),
"icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
},
{
"name": I18n.tr("Media"),
"icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
},
{
"name": I18n.tr("System"),
"icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
},
{
"name": I18n.tr("Navigation"),
"icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
},
{
"name": I18n.tr("Actions"),
"icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
},
{
"name": I18n.tr("Status"),
"icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
},
{
"name": I18n.tr("Fun"),
"icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
}
]
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
if (iconPopup.visible) {
iconPopup.close();
return;
}
const pos = root.mapToItem(Overlay.overlay, 0, 0);
const popupHeight = 500;
const overlayHeight = Overlay.overlay?.height ?? 800;
iconPopup.x = pos.x;
if (pos.y + root.height + popupHeight + 4 > overlayHeight) {
iconPopup.y = pos.y - popupHeight - 4;
} else {
iconPopup.y = pos.y + root.height + 4;
}
iconPopup.open();
}
}
Row {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingS
spacing: Theme.spacingS
DankIcon {
name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add")
size: 16
color: root.currentIcon ? Theme.surfaceText : Theme.outline
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: root.currentIcon ? root.currentIcon : I18n.tr("Choose icon")
font.pixelSize: Theme.fontSizeSmall
color: root.currentIcon ? Theme.surfaceText : Theme.outline
anchors.verticalCenter: parent.verticalCenter
width: 160
elide: Text.ElideRight
}
}
DankIcon {
name: iconPopup.visible ? "expand_less" : "expand_more"
size: 16
color: Theme.outline
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS
anchors.verticalCenter: parent.verticalCenter
}
Popup {
id: iconPopup
parent: root.Overlay.overlay
width: 320
height: Math.min(500, dropdownContent.implicitHeight + 32)
padding: 0
modal: true
dim: false
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
background: Rectangle {
color: "transparent"
}
contentItem: Rectangle {
id: contentSurface
color: Theme.surface
radius: Theme.cornerRadius
ElevationShadow {
id: shadowLayer
anchors.fill: parent
z: -1
level: Theme.elevationLevel2
fallbackOffset: 4
targetRadius: contentSurface.radius
targetColor: contentSurface.color
shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
shadowEnabled: Theme.elevationEnabled
}
Rectangle {
width: 24
height: 24
radius: 12
color: closeMouseArea.containsMouse ? Theme.errorHover : Theme.withAlpha(Theme.errorHover, 0)
anchors.top: parent.top
anchors.right: parent.right
anchors.topMargin: Theme.spacingS
anchors.rightMargin: Theme.spacingS
z: 1
DankIcon {
name: "close"
size: 16
color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
anchors.centerIn: parent
}
MouseArea {
id: closeMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: iconPopup.close()
}
}
DankFlickable {
anchors.fill: parent
anchors.margins: Theme.spacingS
contentHeight: dropdownContent.height
clip: true
pressDelay: 0
Column {
id: dropdownContent
width: parent.width
spacing: Theme.spacingM
Repeater {
model: root.iconCategories
Column {
required property var modelData
width: parent.width
spacing: Theme.spacingS
StyledText {
text: modelData.name
font.pixelSize: Theme.fontSizeSmall
font.weight: Font.Medium
color: Theme.surfaceText
}
Flow {
width: parent.width
spacing: Theme.spacingXS
Repeater {
model: modelData.icons
Rectangle {
required property string modelData
width: 36
height: 36
radius: Theme.cornerRadius
color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
border.width: 2
DankIcon {
name: parent.modelData
size: 20
color: root.currentIcon === parent.modelData ? Theme.primary : Theme.surfaceText
anchors.centerIn: parent
}
MouseArea {
id: iconMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root.iconSelected(parent.modelData, "icon");
iconPopup.close();
}
}
Behavior on color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
}
}
}
}
}
}
}
function setIcon(iconName, type) {
root.iconType = type;
root.iconType = "icon";
root.currentIcon = iconName;
}
}
DankCommon.DankIconPicker {}
+2 -276
View File
@@ -1,277 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.Widgets
import "ScrollConstants.js" as Scroll
import qs.DankCommon.Widgets as DankCommon
ListView {
id: listView
property real scrollBarTopMargin: 0
property real mouseWheelSpeed: Scroll.mouseWheelSpeed
property real savedY: 0
property bool justChanged: false
property bool isUserScrolling: false
property real momentumVelocity: 0
property bool isMomentumActive: false
property real friction: Scroll.friction
flickDeceleration: Scroll.flickDeceleration
maximumFlickVelocity: Scroll.maximumFlickVelocity
boundsBehavior: Flickable.StopAtBounds
boundsMovement: Flickable.FollowBoundsBehavior
pressDelay: 0
flickableDirection: Flickable.VerticalFlick
add: ListViewTransitions.add
remove: ListViewTransitions.remove
displaced: ListViewTransitions.displaced
move: ListViewTransitions.move
// QQmlDelegateModel can release a delegate back to its cache without hiding it,
// leaving a stale row painted over live ones. Hide anything the view no longer claims.
Connections {
target: listView.model?.objectName !== undefined ? listView.model : null
ignoreUnknownSignals: true
function onRowsInserted() {
orphanSweep.arm();
}
function onRowsRemoved() {
orphanSweep.arm();
}
function onRowsMoved() {
orphanSweep.arm();
}
function onModelReset() {
orphanSweep.arm();
}
}
FrameAnimation {
id: orphanSweep
property int frames: 0
function arm() {
frames = 0;
running = true;
}
onTriggered: {
const kids = listView.contentItem.children;
const rows = [];
for (let i = 0; i < kids.length; i++) {
const c = kids[i];
if (!c || c.index === undefined)
continue;
const claimed = listView.itemAtIndex(c.index) === c;
if (claimed && !c.visible) {
c.visible = true;
continue;
}
if (c.visible)
rows.push({
item: c,
claimed: claimed
});
}
for (let a = 0; a < rows.length; a++) {
if (rows[a].claimed)
continue;
for (let b = 0; b < rows.length; b++) {
if (a === b || !rows[b].claimed)
continue;
if (Math.abs(rows[a].item.y - rows[b].item.y) < rows[b].item.height / 2) {
rows[a].item.visible = false;
break;
}
}
}
if (++frames >= 3)
running = false;
}
}
onMovementStarted: {
isUserScrolling = true;
vbar._scrollBarActive = true;
vbar.hideTimer.stop();
}
onMovementEnded: {
isUserScrolling = false;
vbar.hideTimer.restart();
}
onContentYChanged: {
if (!justChanged && isUserScrolling) {
savedY = contentY;
}
justChanged = false;
}
onModelChanged: {
justChanged = true;
contentY = savedY;
}
WheelHandler {
id: wheelHandler
property real touchpadSpeed: Scroll.touchpadSpeed
property real lastWheelTime: 0
property real momentum: 0
property var velocitySamples: []
property bool sessionUsedMouseWheel: false
function startMomentum() {
isMomentumActive = true;
momentumAnim.running = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
onWheel: event => {
isUserScrolling = true;
vbar._scrollBarActive = true;
vbar.hideTimer.restart();
const currentTime = Date.now();
const timeDelta = currentTime - lastWheelTime;
lastWheelTime = currentTime;
const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
const deltaY = event.angleDelta.y;
const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
const isTouchpad = hasPixel;
if (isTraditionalMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
const lines = Math.round(Math.abs(deltaY) / 120);
const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
let newY = listView.contentY + scrollAmount;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isHighDpiMouse) {
sessionUsedMouseWheel = true;
momentumAnim.running = false;
isMomentumActive = false;
velocitySamples = [];
momentum = 0;
momentumVelocity = 0;
let delta = deltaY / 8 * touchpadSpeed;
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
} else if (isTouchpad) {
sessionUsedMouseWheel = false;
momentumAnim.running = false;
isMomentumActive = false;
let delta = event.pixelDelta.y * touchpadSpeed;
velocitySamples.push({
"delta": delta,
"time": currentTime
});
velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
if (velocitySamples.length > 1) {
const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
const timeSpan = currentTime - velocitySamples[0].time;
if (timeSpan > 0) {
momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
}
}
if (timeDelta < Scroll.momentumTimeThreshold) {
momentum = momentum * Scroll.momentumRetention + delta * Scroll.momentumDeltaFactor;
delta += momentum;
} else {
momentum = 0;
}
let newY = listView.contentY - delta;
const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
newY = Math.max(listView.originY, Math.min(maxY, newY));
if (listView.flicking) {
listView.cancelFlick();
}
listView.contentY = newY;
savedY = newY;
}
event.accepted = true;
}
onActiveChanged: {
if (!active) {
isUserScrolling = false;
if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
startMomentum();
} else {
velocitySamples = [];
momentumVelocity = 0;
}
}
}
}
FrameAnimation {
id: momentumAnim
running: false
onTriggered: {
const dt = frameTime;
const newY = contentY - momentumVelocity * dt;
const maxY = Math.max(0, contentHeight - height + originY);
const minY = originY;
if (newY < minY || newY > maxY) {
contentY = newY < minY ? minY : maxY;
savedY = contentY;
running = false;
isMomentumActive = false;
momentumVelocity = 0;
return;
}
contentY = newY;
savedY = newY;
momentumVelocity *= Math.pow(friction, dt / 0.016);
if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
running = false;
isMomentumActive = false;
momentumVelocity = 0;
}
}
}
ScrollBar.vertical: DankScrollbar {
id: vbar
topPadding: listView.scrollBarTopMargin
}
}
DankCommon.DankListView {}
+2 -270
View File
@@ -1,271 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
activeFocusOnTab: true
KeyNavigation.tab: keyNavigationTab
KeyNavigation.backtab: keyNavigationBacktab
onActiveFocusChanged: {
if (activeFocus) {
locationInput.forceActiveFocus();
}
}
property string currentLocation: ""
property string placeholderText: I18n.tr("Search for a location...")
property bool _internalChange: false
property bool isLoading: false
property string currentSearchText: ""
property Item keyNavigationTab: null
property Item keyNavigationBacktab: null
signal locationSelected(string displayName, string coordinates)
function resetSearchState() {
locationSearchTimer.stop();
dropdownHideTimer.stop();
isLoading = false;
searchResultsModel.clear();
}
// CJK city names are commonly two code points (, , ).
function canSearch(t) {
return t.length > 2 || (t.length >= 2 && /[぀-ヿ㐀-䶿一-鿿豈-﫿가-힯]/.test(t));
}
width: parent.width
height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
ListModel {
id: searchResultsModel
}
Timer {
id: locationSearchTimer
interval: 500
running: false
repeat: false
onTriggered: {
if (root.canSearch(locationInput.text)) {
searchResultsModel.clear();
root.isLoading = true;
const searchLocation = locationInput.text;
root.currentSearchText = searchLocation;
const encodedLocation = encodeURIComponent(searchLocation);
const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
Proc.runCommand("locationSearch", [Proc.dmsBin, "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
root.isLoading = false;
if (exitCode !== 0) {
searchResultsModel.clear();
return;
}
if (root.currentSearchText !== locationInput.text)
return;
const raw = output.trim();
searchResultsModel.clear();
if (!raw || raw[0] !== "[") {
return;
}
try {
const data = JSON.parse(raw);
if (data.length === 0) {
return;
}
for (var i = 0; i < Math.min(data.length, 5); i++) {
const location = data[i];
if (location.display_name && location.lat && location.lon) {
const parts = location.display_name.split(', ');
let cleanName = parts[0];
if (parts.length > 1) {
const state = parts[parts.length - 2];
if (state && state !== cleanName)
cleanName += `, ${state}`;
}
const query = `${location.lat},${location.lon}`;
searchResultsModel.append({
"name": cleanName,
"query": query
});
}
}
} catch (e) {}
});
}
}
}
Timer {
id: dropdownHideTimer
interval: 200
running: false
repeat: false
onTriggered: {
if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
root.resetSearchState();
}
}
Item {
id: searchInputField
width: parent.width
height: 48
DankTextField {
id: locationInput
width: parent.width
height: parent.height
leftIconName: "search"
placeholderText: root.placeholderText
text: ""
backgroundColor: Theme.surfaceVariant
normalBorderColor: Theme.primarySelected
focusedBorderColor: Theme.primary
keyNavigationTab: root.keyNavigationTab
keyNavigationBacktab: root.keyNavigationBacktab
onTextEdited: {
if (root._internalChange)
return;
if (getActiveFocus()) {
if (root.canSearch(text)) {
root.isLoading = true;
locationSearchTimer.restart();
} else {
root.resetSearchState();
}
}
}
onFocusStateChanged: hasFocus => {
if (hasFocus) {
dropdownHideTimer.stop();
} else {
dropdownHideTimer.start();
}
}
}
DankIcon {
name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error")
size: Theme.iconSize - 4
color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error)
anchors.right: parent.right
anchors.rightMargin: Theme.spacingM
anchors.verticalCenter: parent.verticalCenter
opacity: (locationInput.getActiveFocus() && root.canSearch(locationInput.text)) ? 1 : 0
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
StyledRect {
id: searchDropdown
property bool hovered: false
width: parent.width
height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
y: searchInputField.height
radius: Theme.cornerRadius
color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
border.color: Theme.primarySelected
border.width: 1
visible: locationInput.getActiveFocus() && root.canSearch(locationInput.text) && (searchResultsModel.count > 0 || root.isLoading)
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: {
parent.hovered = true;
dropdownHideTimer.stop();
}
onExited: {
parent.hovered = false;
if (!locationInput.getActiveFocus())
dropdownHideTimer.start();
}
acceptedButtons: Qt.NoButton
}
Item {
anchors.fill: parent
anchors.margins: Theme.spacingS
DankListView {
id: searchResultsList
anchors.fill: parent
clip: true
model: searchResultsModel
spacing: Theme.spacingXXS
delegate: StyledRect {
width: searchResultsList.width
height: 36
radius: Theme.cornerRadius
color: resultMouseArea.containsMouse ? Theme.surfaceLight : Theme.withAlpha(Theme.surfaceLight, 0)
Row {
anchors.fill: parent
anchors.margins: Theme.spacingM
spacing: Theme.spacingS
DankIcon {
name: "place"
size: Theme.iconSize - 6
color: Theme.surfaceVariantText
anchors.verticalCenter: parent.verticalCenter
}
StyledText {
text: model.name || "Unknown"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceText
anchors.verticalCenter: parent.verticalCenter
elide: Text.ElideRight
width: parent.width - 30
}
}
MouseArea {
id: resultMouseArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: {
root._internalChange = true;
const selectedName = model.name;
const selectedQuery = model.query;
locationInput.text = selectedName;
root.locationSelected(selectedName, selectedQuery);
root.resetSearchState();
locationInput.setFocus(false);
root._internalChange = false;
}
}
}
}
StyledText {
anchors.centerIn: parent
text: root.isLoading ? "Searching..." : "No locations found"
font.pixelSize: Theme.fontSizeMedium
color: Theme.surfaceVariantText
visible: searchResultsList.count === 0 && root.canSearch(locationInput.text)
}
}
}
}
DankCommon.DankLocationSearch {}
+2 -163
View File
@@ -1,164 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string name: ""
property int size: Theme.fontSizeMedium
property alias color: icon.color
width: size
height: size
visible: text.length > 0
// This is for file browser, particularly - might want another map later for app IDs
readonly property var iconMap: ({
// --- Distribution logos ---
"debian": "\u{f08da}",
"arch": "\u{f08c7}",
"archcraft": "\u{f345}",
"guix": "\u{f325}",
"fedora": "\u{f08db}",
"nixos": "\u{f1105}",
"ubuntu": "\u{f0548}",
"gentoo": "\u{f08e8}",
"endeavouros": "\u{f322}",
"manjaro": "\u{f160a}",
"opensuse": "\u{f314}",
"artix": "\u{f31f}",
"void": "\u{f32e}",
// --- special types ---
"folder": "\u{F024B}",
"file": "\u{F0214}",
// --- special filenames (no extension) ---
"docker": "\u{F0868}",
"makefile": "\u{F09EE}",
"license": "\u{F09EE}",
"readme": "\u{F0354}",
// --- programming languages ---
"rs": "\u{F1617}",
"dart": "\u{e798}",
"go": "\u{F07D3}",
"py": "\u{F0320}",
"js": "\u{F031E}",
"jsx": "\u{F031E}",
"ts": "\u{F06E6}",
"tsx": "\u{F06E6}",
"java": "\u{F0B37}",
"c": "\u{F0671}",
"cpp": "\u{F0672}",
"cxx": "\u{F0672}",
"h": "\u{F0672}",
"hpp": "\u{F0672}",
"cs": "\u{F031B}",
"html": "\u{e60e}",
"htm": "\u{e60e}",
"css": "\u{E6b8}",
"scss": "\u{F031C}",
"less": "\u{F031C}",
"md": "\u{F0354}",
"markdown": "\u{F0354}",
"json": "\u{eb0f}",
"jsonc": "\u{eb0f}",
"yaml": "\u{e8eb}",
"yml": "\u{e8eb}",
"xml": "\u{F09EE}",
"sql": "\u{f1c0}",
// --- scripts / shells ---
"sh": "\u{f0bc1}",
"bash": "\u{f0bc1}",
"zsh": "\u{f0bc1}",
"fish": "\u{f0bc1}",
"ps1": "\u{f0bc1}",
"bat": "\u{f0bc1}",
// --- data / config ---
"toml": "\u{e6b2}",
"ini": "\u{F09EE}",
"conf": "\u{F09EE}",
"cfg": "\u{F09EE}",
"csv": "\u{eefc}",
"tsv": "\u{F021C}",
// --- docs / office ---
"pdf": "\u{F0226}",
"doc": "\u{F09EE}",
"docx": "\u{F09EE}",
"rtf": "\u{F09EE}",
"ppt": "\u{F09EE}",
"pptx": "\u{F09EE}",
"log": "\u{F09EE}",
"xls": "\u{F021C}",
"xlsx": "\u{F021C}",
// --- images ---
"ico": "\u{F021F}",
// --- audio / video ---
"mp3": "\u{e638}",
"wav": "\u{e638}",
"flac": "\u{e638}",
"ogg": "\u{e638}",
"mp4": "\u{f0567}",
"mkv": "\u{f0567}",
"webm": "\u{f0567}",
"mov": "\u{f0567}",
// --- archives / packages ---
"zip": "\u{e6aa}",
"tar": "\u{f003c}",
"gz": "\u{f003c}",
"bz2": "\u{f003c}",
"7z": "\u{f003c}",
// --- containers / infra / cloud ---
"dockerfile": "\u{F0868}",
"yml.k8s": "\u{F09EE}",
"yaml.k8s": "\u{F09EE}",
"tf": "\u{F09EE}",
"tfvars": "\u{F09EE}",
// --- moon phases
"moon_new": "\u{F0F64}",
"moon_waxing_crescent": "\u{F0F67}",
"moon_first_quarter": "\u{F0F61}",
"moon_waxing_gibbous": "\u{F0F68}",
"moon_full": "\u{F0F62}",
"moon_waning_gibbous": "\u{F0F66}",
"moon_last_quarter": "\u{F0F63}",
"moon_waning_crescent": "\u{F0F65}"
})
readonly property string text: iconMap[name] || iconMap["file"] || ""
function getIconForFile(fileName) {
const lowerName = fileName.toLowerCase();
if (lowerName.startsWith("dockerfile")) {
return "docker";
}
const ext = fileName.split('.').pop();
return ext || "";
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
StyledText {
id: icon
anchors.centerIn: parent
font.family: firaCodeFont.name
font.pixelSize: root.size
color: Theme.surfaceText
text: root.text
antialiasing: true
}
}
DankCommon.DankNFIcon {}
+2 -72
View File
@@ -1,73 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Column {
id: root
property string text: ""
property var incrementTooltipText: ""
property var decrementTooltipText: ""
property var onIncrement: undefined
property var onDecrement: undefined
property string incrementIconName: "keyboard_arrow_up"
property string decrementIconName: "keyboard_arrow_down"
property bool incrementEnabled: true
property bool decrementEnabled: true
property color textColor: Theme.surfaceText
property color iconColor: Theme.withAlpha(Theme.surfaceText, 0.5)
property color backgroundColor: Theme.primary
property int textSize: Theme.fontSizeSmall
property var iconSize: 12
property int buttonSize: 20
property int horizontalPadding: Theme.spacingL
readonly property bool effectiveIncrementEnabled: root.onIncrement ? root.incrementEnabled : false
readonly property bool effectiveDecrementEnabled: root.onDecrement ? root.decrementEnabled : false
width: Math.max(buttonSize * 2, root.implicitWidth + horizontalPadding * 2)
spacing: Theme.spacingXS
DankActionButton {
anchors.horizontalCenter: parent.horizontalCenter
enabled: root.effectiveIncrementEnabled
iconColor: root.effectiveIncrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
iconSize: root.iconSize
buttonSize: root.buttonSize
iconName: root.incrementIconName
onClicked: if (typeof root.onIncrement === 'function')
root.onIncrement()
tooltipText: root.incrementTooltipText
}
Row {
anchors.horizontalCenter: parent.horizontalCenter
Item {
width: 5
height: 1
}
StyledText {
isMonospace: true
text: root.text
font.pixelSize: root.textSize
color: root.textColor
}
Item {
width: 5
height: 1
}
}
DankActionButton {
anchors.horizontalCenter: parent.horizontalCenter
enabled: root.effectiveDecrementEnabled
iconColor: root.effectiveDecrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
iconSize: root.iconSize
buttonSize: root.buttonSize
iconName: root.decrementIconName
onClicked: if (typeof root.onDecrement === 'function')
root.onDecrement()
tooltipText: root.decrementTooltipText
}
}
DankCommon.DankNumberStepper {}
+2 -104
View File
@@ -1,105 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property color rippleColor: Theme.primary
property real cornerRadius: 0
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
property int animationDuration: Theme.expressiveDurations.expressiveDefaultSpatial
property real _rippleX: 0
property real _rippleY: 0
property real _rippleMaxRadius: 0
readonly property alias animating: rippleAnim.running
anchors.fill: parent
function trigger(x, y) {
if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None)
return;
_rippleX = x;
_rippleY = y;
const dist = (ox, oy) => ox * ox + oy * oy;
_rippleMaxRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y)));
rippleAnim.restart();
}
SequentialAnimation {
id: rippleAnim
PropertyAction {
target: rippleFx
property: "rippleCenterX"
value: root._rippleX
}
PropertyAction {
target: rippleFx
property: "rippleCenterY"
value: root._rippleY
}
PropertyAction {
target: rippleFx
property: "rippleRadius"
value: 0
}
PropertyAction {
target: rippleFx
property: "rippleOpacity"
value: 0.10
}
ParallelAnimation {
DankAnim {
target: rippleFx
property: "rippleRadius"
from: 0
to: root._rippleMaxRadius
duration: root.animationDuration
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
SequentialAnimation {
PauseAnimation {
duration: Math.round(root.animationDuration * 0.6)
}
DankAnim {
target: rippleFx
property: "rippleOpacity"
to: 0
duration: root.animationDuration
easing.bezierCurve: Theme.expressiveCurves.standard
}
}
}
}
ShaderEffect {
id: rippleFx
visible: rippleAnim.running
property real rippleCenterX: 0
property real rippleCenterY: 0
property real rippleRadius: 0
property real rippleOpacity: 0
x: Math.max(0, rippleCenterX - rippleRadius)
y: Math.max(0, rippleCenterY - rippleRadius)
width: Math.max(0, Math.min(root.width, rippleCenterX + rippleRadius) - x)
height: Math.max(0, Math.min(root.height, rippleCenterY + rippleRadius) - y)
property real widthPx: width
property real heightPx: height
property real cornerRadiusPx: root.cornerRadius
property real offsetX: x
property real offsetY: y
property real parentWidth: root.width
property real parentHeight: root.height
property vector4d rippleCol: Qt.vector4d(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, root.rippleColor.a)
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/ripple.frag.qsb")
}
}
DankCommon.DankRipple {}
+2 -80
View File
@@ -1,81 +1,3 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Widgets
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
required property string source
property int size: 24
property string cornerIcon: ""
property int cornerIconSize: Math.max(10, size * 0.4)
property color cornerIconColor: Theme.surfaceText
property color cornerIconBackground: Theme.surface
property color colorOverride: "transparent"
property real brightnessOverride: 0.0
property real contrastOverride: 0.0
property real saturationOverride: 0.0
readonly property bool hasCornerIcon: cornerIcon !== ""
readonly property bool hasColorOverride: colorOverride.a > 0
readonly property bool hasColorEffect: hasColorOverride || brightnessOverride !== 0.0 || contrastOverride !== 0.0 || saturationOverride !== 0.0
readonly property string resolvedSource: {
if (!source)
return "";
if (source.startsWith("file://"))
return source;
if (source.startsWith("/"))
return "file://" + source;
if (source.startsWith("qrc:"))
return source;
return source;
}
implicitWidth: size
implicitHeight: size
IconImage {
id: iconImage
anchors.fill: parent
source: root.resolvedSource
smooth: true
mipmap: true
asynchronous: true
implicitSize: root.size * 2
backer.sourceSize.width: root.size * 2
backer.sourceSize.height: root.size * 2
backer.cache: true
layer.enabled: root.hasColorEffect
layer.smooth: true
layer.mipmap: true
layer.effect: MultiEffect {
saturation: root.saturationOverride
colorization: root.hasColorOverride ? 1 : 0
colorizationColor: root.colorOverride
brightness: root.brightnessOverride
contrast: root.contrastOverride
}
}
Rectangle {
visible: root.hasCornerIcon
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.rightMargin: -2
anchors.bottomMargin: -2
width: root.cornerIconSize + 4
height: root.cornerIconSize + 4
radius: width / 2
color: root.cornerIconBackground
border.width: 1
border.color: Theme.surfaceLight
DankIcon {
anchors.centerIn: parent
name: root.cornerIcon
size: root.cornerIconSize
color: root.cornerIconColor
}
}
}
DankCommon.DankSVGIcon {}
+2 -45
View File
@@ -1,46 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.DankCommon.Widgets as DankCommon
ScrollBar {
id: scrollbar
property var targetFlickable: null
readonly property var _target: targetFlickable ?? parent
property bool _scrollBarActive: false
property alias hideTimer: hideScrollBarTimer
property bool _isParentMoving: _target && (_target.moving || _target.flicking || _target.isMomentumActive)
property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive
policy: (_target && _target.contentHeight > _target.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
minimumSize: 0.08
implicitWidth: 10
interactive: true
hoverEnabled: true
z: 1000
opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0
visible: policy !== ScrollBar.AlwaysOff
Behavior on opacity {
NumberAnimation {
duration: 160
easing.type: Easing.OutQuad
}
}
contentItem: Rectangle {
implicitWidth: 6
radius: width / 2
color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium
opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6
}
background: Item {}
Timer {
id: hideScrollBarTimer
interval: 1200
onTriggered: scrollbar._scrollBarActive = false
}
}
DankCommon.DankScrollbar {}
+2 -307
View File
@@ -1,308 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: slider
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property int value: 50
property int minimum: 0
property int maximum: 100
property int step: 1
property string leftIcon: ""
property string rightIcon: ""
property string unit: "%"
property bool showValue: true
property bool isDragging: false
property bool wheelEnabled: true
property bool centerMinimum: false
property real valueOverride: -1
property bool alwaysShowValue: false
readonly property bool containsMouse: sliderMouseArea.containsMouse
property color thumbOutlineColor: Theme.surfaceContainer
property color trackColor: enabled ? Theme.outline : Theme.outline
property bool usePopupTransparency: !checkParentDisablesTransparency()
property real trackOpacity: usePopupTransparency ? Theme.popupTransparency : 1.0
signal sliderValueChanged(int newValue)
signal sliderDragFinished(int finalValue)
height: 48
function updateValueFromPosition(x) {
let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width)));
if (centerMinimum)
ratio = Math.max(0, (ratio - 0.5) * 2);
let rawValue = minimum + ratio * (maximum - minimum);
let newValue = step > 1 ? Math.round(rawValue / step) * step : Math.round(rawValue);
newValue = Math.max(minimum, Math.min(maximum, newValue));
if (newValue !== value) {
value = newValue;
sliderValueChanged(newValue);
}
}
Row {
anchors.centerIn: parent
width: parent.width
spacing: Theme.spacingM
DankIcon {
name: slider.leftIcon
size: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter
visible: slider.leftIcon.length > 0
}
StyledRect {
id: sliderTrack
property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
height: 12
radius: Theme.cornerRadius
color: Theme.withAlpha(slider.trackColor, slider.trackOpacity)
anchors.verticalCenter: parent.verticalCenter
clip: false
StyledRect {
id: sliderFill
height: parent.height
radius: Theme.cornerRadius
topRightRadius: 0
bottomRightRadius: 0
width: {
const range = slider.maximum - slider.minimum;
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
const travel = sliderTrack.width - sliderHandle.width;
const handleLeft = travel * ratio;
const endPoint = handleLeft - 3;
return Math.max(0, Math.min(sliderTrack.width, endPoint));
}
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
}
StyledRect {
id: sliderHandle
property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging
width: 4
height: 20
radius: Theme.cornerRadius
x: {
const range = slider.maximum - slider.minimum;
const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
const travel = sliderTrack.width - width;
return Math.max(0, Math.min(travel, travel * ratio));
}
anchors.verticalCenter: parent.verticalCenter
color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
border.width: 0
border.color: slider.thumbOutlineColor
StyledRect {
anchors.fill: parent
radius: Theme.cornerRadius
color: Theme.onPrimary
opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0
visible: opacity > 0
}
StyledRect {
anchors.centerIn: parent
width: parent.width + 20
height: parent.height + 20
radius: width / 2
color: "transparent"
border.width: 2
border.color: Theme.primary
opacity: slider.enabled && slider.focus ? 0.3 : 0
visible: opacity > 0
}
Rectangle {
id: ripple
anchors.centerIn: parent
width: 0
height: 0
radius: width / 2
color: Theme.onPrimary
opacity: 0
function start() {
opacity = 0.16;
width = 0;
height = 0;
rippleAnimation.start();
}
SequentialAnimation {
id: rippleAnimation
NumberAnimation {
target: ripple
properties: "width,height"
to: 28
duration: 180
}
NumberAnimation {
target: ripple
property: "opacity"
to: 0
duration: 150
}
}
}
TapHandler {
acceptedButtons: Qt.LeftButton
onPressedChanged: {
if (pressed && slider.enabled) {
ripple.start();
}
}
}
scale: active ? 1.05 : 1.0
Behavior on scale {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
Item {
id: sliderContainer
anchors.fill: parent
MouseArea {
id: sliderMouseArea
property bool isDragging: false
anchors.fill: parent
anchors.topMargin: -10
anchors.bottomMargin: -10
hoverEnabled: true
cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
enabled: slider.enabled
preventStealing: true
acceptedButtons: Qt.LeftButton
onWheel: wheelEvent => {
if (!slider.wheelEnabled) {
wheelEvent.accepted = false;
return;
}
let wheelStep = slider.step > 1 ? slider.step : Math.max(1, (maximum - minimum) / 100);
let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + wheelStep) : Math.max(minimum, value - wheelStep);
if (slider.step > 1)
newValue = Math.round(newValue / slider.step) * slider.step;
newValue = Math.round(newValue);
if (newValue !== value) {
value = newValue;
sliderValueChanged(newValue);
}
wheelEvent.accepted = true;
}
onPressed: mouse => {
if (slider.enabled) {
slider.isDragging = true;
sliderMouseArea.isDragging = true;
updateValueFromPosition(mouse.x);
}
}
onReleased: {
if (slider.enabled) {
slider.isDragging = false;
sliderMouseArea.isDragging = false;
slider.sliderDragFinished(slider.value);
}
}
onPositionChanged: mouse => {
if (pressed && slider.isDragging && slider.enabled) {
updateValueFromPosition(mouse.x);
}
}
onClicked: mouse => {
if (slider.enabled && !slider.isDragging) {
updateValueFromPosition(mouse.x);
}
}
}
}
StyledRect {
id: valueTooltip
width: tooltipText.reservedWidth + Theme.spacingS * 2
height: tooltipText.contentHeight + Theme.spacingXS * 2
radius: Theme.cornerRadius
color: Theme.surfaceContainer
border.color: Theme.outline
border.width: 1
anchors.bottom: parent.top
anchors.bottomMargin: Theme.spacingM
x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width / 2 - width / 2))
visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
opacity: visible ? 1 : 0
NumericText {
id: tooltipText
text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
reserveText: {
let widest = "";
const samples = [slider.minimum, slider.maximum];
if (slider.valueOverride >= 0)
samples.push(slider.valueOverride);
for (let i = 0; i < samples.length; i++) {
const candidate = Math.round(samples[i]) + slider.unit;
if (candidate.length > widest.length)
widest = candidate;
}
return widest;
}
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
font.weight: Font.Medium
anchors.centerIn: parent
font.hintingPreference: Font.PreferFullHinting
}
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
}
DankIcon {
name: slider.rightIcon
size: Theme.iconSize
color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
anchors.verticalCenter: parent.verticalCenter
visible: slider.rightIcon.length > 0
}
}
}
DankCommon.DankSlider {}
+2 -107
View File
@@ -1,108 +1,3 @@
import QtQuick
import QtQuick.Shapes
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property real size: 48
property real strokeWidth: Math.max(2, size / 12)
property color color: Theme.primary
property bool running: visible
implicitWidth: size
implicitHeight: size
onRunningChanged: {
if (running)
return;
arc.rotation = 0;
arc.startAngle = 0;
arc.sweepAngle = 16;
}
Item {
id: rotator
anchors.fill: parent
RotationAnimator on rotation {
from: 0
to: 360
duration: 1568
loops: Animation.Infinite
running: root.running
}
Shape {
id: arc
property real startAngle: 0
property real sweepAngle: 16
anchors.fill: parent
antialiasing: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
strokeColor: root.color
strokeWidth: root.strokeWidth
fillColor: "transparent"
capStyle: ShapePath.RoundCap
PathAngleArc {
centerX: arc.width / 2
centerY: arc.height / 2
radiusX: Math.max(1, (Math.min(arc.width, arc.height) - root.strokeWidth) / 2)
radiusY: radiusX
startAngle: arc.startAngle
sweepAngle: arc.sweepAngle
}
}
}
SequentialAnimation {
running: root.running
loops: Animation.Infinite
NumberAnimation {
target: arc
property: "sweepAngle"
from: 16
to: 270
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
ParallelAnimation {
NumberAnimation {
target: arc
property: "startAngle"
from: 0
to: 254
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
NumberAnimation {
target: arc
property: "sweepAngle"
from: 270
to: 16
duration: 666
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.standard
}
}
ScriptAction {
script: {
arc.rotation = (arc.rotation + 254) % 360;
arc.startAngle = 0;
}
}
}
}
}
DankCommon.DankSpinner {}
+2 -269
View File
@@ -1,270 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
FocusScope {
id: tabBar
property alias model: tabRepeater.model
property int currentIndex: 0
property int spacing: Theme.spacingL
property int tabHeight: 56
property bool showIcons: true
property bool equalWidthTabs: true
property bool enableArrowNavigation: true
property Item nextFocusTarget: null
property Item previousFocusTarget: null
signal tabClicked(int index)
signal actionTriggered(int index)
focus: false
activeFocusOnTab: true
height: tabHeight
KeyNavigation.tab: nextFocusTarget
KeyNavigation.down: nextFocusTarget
KeyNavigation.backtab: previousFocusTarget
KeyNavigation.up: previousFocusTarget
Keys.onPressed: event => {
if (!tabBar.activeFocus || tabRepeater.count === 0)
return;
function findSelectableIndex(startIndex, step) {
let idx = startIndex;
for (let i = 0; i < tabRepeater.count; i++) {
idx = (idx + step + tabRepeater.count) % tabRepeater.count;
const item = tabRepeater.itemAt(idx);
if (item && !item.isAction)
return idx;
}
return -1;
}
const goToIndex = nextIndex => {
if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) {
tabBar.currentIndex = nextIndex;
tabBar.tabClicked(nextIndex);
}
};
const resolveTarget = item => {
if (!item)
return null;
if (item.focusTarget)
return resolveTarget(item.focusTarget);
return item;
};
const focusItem = item => {
const target = resolveTarget(item);
if (!target)
return false;
if (target.requestFocus) {
Qt.callLater(() => target.requestFocus());
return true;
}
if (target.forceActiveFocus) {
Qt.callLater(() => target.forceActiveFocus());
return true;
}
return false;
};
if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) {
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1;
const nextIndex = findSelectableIndex(baseIndex, 1);
if (nextIndex >= 0) {
goToIndex(nextIndex);
event.accepted = true;
}
} else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) {
const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0;
const nextIndex = findSelectableIndex(baseIndex, -1);
if (nextIndex >= 0) {
goToIndex(nextIndex);
event.accepted = true;
}
} else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) {
if (focusItem(tabBar.previousFocusTarget)) {
event.accepted = true;
}
} else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) {
if (focusItem(tabBar.nextFocusTarget)) {
event.accepted = true;
}
} else if (event.key === Qt.Key_Up) {
if (focusItem(tabBar.previousFocusTarget)) {
event.accepted = true;
}
}
}
Row {
id: tabRow
anchors.fill: parent
spacing: tabBar.spacing
Repeater {
id: tabRepeater
Item {
id: tabItem
property bool isAction: modelData && modelData.isAction === true
property bool isActive: !isAction && tabBar.currentIndex === index
property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
property bool hasText: modelData && modelData.text && modelData.text.length > 0
width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
height: tabBar.tabHeight
Column {
id: contentCol
anchors.centerIn: parent
spacing: Theme.spacingXS
DankIcon {
name: modelData.icon || ""
anchors.horizontalCenter: parent.horizontalCenter
size: Theme.iconSize
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
visible: hasIcon
}
StyledText {
text: modelData.text || ""
anchors.horizontalCenter: parent.horizontalCenter
font.pixelSize: Theme.fontSizeMedium
color: tabItem.isActive ? Theme.primary : Theme.surfaceText
font.weight: Font.Medium
visible: hasText
}
}
Rectangle {
id: stateLayer
anchors.fill: parent
color: Theme.surfaceTint
opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
visible: opacity > 0
radius: Theme.cornerRadius
Behavior on opacity {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankRipple {
id: tabRipple
cornerRadius: Theme.cornerRadius
}
MouseArea {
id: tabArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onPressed: mouse => tabRipple.trigger(mouse.x, mouse.y)
onClicked: {
if (tabItem.isAction) {
tabBar.actionTriggered(index);
} else {
tabBar.tabClicked(index);
}
}
}
}
}
}
Rectangle {
id: indicator
y: parent.height + 7
height: 3
width: 60
topLeftRadius: Theme.cornerRadius
topRightRadius: Theme.cornerRadius
bottomLeftRadius: 0
bottomRightRadius: 0
color: Theme.primary
visible: false
property bool animationEnabled: false
property bool initialSetupComplete: false
Behavior on x {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
Behavior on width {
enabled: indicator.animationEnabled
NumberAnimation {
duration: Theme.mediumDuration
easing.type: Theme.standardEasing
}
}
}
Rectangle {
width: parent.width
height: 1
y: parent.height + 10
color: Theme.outlineStrong
}
function updateIndicator() {
if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
indicator.visible = false;
indicator.initialSetupComplete = false;
return;
}
const item = tabRepeater.itemAt(currentIndex);
if (!item || item.isAction) {
return;
}
const tabPos = item.mapToItem(tabBar, 0, 0);
const tabCenterX = tabPos.x + item.width / 2;
const indicatorWidth = 60;
if (tabPos.x < 10 && currentIndex > 0) {
Qt.callLater(updateIndicator);
return;
}
if (!indicator.initialSetupComplete) {
indicator.animationEnabled = false;
indicator.width = indicatorWidth;
indicator.x = tabCenterX - indicatorWidth / 2;
indicator.visible = true;
indicator.initialSetupComplete = true;
indicator.animationEnabled = true;
} else {
indicator.width = indicatorWidth;
indicator.x = tabCenterX - indicatorWidth / 2;
indicator.visible = true;
}
}
function snapIndicator() {
indicator.initialSetupComplete = false;
updateIndicator();
}
onCurrentIndexChanged: {
Qt.callLater(updateIndicator);
}
onWidthChanged: Qt.callLater(updateIndicator)
}
DankCommon.DankTabBar {}
+2 -30
View File
@@ -1,31 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Rectangle {
id: root
property bool shown: true
property bool blinkOn: true
readonly property int flashTime: Qt.styleHints.cursorFlashTime
function resetBlink() {
blinkOn = true;
blinkTimer.restart();
}
width: 2
radius: 1
color: Theme.primary
visible: shown && blinkOn
onShownChanged: resetBlink()
Timer {
id: blinkTimer
running: root.shown && root.flashTime > 1
interval: root.flashTime / 2
repeat: true
onTriggered: root.blinkOn = !root.blinkOn
}
}
DankCommon.DankTextCursor {}
+2 -289
View File
@@ -1,290 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
StyledRect {
id: root
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
KeyNavigation.tab: keyNavigationTab
KeyNavigation.backtab: keyNavigationBacktab
function checkParentDisablesTransparency() {
let p = parent;
while (p) {
if (p.disablePopupTransparency === true)
return true;
p = p.parent;
}
return false;
}
property alias text: textInput.text
property alias cursorPosition: textInput.cursorPosition
property string placeholderText: ""
property string labelText: ""
property alias font: textInput.font
property alias textColor: textInput.color
property alias echoMode: textInput.echoMode
property alias validator: textInput.validator
property alias maximumLength: textInput.maximumLength
property string leftIconName: ""
property int leftIconSize: Theme.iconSize
property color leftIconColor: Theme.surfaceVariantText
property color leftIconFocusedColor: Theme.primary
property bool showClearButton: false
property bool showPasswordToggle: false
property real rightAccessoryWidth: 0
property bool passwordVisible: false
property bool usePopupTransparency: !checkParentDisablesTransparency()
property color backgroundColor: usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
property color focusedBorderColor: Theme.primary
property color normalBorderColor: Theme.outlineMedium
property color placeholderColor: Theme.outlineButton
property real borderWidth: 1
property real focusedBorderWidth: 2
property real cornerRadius: Theme.cornerRadius
readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
readonly property real rightPadding: {
let p = Theme.spacingS + rightAccessoryWidth;
if (showPasswordToggle)
p += 20 + Theme.spacingXS;
if (showClearButton && text.length > 0)
p += 20 + Theme.spacingXS;
return p;
}
property real topPadding: Theme.spacingS
property real bottomPadding: Theme.spacingS
property bool ignoreLeftRightKeys: false
property bool ignoreUpDownKeys: false
property bool ignoreTabKeys: false
property var keyForwardTargets: []
property Item keyNavigationTab: null
property Item keyNavigationBacktab: null
signal textEdited
signal editingFinished
signal accepted
signal focusStateChanged(bool hasFocus)
function getActiveFocus() {
return textInput.activeFocus;
}
function setFocus(value) {
textInput.focus = value;
}
function forceActiveFocus() {
textInput.forceActiveFocus();
}
function selectAll() {
textInput.selectAll();
}
function clear() {
textInput.clear();
}
function insertText(str) {
textInput.insert(textInput.cursorPosition, str);
}
readonly property real labelBandHeight: Math.round(Theme.fontSizeSmall * 1.4) + Theme.spacingXS * 2
width: 200
height: labelText !== "" ? Math.round(Theme.fontSizeMedium * 3) + labelBandHeight : Math.round(Theme.fontSizeMedium * 3)
radius: cornerRadius
color: backgroundColor
border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth
DankIcon {
id: leftIcon
anchors.left: parent.left
anchors.leftMargin: Theme.spacingM
anchors.verticalCenter: textInput.verticalCenter
name: leftIconName
size: leftIconSize
color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
visible: leftIconName !== ""
}
StyledText {
id: fieldLabel
anchors.left: textInput.left
anchors.right: textInput.right
anchors.top: parent.top
anchors.topMargin: Theme.spacingXS
text: root.labelText
visible: root.labelText !== ""
font.pixelSize: Theme.fontSizeSmall
color: textInput.activeFocus ? Theme.primary : Theme.surfaceVariantText
elide: Text.ElideRight
}
TextInput {
id: textInput
anchors.left: leftIcon.visible ? leftIcon.right : parent.left
anchors.leftMargin: Theme.spacingM
anchors.right: rightButtonsRow.left
anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM
anchors.top: parent.top
anchors.topMargin: root.labelText !== "" ? root.labelBandHeight : root.topPadding
anchors.bottom: parent.bottom
anchors.bottomMargin: root.bottomPadding
font.pixelSize: Theme.fontSizeMedium
font.family: Theme.fontFamily
color: Theme.surfaceText
selectionColor: Theme.primaryContainer
selectedTextColor: Theme.primary
horizontalAlignment: TextInput.AlignLeft
verticalAlignment: TextInput.AlignVCenter
selectByMouse: !root.ignoreLeftRightKeys
clip: true
activeFocusOnTab: true
KeyNavigation.tab: root.keyNavigationTab
KeyNavigation.backtab: root.keyNavigationBacktab
onTextChanged: root.textEdited()
onEditingFinished: root.editingFinished()
onAccepted: root.accepted()
onActiveFocusChanged: root.focusStateChanged(activeFocus)
Keys.forwardTo: root.keyForwardTargets
Keys.onLeftPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
// Allow normal TextInput cursor movement
event.accepted = false;
}
}
Keys.onRightPressed: event => {
if (root.ignoreLeftRightKeys) {
event.accepted = true;
} else {
event.accepted = false;
}
}
Keys.onPressed: event => {
if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
return;
}
if (root.ignoreUpDownKeys && (event.key === Qt.Key_Up || event.key === Qt.Key_Down)) {
event.accepted = false;
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
return;
}
if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
for (var i = 0; i < root.keyForwardTargets.length; i++) {
if (root.keyForwardTargets[i])
root.keyForwardTargets[i].Keys.pressed(event);
}
}
}
MouseArea {
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.IBeamCursor
acceptedButtons: Qt.NoButton
}
}
Row {
id: rightButtonsRow
anchors.right: parent.right
anchors.rightMargin: Theme.spacingS + root.rightAccessoryWidth
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
visible: showPasswordToggle || (showClearButton && text.length > 0)
StyledRect {
id: passwordToggleButton
width: 20
height: 20
radius: 10
color: passwordToggleArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
visible: showPasswordToggle
DankIcon {
anchors.centerIn: parent
name: passwordVisible ? "visibility_off" : "visibility"
size: 14
color: passwordToggleArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
}
MouseArea {
id: passwordToggleArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: passwordVisible = !passwordVisible
}
}
StyledRect {
id: clearButton
width: 20
height: 20
radius: 10
color: clearArea.containsMouse ? Theme.outlineStrong : Theme.withAlpha(Theme.outlineStrong, 0)
visible: showClearButton && text.length > 0
DankIcon {
anchors.centerIn: parent
name: "close"
size: 14
color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
}
MouseArea {
id: clearArea
anchors.fill: parent
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: textInput.text = ""
}
}
}
StyledText {
id: placeholderLabel
anchors.fill: textInput
text: root.placeholderText
font: textInput.font
color: placeholderColor
horizontalAlignment: Text.AlignLeft
verticalAlignment: textInput.verticalAlignment
visible: textInput.text.length === 0 && !textInput.activeFocus
elide: I18n.isRtl ? Text.ElideLeft : Text.ElideRight
}
Behavior on border.color {
ColorAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
Behavior on border.width {
NumberAnimation {
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
}
DankCommon.DankTextField {}
+2 -193
View File
@@ -1,194 +1,3 @@
import QtQuick
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: toggle
LayoutMirroring.enabled: I18n.isRtl
LayoutMirroring.childrenInherit: true
// API
property bool checked: false
property bool toggling: false
property string text: ""
property string description: ""
property color descriptionColor: Theme.surfaceVariantText
property bool hideText: false
signal clicked
signal toggled(bool checked)
signal toggleCompleted(bool checked)
readonly property bool showText: text && !hideText
readonly property int trackWidth: 52
readonly property int trackHeight: 30
readonly property int insetCircle: 24
width: showText ? parent.width : trackWidth
height: showText ? Math.max(trackHeight, textColumn.implicitHeight + Theme.spacingM * 2) : trackHeight
function handleClick() {
if (!enabled)
return;
clicked();
toggled(!checked);
}
StyledRect {
id: background
anchors.fill: parent
radius: showText ? Theme.cornerRadius : 0
color: "transparent"
visible: showText
StateLayer {
visible: showText
disabled: !toggle.enabled
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: toggle.handleClick()
}
}
Row {
anchors.left: parent.left
anchors.right: toggleTrack.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: Theme.spacingM
anchors.rightMargin: Theme.spacingM
spacing: Theme.spacingXS
visible: showText
Column {
id: textColumn
width: parent.width
anchors.verticalCenter: parent.verticalCenter
spacing: Theme.spacingXS
StyledText {
text: toggle.text
font.pixelSize: Appearance.fontSize.normal
font.weight: Font.Medium
opacity: toggle.enabled ? 1 : 0.4
width: parent.width
horizontalAlignment: Text.AlignLeft
}
StyledText {
text: toggle.description
font.pixelSize: Appearance.fontSize.small
color: toggle.descriptionColor
wrapMode: Text.WordWrap
width: parent.width
visible: toggle.description.length > 0
horizontalAlignment: Text.AlignLeft
}
}
}
StyledRect {
id: toggleTrack
width: showText ? trackWidth : Math.max(parent.width, trackWidth)
height: showText ? trackHeight : Math.max(parent.height, trackHeight)
anchors.right: parent.right
anchors.rightMargin: showText ? Theme.spacingM : 0
anchors.verticalCenter: parent.verticalCenter
radius: Theme.cornerRadius
// Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.12), 0)) : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
opacity: toggle.toggling ? 0.6 : 1
// M3 disabled unchecked border: on surface 12% opacity
border.color: toggle.checked ? Theme.withAlpha(Theme.outline, 0) : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
readonly property int pad: Math.round((height - thumb.width) / 2)
readonly property int edgeLeft: pad
readonly property int edgeRight: width - thumb.width - pad
StyledRect {
id: thumb
width: toggle.checked ? insetCircle : insetCircle - 4
height: toggle.checked ? insetCircle : insetCircle - 4
radius: Theme.cornerRadius
anchors.verticalCenter: parent.verticalCenter
// M3 disabled thumb:
// checked = solid surface | unchecked = outlined off-state thumb
color: !toggle.enabled ? (toggle.checked ? Theme.surface : Theme.withAlpha(Theme.surface, 0)) : (toggle.checked ? Theme.surface : Theme.outline)
border.color: !toggle.enabled ? (toggle.checked ? Theme.withAlpha(Qt.alpha(Theme.surfaceText, 0.38), 0) : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
border.width: (toggle.checked && toggle.enabled) ? 1 : 2
x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft
Behavior on x {
SequentialAnimation {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
}
ScriptAction {
script: {
toggle.toggleCompleted(toggle.checked);
}
}
}
}
Behavior on color {
ColorAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
Behavior on border.width {
NumberAnimation {
duration: Appearance.anim.durations.normal
easing.type: Easing.BezierSpline
easing.bezierCurve: Appearance.anim.curves.emphasized
}
}
DankIcon {
id: checkIcon
anchors.centerIn: parent
name: "check"
size: 20
// M3 disabled icon: on surface 38%
color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
filled: true
opacity: (toggle.checked && toggle.enabled) ? 1 : 0
scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
Behavior on opacity {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
Behavior on scale {
NumberAnimation {
duration: Anims.durShort
easing.type: Easing.BezierSpline
easing.bezierCurve: Anims.emphasized
}
}
}
}
StateLayer {
disabled: !toggle.enabled
stateColor: Theme.primary
cornerRadius: parent.radius
onClicked: toggle.handleClick()
}
}
}
DankCommon.DankToggle {}
+2 -153
View File
@@ -1,154 +1,3 @@
import QtQuick
import QtQuick.Controls
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string text: ""
function show(text, item, offsetX, offsetY, preferredSide) {
if (!item)
return;
let windowContentItem = item.Window?.window?.contentItem;
if (!windowContentItem) {
let current = item;
while (current) {
if (current.Window?.window?.contentItem) {
windowContentItem = current.Window.window.contentItem;
break;
}
current = current.parent;
}
}
if (!windowContentItem)
return;
tooltip.parent = windowContentItem;
tooltip.text = text;
const itemPos = item.mapToItem(windowContentItem, 0, 0);
const parentWidth = windowContentItem.width;
const parentHeight = windowContentItem.height;
const tooltipWidth = tooltip.implicitWidth;
const tooltipHeight = tooltip.implicitHeight;
const side = preferredSide || _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight);
let targetX = 0;
let targetY = 0;
switch (side) {
case "left":
targetX = itemPos.x - tooltipWidth - 8;
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
break;
case "right":
targetX = itemPos.x + item.width + 8;
targetY = itemPos.y + (item.height - tooltipHeight) / 2;
break;
case "top":
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
targetY = itemPos.y - tooltipHeight - 8;
break;
case "bottom":
default:
targetX = itemPos.x + (item.width - tooltipWidth) / 2;
targetY = itemPos.y + item.height + 8;
break;
}
tooltip.x = Math.max(4, Math.min(parentWidth - tooltipWidth - 4, targetX + (offsetX || 0)));
tooltip.y = Math.max(4, Math.min(parentHeight - tooltipHeight - 4, targetY + (offsetY || 0)));
tooltip.open();
}
function _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight) {
const itemCenterX = itemPos.x + item.width / 2;
const itemCenterY = itemPos.y + item.height / 2;
const spaceLeft = itemPos.x;
const spaceRight = parentWidth - (itemPos.x + item.width);
const spaceTop = itemPos.y;
const spaceBottom = parentHeight - (itemPos.y + item.height);
if (spaceRight >= tooltipWidth + 16) {
return "right";
}
if (spaceLeft >= tooltipWidth + 16) {
return "left";
}
if (spaceBottom >= tooltipHeight + 16) {
return "bottom";
}
if (spaceTop >= tooltipHeight + 16) {
return "top";
}
if (itemCenterX > parentWidth / 2) {
return "left";
}
return "right";
}
function hide() {
tooltip.close();
}
Popup {
id: tooltip
property string text: ""
leftPadding: Theme.spacingM
rightPadding: Theme.spacingM
topPadding: Theme.spacingS
bottomPadding: Theme.spacingS
closePolicy: Popup.NoAutoClose
modal: false
dim: false
background: Rectangle {
color: Theme.surfaceContainerHigh
radius: Theme.cornerRadius
border.width: 1
border.color: Theme.outlineMedium
}
contentItem: Text {
id: textContent
width: Math.min(implicitWidth, 500)
text: tooltip.text
font.pixelSize: Theme.fontSizeSmall
color: Theme.surfaceText
wrapMode: Text.NoWrap
maximumLineCount: 1
elide: Text.ElideRight
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
enter: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: Theme.shortDuration
easing.type: Theme.standardEasing
}
}
exit: Transition {
NumberAnimation {
property: "opacity"
from: 1
to: 0
duration: Theme.shorterDuration
easing.type: Theme.standardEasing
}
}
}
}
DankCommon.DankTooltipV2 {}
+2 -114
View File
@@ -1,115 +1,3 @@
import QtQuick
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
readonly property real edgeSize: 8
required property var targetWindow
readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height
anchors.fill: parent
function tryStartMove() {
targetWindow.startSystemMove();
}
function tryStartResize(edges) {
if (!canMaximize)
return;
targetWindow.startSystemResize(edges);
}
function tryToggleMaximize() {
if (!canMaximize)
return;
targetWindow.maximized = !targetWindow.maximized;
}
MouseArea {
visible: root.canMaximize
height: root.edgeSize
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.leftMargin: 6
anchors.rightMargin: 6
cursorShape: Qt.SizeVerCursor
onPressed: root.tryStartResize(Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
anchors.left: parent.left
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 6
anchors.bottomMargin: 6
cursorShape: Qt.SizeHorCursor
onPressed: root.tryStartResize(Qt.LeftEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
anchors.right: parent.right
anchors.top: parent.top
anchors.bottom: parent.bottom
anchors.topMargin: 6
anchors.bottomMargin: 6
cursorShape: Qt.SizeHorCursor
onPressed: root.tryStartResize(Qt.RightEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.left: parent.left
anchors.top: parent.top
cursorShape: Qt.SizeFDiagCursor
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.right: parent.right
anchors.top: parent.top
cursorShape: Qt.SizeBDiagCursor
onPressed: root.tryStartResize(Qt.RightEdge | Qt.TopEdge)
}
MouseArea {
visible: root.canMaximize
height: root.edgeSize
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.leftMargin: 6
anchors.rightMargin: 6
cursorShape: Qt.SizeVerCursor
onPressed: root.tryStartResize(Qt.BottomEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.left: parent.left
anchors.bottom: parent.bottom
cursorShape: Qt.SizeBDiagCursor
onPressed: root.tryStartResize(Qt.LeftEdge | Qt.BottomEdge)
}
MouseArea {
visible: root.canMaximize
width: root.edgeSize
height: root.edgeSize
anchors.right: parent.right
anchors.bottom: parent.bottom
cursorShape: Qt.SizeFDiagCursor
onPressed: root.tryStartResize(Qt.RightEdge | Qt.BottomEdge)
}
}
DankCommon.FloatingWindowControls {}
+2 -63
View File
@@ -1,64 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
// Wave progress indicator: track, animated fill, seek preview and playhead are
// all drawn in a single fragment shader (Shaders/frag/wave_progress.frag).
Item {
id: root
property real value: 0
property real actualValue: value
property bool showActualPlaybackState: false
property real lineWidth: 2
property real wavelength: 20
property real amp: 1.6
property real phase: 0.0
property bool isPlaying: false
property real currentAmp: 1.6
property color trackColor: Theme.withAlpha(Theme.surfaceVariant, 0.40)
property color fillColor: Theme.primary
property color playheadColor: Theme.primary
property color actualProgressColor: Theme.onSurface_38
Behavior on currentAmp {
NumberAnimation {
duration: 300
easing.type: Easing.OutCubic
}
}
onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
Component.onCompleted: currentAmp = isPlaying ? amp : 0
ShaderEffect {
anchors.fill: parent
blending: true
readonly property real widthPx: width
readonly property real heightPx: height
readonly property real value: root.value
readonly property real actualValue: root.actualValue
readonly property real phase: root.phase
readonly property real ampPx: root.currentAmp
readonly property real wavelengthPx: root.wavelength
readonly property real lineWidthPx: root.lineWidth
readonly property real showActual: root.showActualPlaybackState ? 1.0 : 0.0
readonly property color fillColor: root.fillColor
readonly property color trackColor: root.trackColor
readonly property color playheadColor: root.playheadColor
readonly property color actualColor: root.actualProgressColor
fragmentShader: Qt.resolvedUrl("../Shaders/qsb/wave_progress.frag.qsb")
}
signal frameTicked
FrameAnimation {
running: root.visible && (root.isPlaying || root.currentAmp > 0) && (root.Window.window?.visible ?? false)
onTriggered: {
if (!root.isPlaying)
return;
root.phase = (root.phase + 0.03 * frameTime * 60) % 6.28318530718;
root.frameTicked();
}
}
}
DankCommon.M3WaveProgress {}
+2 -21
View File
@@ -1,22 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
StyledText {
id: root
property string reserveText: ""
readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
isMonospace: true
wrapMode: Text.NoWrap
StyledTextMetrics {
id: reserveMetrics
isMonospace: root.isMonospace
font.pixelSize: root.font.pixelSize
font.family: root.font.family
font.weight: root.font.weight
font.hintingPreference: root.font.hintingPreference
text: root.reserveText
}
}
DankCommon.NumericText {}
-14
View File
@@ -1,14 +0,0 @@
.pragma library
const friction = 0.96;
const touchpadSpeed = 3.5;
const mouseWheelSpeed = 60;
const momentumRetention = 0.92;
const momentumDeltaFactor = 0.15;
const maxMomentumVelocity = 2500;
const minMomentumVelocity = 50;
const momentumStopThreshold = 5;
const velocitySampleWindowMs = 100;
const momentumTimeThreshold = 50;
const flickDeceleration = 1500;
const maximumFlickVelocity = 2000;
+2 -73
View File
@@ -1,74 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
MouseArea {
id: root
property bool disabled: false
property color stateColor: Theme.surfaceText
property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
property var tooltipText: null
property string tooltipSide: "bottom"
property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
anchors.fill: parent
cursorShape: disabled ? undefined : Qt.PointingHandCursor
hoverEnabled: true
onPressed: mouse => {
if (!disabled && enableRipple) {
rippleLayer.trigger(mouse.x, mouse.y);
}
}
Rectangle {
id: stateRect
anchors.fill: parent
radius: root.cornerRadius
color: Theme.withAlpha(stateColor, stateOpacity)
Behavior on color {
enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
DankColorAnim {
duration: Theme.shorterDuration
easing.bezierCurve: Theme.expressiveCurves.standardDecel
}
}
}
DankRipple {
id: rippleLayer
anchors.fill: parent
rippleColor: root.stateColor
cornerRadius: root.cornerRadius
enableRipple: root.enableRipple
}
Timer {
id: hoverDelay
interval: 400
repeat: false
onTriggered: {
tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
}
}
onEntered: {
if (!tooltipText)
return;
hoverDelay.restart();
}
onExited: {
if (!tooltipText)
return;
hoverDelay.stop();
tooltip.hide();
}
DankTooltipV2 {
id: tooltip
}
}
DankCommon.StateLayer {}
+2 -28
View File
@@ -1,29 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Rectangle {
color: "transparent"
radius: Appearance.rounding.normal
readonly property var standardAnimation: {
"duration": Appearance.anim.durations.normal,
"easing.type": Easing.BezierSpline,
"easing.bezierCurve": Appearance.anim.curves.standard
}
Behavior on radius {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
Behavior on opacity {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
}
DankCommon.StyledRect {}
+2 -75
View File
@@ -1,76 +1,3 @@
import QtQuick
import qs.Common
import qs.DankCommon.Widgets as DankCommon
Text {
property bool isMonospace: false
FontLoader {
id: interFont
source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
}
FontLoader {
id: firaCodeFont
source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
}
readonly property string resolvedFontFamily: {
const requestedFont = isMonospace ? Theme.monoFontFamily : Theme.fontFamily;
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
if (requestedFont === defaultFont) {
return isMonospace ? firaCodeFont.name : interFont.name;
}
return requestedFont;
}
readonly property int resolvedRenderType: {
switch (SettingsData.textRenderType) {
case SettingsData.TextRenderType.Qt:
return Text.QtRendering;
case SettingsData.TextRenderType.Curve:
return Text.CurveRendering;
default:
return Text.NativeRendering;
}
}
readonly property int resolvedRenderQuality: {
switch (SettingsData.textRenderQuality) {
case SettingsData.TextRenderQuality.Low:
return Text.LowRenderTypeQuality;
case SettingsData.TextRenderQuality.Normal:
return Text.NormalRenderTypeQuality;
case SettingsData.TextRenderQuality.High:
return Text.HighRenderTypeQuality;
case SettingsData.TextRenderQuality.VeryHigh:
return Text.VeryHighRenderTypeQuality;
default:
return Text.DefaultRenderTypeQuality;
}
}
readonly property var standardAnimation: {
"duration": Appearance.anim.durations.normal,
"easing.type": Easing.BezierSpline,
"easing.bezierCurve": Appearance.anim.curves.standard
}
color: Theme.surfaceText
font.pixelSize: Appearance.fontSize.normal
font.family: resolvedFontFamily
font.weight: Theme.fontWeight
textFormat: Text.PlainText
wrapMode: Text.WordWrap
elide: Text.ElideRight
verticalAlignment: Text.AlignVCenter
renderType: resolvedRenderType
renderTypeQuality: resolvedRenderQuality
Behavior on opacity {
NumberAnimation {
duration: standardAnimation.duration
easing.type: standardAnimation["easing.type"]
easing.bezierCurve: standardAnimation["easing.bezierCurve"]
}
}
}
DankCommon.StyledText {}
+2 -23
View File
@@ -1,24 +1,3 @@
import QtQuick
import qs.Common
import qs.Services
import qs.DankCommon.Widgets as DankCommon
TextMetrics {
property bool isMonospace: false
readonly property string resolvedFontFamily: {
const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily;
const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
if (requestedFont === defaultFont) {
const availableFonts = Qt.fontFamilies();
if (!availableFonts.includes(requestedFont)) {
return isMonospace ? "Monospace" : "DejaVu Sans";
}
}
return requestedFont;
}
font.pixelSize: Appearance.fontSize.normal
font.family: resolvedFontFamily
font.weight: SettingsData.fontWeight
}
DankCommon.StyledTextMetrics {}
+2 -80
View File
@@ -1,81 +1,3 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Widgets
import qs.Common
import qs.Widgets
import qs.DankCommon.Widgets as DankCommon
Item {
id: root
property string colorOverride: ""
property real brightnessOverride: 0.5
property real contrastOverride: 1
readonly property bool hasColorOverride: colorOverride !== ""
property bool useNerdFont: false
property string nerdFontIcon: ""
IconImage {
id: iconImage
anchors.fill: parent
visible: !root.useNerdFont
smooth: true
asynchronous: true
layer.enabled: hasColorOverride
layer.effect: MultiEffect {
colorization: 1
colorizationColor: colorOverride
brightness: brightnessOverride
contrast: contrastOverride
}
}
DankNFIcon {
id: nfIcon
anchors.centerIn: parent
visible: root.useNerdFont
name: root.nerdFontIcon
size: Math.min(root.width, root.height)
color: hasColorOverride ? colorOverride : Theme.surfaceText
}
Component.onCompleted: {
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $ID"], (output, exitCode) => {
if (!root || exitCode !== 0 || !output) return
const distroId = output.trim()
if (!distroId) return
const supportedDistroNFs = ["debian", "arch", "archcraft", "fedora", "nixos", "ubuntu", "guix", "gentoo", "endeavouros", "manjaro", "opensuse"]
if (supportedDistroNFs.includes(distroId)) {
if (!root) return
root.useNerdFont = true
root.nerdFontIcon = distroId
return
}
Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (logoOutput, logoExitCode) => {
if (!root || !iconImage || logoExitCode !== 0 || !logoOutput) return
const logo = logoOutput.trim()
if (!logo) return
if (logo === "cachyos") {
iconImage.source = "file:///usr/share/icons/cachyos.svg"
return
}
if (logo === "guix-icon") {
iconImage.source = "file:///run/current-system/profile/share/icons/hicolor/scalable/apps/guix-icon.svg"
return
}
if (logo === "zirconium") {
iconImage.source = "file:///usr/share/zirconium/pixmaps/logo-z.svg"
return
}
iconImage.source = Quickshell.iconPath(logo, true)
}, 0)
}, 0)
}
}
DankCommon.SystemLogo {}
Binary file not shown.
-92
View File
@@ -1,92 +0,0 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

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