Compare commits

..
39 Commits
Author SHA1 Message Date
arabianq 1616d80ee9 change version to 1.14.0 2026-07-24 23:58:39 +03:00
arabianq d97d4c60d2 cargo update 2026-07-24 23:57:38 +03:00
Tarasov AleksandrandGitHub 0c3d7cbd35 feat(gui): better search with automatic tree expansion, directories scanning and caching 2026-07-24 23:49:10 +03:00
arabianq 6d7abdbf53 change version to 1.13.0 2026-07-24 05:52:27 +03:00
arabianq 41bb035b2c cargo update 2026-07-24 05:43:52 +03:00
arabianq 3873bf4270 deps: update rodio 2026-07-24 05:43:02 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2b60b47cce chore(deps): bump clap from 4.6.3 to 4.6.4 in the clap-stack group (#176)
* chore(deps): bump clap from 4.6.3 to 4.6.4 in the clap-stack group

Bumps the clap-stack group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.6.3 to 4.6.4
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.3...clap_complete-v4.6.4)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: clap-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 05:42:53 +03:00
Tarasov AleksandrandGitHub 65ae8589db feat(lib, daemon, cli, gui): add volume multiplier (#175)
* initial implementation

* rename DaemonConfig.volume_multiplier to default_volume_multiplier

* add volume_multiplier to the FullState

* add pwsp-gui integration
2026-07-24 05:33:51 +03:00
Tarasov AleksandrandGitHub a33d023950 feat(daemon, cli): global DaemonConfig and an ability to get/save it using cli (#174)
* use one global Arc<DaemonConfig>

* implement commands

* replace Arc<DaemonConfig> with Arc<Mutex<DaemonConfig>> and fix pwsp-gui

* add pwsp-cli support

* replace serde_json::to_string_pretty with to_string

* refactor

* implement UpdateDaemonConfigCommand and use it in pwsp-gui so it now uses real in-memory config, not the saved one

* implement utils::gui get_daemon_config and update_daemon_config to simplify code
2026-07-24 04:49:51 +03:00
arabianq 5c73bc1bea fix cagro sources 2026-07-20 21:26:08 +03:00
arabianq 42e305869a change version to 1.12.5 2026-07-20 21:20:02 +03:00
arabianq cb7f4f7f6b update cargo-sources.json 2026-07-20 21:18:43 +03:00
arabianq 62821904af cargo update 2026-07-20 21:16:56 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tarasov Aleksandrarabianq
3a218283db chore(deps): bump the serde-stack group with 2 updates (#169)
* chore(deps): bump the serde-stack group with 2 updates

Bumps the serde-stack group with 2 updates: [serde](https://github.com/serde-rs/serde) and [serde_json](https://github.com/serde-rs/json).


Updates `serde` from 1.0.228 to 1.0.229
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.228...v1.0.229)

Updates `serde_json` from 1.0.150 to 1.0.151
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.150...v1.0.151)

---
updated-dependencies:
- dependency-name: serde
  dependency-version: 1.0.229
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: serde-stack
- dependency-name: serde_json
  dependency-version: 1.0.151
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: serde-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

* fix

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-20 20:58:10 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Tarasov Aleksandr
02fdfb6ca8 chore(deps): bump rust-i18n from 4.2.0 to 4.2.1 (#172)
* chore(deps): bump rust-i18n from 4.2.0 to 4.2.1

Bumps [rust-i18n](https://github.com/longbridge/rust-i18n) from 4.2.0 to 4.2.1.
- [Release notes](https://github.com/longbridge/rust-i18n/releases)
- [Commits](https://github.com/longbridge/rust-i18n/compare/v4.2.0...v4.2.1)

---
updated-dependencies:
- dependency-name: rust-i18n
  dependency-version: 4.2.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Tarasov Aleksandr <55220741+arabianq@users.noreply.github.com>
2026-07-20 19:33:48 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a027838d0e chore(deps): bump tokio from 1.52.3 to 1.53.0 in the tokio-stack group (#170)
* chore(deps): bump tokio from 1.52.3 to 1.53.0 in the tokio-stack group

Bumps the tokio-stack group with 1 update: [tokio](https://github.com/tokio-rs/tokio).


Updates `tokio` from 1.52.3 to 1.53.0
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.52.3...tokio-1.53.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-version: 1.53.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: tokio-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:22:03 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1828d903a1 chore(deps): bump clap from 4.6.1 to 4.6.3 in the clap-stack group (#171)
* chore(deps): bump clap from 4.6.1 to 4.6.3 in the clap-stack group

Bumps the clap-stack group with 1 update: [clap](https://github.com/clap-rs/clap).


Updates `clap` from 4.6.1 to 4.6.3
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.6.1...clap_complete-v4.6.3)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: clap-stack
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:21:48 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
83ca7a5e34 chore(deps): bump async-trait from 0.1.89 to 0.1.91 (#173)
* chore(deps): bump async-trait from 0.1.89 to 0.1.91

Bumps [async-trait](https://github.com/dtolnay/async-trait) from 0.1.89 to 0.1.91.
- [Release notes](https://github.com/dtolnay/async-trait/releases)
- [Commits](https://github.com/dtolnay/async-trait/compare/0.1.89...0.1.91)

---
updated-dependencies:
- dependency-name: async-trait
  dependency-version: 0.1.91
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(flatpak): automatically update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:18:56 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f2aca5e34a chore(deps): bump actions/setup-python from 6 to 7 (#168)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 19:13:41 +03:00
6130a99c4b fix(lib): don't create '~' in $HOME (#166)
* fix(lib): don't create '~' in $HOME

* throw an error wen fail to get home directory

---------

Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-20 19:02:10 +03:00
tonocoandGitHub 322b93efa1 fix(scripts): check workspace_package_match and version_match type before passing (#167) 2026-07-19 17:22:46 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>arabianq
683f4127a0 chore(deps): bump rust-i18n from 4.1.0 to 4.2.0 (#163)
* chore(deps): bump rust-i18n from 4.1.0 to 4.2.0

Bumps [rust-i18n](https://github.com/longbridge/rust-i18n) from 4.1.0 to 4.2.0.
- [Release notes](https://github.com/longbridge/rust-i18n/releases)
- [Commits](https://github.com/longbridge/rust-i18n/compare/v4.1.0...v4.2.0)

---
updated-dependencies:
- dependency-name: rust-i18n
  dependency-version: 4.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* update cargo-sources.json

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: arabian <a.tevg@ya.ru>
2026-07-15 16:21:05 +03:00
arabianq c873166e21 fix(dependabot): install tomlkit 2026-07-14 16:43:57 +03:00
Tarasov AleksandrandGitHub ec4e873071 refactor: better systemd service (#161)
* move `pwsp-daemon.service` from `pwsp-gui/assets` to pwsp-daemon/assets + autorestart service + better logging + wait for pipewire until start

* adapt all packages

* improve systemd service calling for .deb package
2026-07-07 21:29:26 +03:00
arabianq a91252d7fb change version to 1.12.4 2026-07-07 19:31:00 +03:00
arabianq 239170df45 cargo update 2026-07-07 19:30:15 +03:00
arabianq 5fef42a2e2 cargo fmt 2026-07-07 19:29:48 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
4f8f663f12 fix(pwsp-gui): fix arbitrary file overwrite via path traversal (#159)
* Fix path traversal vulnerability in audio download logic

Co-authored-by: arabianq <55220741+arabianq@users.noreply.github.com>

* std::path::Path -> Path

* add random id to the filename if it's unknown

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:29:12 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
6ccc55a1d0 perf(pwsp-gui): optimize gui file filtering performance (#160)
Avoid unnecessary string cloning and allocation in `pwsp-gui/src/gui/mod.rs` when checking if a file matches the search query.

Specifically, we replaced `.to_string_lossy().to_string()` with `.to_string_lossy()` because `.to_lowercase()` directly works on `Cow<str>`, avoiding an intermediate String allocation.

Measured improvement was verified using an isolated benchmark demonstrating roughly 10-15% reduction in execution time.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 19:00:16 +03:00
Tarasov AleksandrGitHubgoogle-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
d266a3f073 fix(pwsp-lib): fixed unhandled panic on missing config directory (#157)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-07-07 18:58:43 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2993592be chore(deps): bump actions/upload-artifact from 4 to 7 (#153)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:56:24 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1a53ab62dc chore(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#154)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 16:06:50 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
51fcd94a61 chore(deps): bump actions/checkout from 4 to 7 (#155)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:59:53 +03:00
arabianq 7f6a122816 ci(flatpak): use /run/ccache for flatpak builds 2026-07-07 15:50:06 +03:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7ca01ed8cb chore(deps): bump stefanzweifel/git-auto-commit-action from 5 to 7 (#156)
Bumps [stefanzweifel/git-auto-commit-action](https://github.com/stefanzweifel/git-auto-commit-action) from 5 to 7.
- [Release notes](https://github.com/stefanzweifel/git-auto-commit-action/releases)
- [Changelog](https://github.com/stefanzweifel/git-auto-commit-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stefanzweifel/git-auto-commit-action/compare/v5...v7)

---
updated-dependencies:
- dependency-name: stefanzweifel/git-auto-commit-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-07 15:41:31 +03:00
arabianq 07321afb60 docs(README): change pwsp-gui to pwsp 2026-07-05 22:41:49 +03:00
arabianq 8534e516ad docs(README): change arabianq-repo to pwsp for flatpak repo name 2026-07-05 22:41:15 +03:00
arabianq 16ce233f79 packages(flatapak): fix gpg key for flatpak repo 2026-07-05 22:36:20 +03:00
arabianq c2f4658871 packages(flatpak): add flatpakrepo file generation 2026-07-05 22:22:50 +03:00
41 changed files with 1926 additions and 1024 deletions
+7 -7
View File
@@ -37,7 +37,7 @@ jobs:
pkg-config pkg-config
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -90,7 +90,7 @@ jobs:
zip -j "$ARCHIVE_NAME" "${FILES[@]}" zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload ZIP artifact - name: Upload ZIP artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: archive-${{ matrix.arch }} name: archive-${{ matrix.arch }}
path: pwsp-*.zip path: pwsp-*.zip
@@ -124,7 +124,7 @@ jobs:
fi fi
- name: Upload .deb artifact - name: Upload .deb artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: deb-packages-${{ matrix.arch }} name: deb-packages-${{ matrix.arch }}
path: target/debian/*.deb path: target/debian/*.deb
@@ -139,7 +139,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Setup GPG - name: Setup GPG
id: gpg id: gpg
@@ -156,7 +156,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Checkout existing gh-pages - name: Checkout existing gh-pages
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: gh-pages ref: gh-pages
path: gh-pages-branch path: gh-pages-branch
@@ -202,7 +202,7 @@ jobs:
done done
- name: Deploy APT Repo to GitHub Pages - name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo publish_dir: ./apt-repo
@@ -225,7 +225,7 @@ jobs:
options: --privileged options: --privileged
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v7
- name: Build Flatpak - name: Build Flatpak
uses: flatpak/flatpak-github-actions/flatpak-builder@v6 uses: flatpak/flatpak-github-actions/flatpak-builder@v6
+4 -4
View File
@@ -14,23 +14,23 @@ jobs:
contents: write # Needed to push back to the branch contents: write # Needed to push back to the branch
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: ${{ github.head_ref }} ref: ${{ github.head_ref }}
- name: Setup Python - name: Setup Python
uses: actions/setup-python@v6 uses: actions/setup-python@v7
with: with:
python-version: "3.x" python-version: "3.x"
- name: Install dependencies - name: Install dependencies
run: pip install aiohttp toml run: pip install aiohttp tomlkit
- name: Generate cargo-sources.json - name: Generate cargo-sources.json
run: bash ./scripts/generate-sources.sh run: bash ./scripts/generate-sources.sh
- name: Commit and push changes - name: Commit and push changes
uses: stefanzweifel/git-auto-commit-action@v5 uses: stefanzweifel/git-auto-commit-action@v7
with: with:
commit_message: "chore(flatpak): automatically update cargo-sources.json" commit_message: "chore(flatpak): automatically update cargo-sources.json"
file_pattern: "packages/flatpak/cargo-sources.json" file_pattern: "packages/flatpak/cargo-sources.json"
+18 -3
View File
@@ -40,7 +40,7 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: ${{ inputs.tag_name || github.ref }} ref: ${{ inputs.tag_name || github.ref }}
@@ -103,7 +103,7 @@ jobs:
pattern: ru.arabianq.pwsp-* pattern: ru.arabianq.pwsp-*
- name: Checkout existing gh-pages - name: Checkout existing gh-pages
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: gh-pages ref: gh-pages
path: gh-pages-branch path: gh-pages-branch
@@ -155,8 +155,23 @@ jobs:
# Update summary and sign the repository # Update summary and sign the repository
flatpak build-update-repo --generate-static-deltas --gpg-sign=${{ steps.gpg.outputs.fingerprint }} pages-repo flatpak build-update-repo --generate-static-deltas --gpg-sign=${{ steps.gpg.outputs.fingerprint }} pages-repo
# Export GPG public key for manual import
gpg --export ${{ steps.gpg.outputs.fingerprint }} > pages-repo/pwsp.gpg
# Generate a .flatpakrepo file for easy installation
GPG_BASE64=$(gpg --export ${{ steps.gpg.outputs.fingerprint }} | base64 -w 0)
cat <<EOF | sed 's/^ *//' > pages-repo/pwsp.flatpakrepo
[Flatpak Repo]
Title=PipeWire Soundpad
Url=https://arabianq.github.io/pipewire-soundpad/
Homepage=https://github.com/arabianq/pipewire-soundpad
Comment=PipeWire Soundpad Flatpak Repository
Description=Official repository for PWSP
GPGKey=${GPG_BASE64}
EOF
- name: Deploy Flatpak Repo to GitHub Pages - name: Deploy Flatpak Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./pages-repo publish_dir: ./pages-repo
+6 -6
View File
@@ -74,7 +74,7 @@ jobs:
pkg-config pkg-config
- name: Checkout code at tag - name: Checkout code at tag
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: ${{ needs.prepare.outputs.tag }} ref: ${{ needs.prepare.outputs.tag }}
fetch-depth: 0 fetch-depth: 0
@@ -119,7 +119,7 @@ jobs:
zip -j "$ARCHIVE_NAME" "${FILES[@]}" zip -j "$ARCHIVE_NAME" "${FILES[@]}"
- name: Upload ZIP artifact - name: Upload ZIP artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: zip-archive-${{ matrix.arch }} name: zip-archive-${{ matrix.arch }}
path: pwsp-*.zip path: pwsp-*.zip
@@ -144,7 +144,7 @@ jobs:
cargo-deb -p pwsp-gui --no-build cargo-deb -p pwsp-gui --no-build
- name: Upload DEB artifact - name: Upload DEB artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v7
with: with:
name: deb-package-${{ matrix.arch }} name: deb-package-${{ matrix.arch }}
path: target/debian/*.deb path: target/debian/*.deb
@@ -196,7 +196,7 @@ jobs:
contents: write contents: write
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v7
- name: Setup GPG - name: Setup GPG
id: gpg id: gpg
@@ -213,7 +213,7 @@ jobs:
merge-multiple: true merge-multiple: true
- name: Checkout existing gh-pages - name: Checkout existing gh-pages
uses: actions/checkout@v4 uses: actions/checkout@v7
with: with:
ref: gh-pages ref: gh-pages
path: gh-pages-branch path: gh-pages-branch
@@ -264,7 +264,7 @@ jobs:
done done
- name: Deploy APT Repo to GitHub Pages - name: Deploy APT Repo to GitHub Pages
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./apt-repo publish_dir: ./apt-repo
Generated
+419 -326
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -8,7 +8,7 @@ members = [
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "1.12.3" version = "1.14.0"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
homepage = "https://pwsp.arabianq.ru" homepage = "https://pwsp.arabianq.ru"
@@ -52,7 +52,7 @@ rustix = { version = "1.1.4", features = ["process"] }
rust-i18n = "4.0.0" rust-i18n = "4.0.0"
sys-locale = "0.3.2" sys-locale = "0.3.2"
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "4746803c2e5f67004264ac946708d47a9faf510c", default-features = false, features = [ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "610955cf524467690cc9abe1a6277e8bffcadb71", default-features = false, features = [
"symphonia-all", "symphonia-all",
"symphonia-libopus", "symphonia-libopus",
"playback", "playback",
+4 -4
View File
@@ -33,13 +33,13 @@ Add our official OSTree repository and install the application:
```bash ```bash
# Add the repository # Add the repository
flatpak remote-add --if-not-exists arabianq-repo https://arabianq.github.io/pipewire-soundpad/ flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Install the Stable version # Install the Stable version
flatpak install arabianq-repo ru.arabianq.pwsp//stable flatpak install pwsp ru.arabianq.pwsp//stable
# OR Install the Nightly version (rolling updates) # OR Install the Nightly version (rolling updates)
flatpak install arabianq-repo ru.arabianq.pwsp//nightly flatpak install pwsp ru.arabianq.pwsp//nightly
``` ```
### 🟠 Debian / Ubuntu (APT Repository) ### 🟠 Debian / Ubuntu (APT Repository)
@@ -59,7 +59,7 @@ echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipe
# 3. Update and install # 3. Update and install
sudo apt update sudo apt update
sudo apt install pwsp-gui sudo apt install pwsp
``` ```
### 🐧 Fedora / RHEL (COPR) ### 🐧 Fedora / RHEL (COPR)
+4 -4
View File
@@ -33,13 +33,13 @@
```bash ```bash
# Добавьте репозиторий # Добавьте репозиторий
flatpak remote-add --if-not-exists arabianq-repo https://arabianq.github.io/pipewire-soundpad/ flatpak remote-add --if-not-exists pwsp https://arabianq.github.io/pipewire-soundpad/pwsp.flatpakrepo
# Установите стабильную (Stable) версию # Установите стабильную (Stable) версию
flatpak install arabianq-repo ru.arabianq.pwsp//stable flatpak install pwsp ru.arabianq.pwsp//stable
# ИЛИ установите Nightly версию (самые свежие обновления) # ИЛИ установите Nightly версию (самые свежие обновления)
flatpak install arabianq-repo ru.arabianq.pwsp//nightly flatpak install pwsp ru.arabianq.pwsp//nightly
``` ```
### 🟠 Debian / Ubuntu (APT Репозиторий) ### 🟠 Debian / Ubuntu (APT Репозиторий)
@@ -59,7 +59,7 @@ echo "deb [signed-by=/etc/apt/keyrings/pwsp.gpg] https://arabianq.github.io/pipe
# 3. Обновите индексы и установите # 3. Обновите индексы и установите
sudo apt update sudo apt update
sudo apt install pwsp-gui sudo apt install pwsp
``` ```
### 🐧 Fedora / RHEL (COPR) ### 🐧 Fedora / RHEL (COPR)
+4 -4
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp-bin pkgbase = pwsp-bin
pkgdesc = Lets you play audio files through your microphone (Pre-built binaries) pkgdesc = Lets you play audio files through your microphone (Pre-built binaries)
pkgver = 1.12.3 pkgver = 1.14.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -10,11 +10,11 @@ pkgbase = pwsp-bin
depends = alsa-lib depends = alsa-lib
provides = pwsp provides = pwsp
conflicts = pwsp conflicts = pwsp
source = pipewire-soundpad-1.12.3.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.3.tar.gz source = pipewire-soundpad-1.14.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.14.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
source_x86_64 = pwsp-1.12.3-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.3/pwsp-v1.12.3-linux-x64.zip source_x86_64 = pwsp-1.14.0-x86_64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.14.0/pwsp-v1.14.0-linux-x64.zip
sha256sums_x86_64 = SKIP sha256sums_x86_64 = SKIP
source_aarch64 = pwsp-1.12.3-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.12.3/pwsp-v1.12.3-linux-arm64.zip source_aarch64 = pwsp-1.14.0-aarch64.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.14.0/pwsp-v1.14.0-linux-arm64.zip
sha256sums_aarch64 = SKIP sha256sums_aarch64 = SKIP
pkgname = pwsp-bin pkgname = pwsp-bin
+2 -2
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgname=pwsp-bin pkgname=pwsp-bin
_pkgname=pipewire-soundpad _pkgname=pipewire-soundpad
pkgver=1.12.3 pkgver=1.14.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)" pkgdesc="Lets you play audio files through your microphone (Pre-built binaries)"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -28,7 +28,7 @@ package() {
install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop" install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "$_srcsrc/pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png" install -Dm644 "$_srcsrc/pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "$_srcsrc/pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service" install -Dm644 "$_srcsrc/pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" install -Dm644 "$_srcsrc/LICENSE" "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE"
} }
+2 -2
View File
@@ -1,6 +1,6 @@
pkgbase = pwsp pkgbase = pwsp
pkgdesc = Lets you play audio files through your microphone pkgdesc = Lets you play audio files through your microphone
pkgver = 1.12.3 pkgver = 1.14.0
pkgrel = 1 pkgrel = 1
url = https://github.com/arabianq/pipewire-soundpad url = https://github.com/arabianq/pipewire-soundpad
arch = x86_64 arch = x86_64
@@ -12,7 +12,7 @@ pkgbase = pwsp
makedepends = cmake makedepends = cmake
makedepends = pipewire makedepends = pipewire
makedepends = alsa-lib makedepends = alsa-lib
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.12.3.tar.gz source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.14.0.tar.gz
sha256sums = SKIP sha256sums = SKIP
pkgname = pwsp pkgname = pwsp
+2 -2
View File
@@ -1,7 +1,7 @@
# Maintainer: Alexander Tarasov <a.tevg@ya.ru> # Maintainer: Alexander Tarasov <a.tevg@ya.ru>
pkgsubn=pwsp pkgsubn=pwsp
pkgname=pwsp pkgname=pwsp
pkgver=1.12.3 pkgver=1.14.0
pkgrel=1 pkgrel=1
pkgdesc="Lets you play audio files through your microphone" pkgdesc="Lets you play audio files through your microphone"
arch=('x86_64' 'aarch64') arch=('x86_64' 'aarch64')
@@ -44,5 +44,5 @@ package() {
install -Dm644 "pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop" install -Dm644 "pwsp-gui/assets/pwsp-gui.desktop" "${pkgdir}/usr/share/applications/pwsp-gui.desktop"
install -Dm644 "pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png" install -Dm644 "pwsp-gui/assets/icon.png" "${pkgdir}/usr/share/icons/hicolor/256x256/apps/pwsp.png"
install -Dm644 "pwsp-gui/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service" install -Dm644 "pwsp-daemon/assets/pwsp-daemon.service" "${pkgdir}/usr/lib/systemd/user/pwsp-daemon.service"
} }
File diff suppressed because one or more lines are too long
@@ -25,7 +25,7 @@
<name>arabian</name> <name>arabian</name>
</developer> </developer>
<releases> <releases>
<release version="1.12.3" date="2026-07-05" /> <release version="1.14.0" date="2026-07-24" />
</releases> </releases>
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
</component> </component>
+1
View File
@@ -30,6 +30,7 @@ modules:
- name: pwsp - name: pwsp
buildsystem: simple buildsystem: simple
build-commands: build-commands:
- if [ -w /run/ccache ]; then mkdir -p /run/ccache/cargo-target && rm -rf target && ln -s /run/ccache/cargo-target target; fi
- export CARGO_HOME=$PWD/cargo && cargo build --release --offline - export CARGO_HOME=$PWD/cargo && cargo build --release --offline
- install -Dm755 target/release/pwsp-daemon /app/bin/pwsp-daemon - install -Dm755 target/release/pwsp-daemon /app/bin/pwsp-daemon
- install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli - install -Dm755 target/release/pwsp-cli /app/bin/pwsp-cli
+10 -1
View File
@@ -68,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files %files
%license LICENSE %license LICENSE
+11 -2
View File
@@ -13,7 +13,7 @@
Name: pwsp Name: pwsp
Version: 1.12.3 Version: 1.14.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
@@ -68,7 +68,16 @@ install -Dm755 target/release/pwsp-gui %{buildroot}%{_bindir}/pwsp-gui
install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop install -Dm644 pwsp-gui/assets/pwsp-gui.desktop %{buildroot}%{_datadir}/applications/pwsp.desktop
install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png install -Dm644 pwsp-gui/assets/icon.png %{buildroot}%{_datadir}/icons/hicolor/256x256/apps/pwsp.png
install -Dm644 pwsp-gui/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service install -Dm644 pwsp-daemon/assets/pwsp-daemon.service %{buildroot}/usr/lib/systemd/user/pwsp-daemon.service
%post
%systemd_user_post pwsp-daemon.service
%preun
%systemd_user_preun pwsp-daemon.service
%postun
%systemd_user_postun_with_restart pwsp-daemon.service
%files %files
%license LICENSE %license LICENSE
+12
View File
@@ -69,6 +69,8 @@ enum Actions {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Daemon configuration
SaveDaemonConfig,
/// Play a sound by hotkey slot name /// Play a sound by hotkey slot name
PlayHotkey { slot: String }, PlayHotkey { slot: String },
/// Remove the hotkey slot /// Remove the hotkey slot
@@ -86,6 +88,8 @@ enum GetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier,
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
#[clap(short, long)] #[clap(short, long)]
@@ -106,6 +110,8 @@ enum GetCommands {
Inputs, Inputs,
/// Version of the daemon /// Version of the daemon
DaemonVersion, DaemonVersion,
/// Daemon configuration
DaemonConfig,
/// Full player state /// Full player state
FullState, FullState,
/// All hotkey slots /// All hotkey slots
@@ -120,6 +126,8 @@ enum SetCommands {
#[clap(short, long)] #[clap(short, long)]
id: Option<u32>, id: Option<u32>,
}, },
/// Volume multiplier for all tracks
VolumeMultiplier { volume: f32 },
/// Playback position (in seconds) /// Playback position (in seconds)
Position { Position {
position: f32, position: f32,
@@ -165,6 +173,7 @@ async fn main() -> Result<()> {
concurrent, concurrent,
} => Request::play(&file_path.to_string_lossy(), concurrent), } => Request::play(&file_path.to_string_lossy(), concurrent),
Actions::ToggleLoop { id } => Request::toggle_loop(id), Actions::ToggleLoop { id } => Request::toggle_loop(id),
Actions::SaveDaemonConfig => Request::save_daemon_config(),
Actions::PlayHotkey { slot } => Request::play_hotkey(&slot), Actions::PlayHotkey { slot } => Request::play_hotkey(&slot),
Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot), Actions::ClearHotkey { slot } => Request::clear_hotkey(&slot),
Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot), Actions::ClearHotkeyKey { slot } => Request::clear_hotkey_key(&slot),
@@ -172,6 +181,7 @@ async fn main() -> Result<()> {
Commands::Get { parameter } => match parameter { Commands::Get { parameter } => match parameter {
GetCommands::IsPaused => Request::get_is_paused(), GetCommands::IsPaused => Request::get_is_paused(),
GetCommands::Volume { id } => Request::get_volume(id), GetCommands::Volume { id } => Request::get_volume(id),
GetCommands::VolumeMultiplier => Request::get_volume_multiplier(),
GetCommands::Position { id } => Request::get_position(id), GetCommands::Position { id } => Request::get_position(id),
GetCommands::Duration { id } => Request::get_duration(id), GetCommands::Duration { id } => Request::get_duration(id),
GetCommands::State => Request::get_state(), GetCommands::State => Request::get_state(),
@@ -179,11 +189,13 @@ async fn main() -> Result<()> {
GetCommands::Input => Request::get_input(), GetCommands::Input => Request::get_input(),
GetCommands::Inputs => Request::get_inputs(), GetCommands::Inputs => Request::get_inputs(),
GetCommands::DaemonVersion => Request::get_daemon_version(), GetCommands::DaemonVersion => Request::get_daemon_version(),
GetCommands::DaemonConfig => Request::get_daemon_config(),
GetCommands::FullState => Request::get_full_state(), GetCommands::FullState => Request::get_full_state(),
GetCommands::Hotkeys => Request::get_hotkeys(), GetCommands::Hotkeys => Request::get_hotkeys(),
}, },
Commands::Set { parameter } => match parameter { Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume, id } => Request::set_volume(volume, id), SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
SetCommands::VolumeMultiplier { volume } => Request::set_volume_multiplier(volume),
SetCommands::Position { position, id } => Request::seek(position, id), SetCommands::Position { position, id } => Request::seek(position, id),
SetCommands::Input { name } => Request::set_input(&name), SetCommands::Input { name } => Request::set_input(&name),
SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id),
+16
View File
@@ -0,0 +1,16 @@
[Unit]
Description=Pipewire Soundpad Daemon
Requires=pipewire.service
After=pipewire.service
PartOf=graphical-session.target
[Service]
Type=simple
ExecStart=/usr/bin/pwsp-daemon
Restart=on-failure
RestartSec=3
RuntimeDirectory=pwsp
SyslogIdentifier=pwsp-daemon
[Install]
WantedBy=default.target
+3 -1
View File
@@ -20,6 +20,7 @@ anyhow.workspace = true
serde.workspace = true serde.workspace = true
serde_json.workspace = true serde_json.workspace = true
dirs.workspace = true
egui.workspace = true egui.workspace = true
eframe.workspace = true eframe.workspace = true
@@ -38,6 +39,7 @@ percent-encoding.workspace = true
name = "pwsp" name = "pwsp"
section = "sound" section = "sound"
priority = "optional" priority = "optional"
maintainer-scripts = "maintainer-scripts"
assets = [ assets = [
[ [
"target/release/pwsp-daemon", "target/release/pwsp-daemon",
@@ -65,7 +67,7 @@ assets = [
"644", "644",
], ],
[ [
"assets/pwsp-daemon.service", "../pwsp-daemon/assets/pwsp-daemon.service",
"usr/lib/systemd/user/pwsp-daemon.service", "usr/lib/systemd/user/pwsp-daemon.service",
"644", "644",
], ],
-11
View File
@@ -1,11 +0,0 @@
[Unit]
Description=Pipewire Soundpad Daemon
After=pipewire.service
[Service]
ExecStart=/usr/bin/pwsp-daemon
Restart=no
RuntimeDirectory=pwsp
[Install]
WantedBy=default.target
+33
View File
@@ -217,6 +217,28 @@ kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול" he = "זכור תמיד עוצמת קול"
pt-BR = "Lembrar volume" pt-BR = "Lembrar volume"
[gui.settings.remember_volume_multiplier]
en = "Always remember volume multiplier"
ru = "Всегда запоминать множитель громкости"
es = "Recordar siempre el multiplicador de volumen"
fr = "Toujours se souvenir du multiplicateur de volume"
zh = "始终记住音量乘数"
ar = "تذكر ضرب الصوت دائمًا"
kz = "Әрқашан дыбыс деңгейін есте сақтау"
he = "זכור תמיד עוצמת קול"
pt-BR = "Lembrar volume"
[gui.settings.volume_multiplier]
en = "Volume multiplier"
ru = "Множитель громкости"
es = "Multiplicador de volumen"
fr = "Multiplicateur de volume"
zh = "音量乘数"
ar = "ضرب الصوت"
kz = "Дыбыс деңгейі"
he = "עוצמת קול"
pt-BR = "Volume"
[gui.settings.remember_mic] [gui.settings.remember_mic]
en = "Always remember microphone" en = "Always remember microphone"
ru = "Всегда запоминать микрофон" ru = "Всегда запоминать микрофон"
@@ -331,6 +353,17 @@ kz = "Ыстық пернелерді іздеу..."
he = "חפש מקשי קיצור..." he = "חפש מקשי קיצור..."
pt-BR = "Buscar atalhos..." pt-BR = "Buscar atalhos..."
[gui.search_truncated_warning]
en = "... and %{count} more results"
ru = "... и ещё %{count} результатов"
es = "... y %{count} resultados más"
fr = "... et %{count} résultats de plus"
zh = "... 还有 %{count} 个结果"
ar = "... و %{count} نتائج أخرى"
kz = "... және тағы %{count} нәтиже"
he = "... ועוד %{count} תוצאות"
pt-BR = "... e mais %{count} resultados"
[gui.hotkeys.add_command_select] [gui.hotkeys.add_command_select]
en = "Add Command" en = "Add Command"
ru = "Добавить команду" ru = "Добавить команду"
+21
View File
@@ -0,0 +1,21 @@
#!/bin/sh
set -e
if [ "$1" = "configure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global enable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user try-restart pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+18
View File
@@ -0,0 +1,18 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ]; then
if [ -x "/usr/bin/systemctl" ]; then
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user daemon-reload" "$username" || true
fi
fi
done
fi
fi
exit 0
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -e
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
if [ -x "/usr/bin/systemctl" ]; then
systemctl --global disable pwsp-daemon.service || true
for uid_dir in /run/user/*; do
if [ -d "$uid_dir/systemd" ]; then
uid=$(basename "$uid_dir")
if [ "$uid" != "0" ]; then
username=$(id -un "$uid" 2>/dev/null) || continue
su -s /bin/sh -c "XDG_RUNTIME_DIR=$uid_dir systemctl --user stop pwsp-daemon.service" "$username" || true
fi
fi
done
fi
fi
exit 0
+10 -9
View File
@@ -92,7 +92,7 @@ impl SoundpadGui {
} }
pub fn handle_input(&mut self, ctx: &Context) { pub fn handle_input(&mut self, ctx: &Context) {
let _modifiers = self.modifiers(ctx); let modifiers = self.modifiers(ctx);
let search_focused = { let search_focused = {
if let Some(focused_id) = self.get_focused(ctx) if let Some(focused_id) = self.get_focused(ctx)
&& let Some(search_id) = self.app_state.search_field_id && let Some(search_id) = self.app_state.search_field_id
@@ -185,14 +185,15 @@ impl SoundpadGui {
} }
// Focus search field // Focus search field
if self.key_pressed(ctx, Key::Slash) { if modifiers.ctrl && self.key_pressed(ctx, Key::F) {
if search_focused { self.app_state.force_focus_search = true;
ctx.memory_mut(|m| { }
m.request_focus(Id::NULL);
}); // Unfocus search field
} else { if search_focused && self.key_pressed(ctx, Key::Escape) {
self.app_state.force_focus_search = true; ctx.memory_mut(|m| {
} m.request_focus(Id::NULL);
});
} }
// Check for hotkey chord triggers // Check for hotkey chord triggers
+186 -54
View File
@@ -9,22 +9,24 @@ use itertools::Itertools;
use pwsp_lib::{ use pwsp_lib::{
types::{ types::{
audio_player::PlayerState, audio_player::PlayerState,
config::GuiConfig, config::{GuiConfig, HotkeyConfig},
config::HotkeyConfig,
gui::{AppState, AudioPlayerState}, gui::{AppState, AudioPlayerState},
socket::Request, socket::Request,
}, },
utils::{ utils::gui::{
daemon::get_daemon_config, get_daemon_config, get_gui_config, make_request_async, make_request_sync,
gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread}, start_app_state_thread, update_daemon_config,
}, },
}; };
use rfd::FileDialog; use rfd::FileDialog;
use std::{ use std::{
cmp::Ordering, cmp::Ordering,
collections::{HashMap, HashSet},
fs, fs,
hash::{DefaultHasher, Hash, Hasher},
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::{Arc, Mutex}, sync::{Arc, Mutex},
thread,
}; };
use system_fonts::{FontStyle, FoundFontSource, find_for_locale}; use system_fonts::{FontStyle, FoundFontSource, find_for_locale};
@@ -32,6 +34,16 @@ const SUPPORTED_EXTENSIONS: [&str; 13] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus", "mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
]; ];
fn get_cache_file_for(path: &Path) -> PathBuf {
let mut hasher = DefaultHasher::new();
path.hash(&mut hasher);
let hash = hasher.finish();
dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.join("pwsp")
.join(format!("{:x}.json", hash))
}
struct SoundpadGui { struct SoundpadGui {
pub app_state: AppState, pub app_state: AppState,
pub config: GuiConfig, pub config: GuiConfig,
@@ -107,19 +119,119 @@ impl SoundpadGui {
} }
} }
pub fn open_dir(&mut self, path: &PathBuf) { pub fn open_dir(&mut self, path: &PathBuf, ctx: Option<Context>, force_rescan: bool) {
self.app_state.current_dir = Some(path.clone()); self.app_state.current_dir = Some(path.clone());
match path.read_dir() {
Ok(read_dir) => { if !self.app_state.dir_cache.contains_key(path) || force_rescan {
self.app_state.listed_files = read_dir match fs::read_dir(path) {
.filter_map(|res| res.ok()) Ok(read_dir) => {
.map(|entry| entry.path()) let files = read_dir
.collect(); .filter_map(|res| res.ok())
} .map(|entry| entry.path())
Err(e) => { .collect::<Vec<_>>();
eprintln!("Failed to read directory {:?}: {}", path, e); self.app_state.listed_files = files.iter().cloned().collect();
self.app_state.listed_files.clear(); self.app_state.dir_cache.insert(path.clone(), files);
}
Err(e) => {
eprintln!("Failed to read directory {:?}: {}", path, e);
self.app_state.listed_files.clear();
}
} }
} else {
self.app_state.listed_files = self
.app_state
.dir_cache
.get(path)
.unwrap()
.clone()
.into_iter()
.collect();
}
let is_scanning = self.app_state.scanning_dirs.lock().unwrap().contains(path);
let is_cached = self.app_state.recursive_files_cache.contains_key(path);
let has_scanned_this_session = self.app_state.scanned_this_session.contains(path);
let needs_scan = force_rescan || !has_scanned_this_session || !is_cached;
if !is_scanning && needs_scan {
self.app_state
.scanning_dirs
.lock()
.unwrap()
.insert(path.clone());
self.app_state.scanned_this_session.insert(path.clone());
let finished_scans = self.app_state.finished_scans.clone();
let scanning_dirs = self.app_state.scanning_dirs.clone();
let path_clone = path.clone();
thread::spawn(move || {
let cache_file = get_cache_file_for(&path_clone);
// 1. Try to load from disk cache if we don't have it in memory yet
if !is_cached
&& let Ok(data) = fs::read_to_string(&cache_file)
&& let Ok((all_files, dir_updates)) = serde_json::from_str::<(
Vec<PathBuf>,
HashMap<PathBuf, Vec<PathBuf>>,
)>(&data)
{
finished_scans.lock().unwrap().push((
path_clone.clone(),
all_files,
dir_updates,
));
if let Some(ctx) = ctx.as_ref() {
ctx.request_repaint();
}
}
// 2. Scan recursively
let mut all_files = Vec::new();
let mut dir_updates = HashMap::new();
let mut dirs_to_visit = vec![path_clone.clone()];
while let Some(dir) = dirs_to_visit.pop() {
if let Ok(entries) = fs::read_dir(&dir) {
let mut children = Vec::new();
for entry in entries.filter_map(|e| e.ok()) {
let p = entry.path();
if p.is_dir() {
dirs_to_visit.push(p.clone());
children.push(p);
} else if crate::gui::SUPPORTED_EXTENSIONS.contains(
&p.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
all_files.push(p.clone());
children.push(p);
}
}
dir_updates.insert(dir, children);
}
}
// 3. Save to disk cache
if let Some(parent) = cache_file.parent() {
fs::create_dir_all(parent).ok();
}
if let Ok(json) = serde_json::to_string(&(all_files.clone(), dir_updates.clone())) {
fs::write(&cache_file, json).ok();
}
// 4. Send to UI
finished_scans
.lock()
.unwrap()
.push((path_clone.clone(), all_files, dir_updates));
scanning_dirs.lock().unwrap().remove(&path_clone);
if let Some(ctx) = ctx {
ctx.request_repaint();
}
});
} }
} }
@@ -130,10 +242,11 @@ impl SoundpadGui {
pub fn set_input(&mut self, name: String) { pub fn set_input(&mut self, name: String) {
make_request_async(Request::set_input(&name)); make_request_async(Request::set_input(&name));
if self.config.save_input { if self.config.save_input
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_input_name = Some(name); daemon_config.default_input_name = Some(name);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
} }
} }
@@ -157,7 +270,14 @@ impl SoundpadGui {
make_request_async(Request::play_hotkey(slot)); make_request_async(Request::play_hotkey(slot));
} }
pub fn get_filtered_files(&self) -> Vec<PathBuf> { pub fn get_filtered_files(
&self,
matching_dirs: &HashSet<PathBuf>,
matching_files: &HashSet<PathBuf>,
) -> Vec<PathBuf> {
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect(); let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
let sort_order = self let sort_order = self
.app_state .app_state
@@ -178,41 +298,51 @@ impl SoundpadGui {
} }
}); });
let search_query = self.app_state.search_query.to_lowercase(); let is_cached = self
let search_query = search_query.trim(); .app_state
.current_dir
.as_ref()
.is_some_and(|d| self.app_state.recursive_files_cache.contains_key(d));
files if !search_query.is_empty() && is_cached {
.into_iter() files
.filter(|entry_path| { .into_iter()
if entry_path.is_dir() { .filter(|p| {
return true; if p.is_dir() {
} matching_dirs.contains(p)
} else {
if !SUPPORTED_EXTENSIONS.contains( matching_files.contains(p)
&entry_path }
.extension() })
.unwrap_or_default() .collect()
.to_str() } else {
.unwrap_or_default(), files
) { .into_iter()
return false; .filter(|entry_path| {
} if !entry_path.is_dir()
&& !SUPPORTED_EXTENSIONS.contains(
if !search_query.is_empty() { &entry_path
let file_name = entry_path .extension()
.file_name() .unwrap_or_default()
.unwrap_or_default() .to_str()
.to_string_lossy() .unwrap_or_default(),
.to_string(); )
{
if !file_name.to_lowercase().contains(search_query) {
return false; return false;
} }
}
true if !search_query.is_empty() {
}) let file_name =
.collect() entry_path.file_name().unwrap_or_default().to_string_lossy();
if !file_name.to_lowercase().contains(search_query) {
return false;
}
}
true
})
.collect()
}
} }
} }
@@ -329,14 +459,16 @@ mod tests {
// On the real OS filesystem, these paths don't exist, so they are treated as files. // On the real OS filesystem, these paths don't exist, so they are treated as files.
// Unsupported extensions (like .txt) will be filtered out. // Unsupported extensions (like .txt) will be filtered out.
// So we expect only file_b and file_c, sorted alphabetically. // So we expect only file_b and file_c, sorted alphabetically.
let filtered = gui.get_filtered_files(); let empty_dirs = HashSet::new();
let empty_files = HashSet::new();
let filtered = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered.len(), 2); assert_eq!(filtered.len(), 2);
assert_eq!(filtered[0], file_b); assert_eq!(filtered[0], file_b);
assert_eq!(filtered[1], file_c); assert_eq!(filtered[1], file_c);
// Test search query // Test search query
gui.app_state.search_query = "c_fi".to_string(); gui.app_state.search_query = "c_fi".to_string();
let filtered_search = gui.get_filtered_files(); let filtered_search = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered_search.len(), 1); assert_eq!(filtered_search.len(), 1);
assert_eq!(filtered_search[0], file_c); assert_eq!(filtered_search[0], file_c);
@@ -349,7 +481,7 @@ mod tests {
}, },
); );
gui.app_state.search_query = String::new(); gui.app_state.search_query = String::new();
let filtered_desc = gui.get_filtered_files(); let filtered_desc = gui.get_filtered_files(&empty_dirs, &empty_files);
assert_eq!(filtered_desc.len(), 2); assert_eq!(filtered_desc.len(), 2);
assert_eq!(filtered_desc[0], file_c); assert_eq!(filtered_desc[0], file_c);
assert_eq!(filtered_desc[1], file_b); assert_eq!(filtered_desc[1], file_b);
+23 -4
View File
@@ -3,7 +3,7 @@ use eframe::{App, Frame as EFrame};
use egui::{CentralPanel, Context, ThemePreference}; use egui::{CentralPanel, Context, ThemePreference};
use pwsp_lib::{ use pwsp_lib::{
types::{config::PreferredTheme, socket::Request}, types::{config::PreferredTheme, socket::Request},
utils::{daemon::get_daemon_config, gui::make_request_async}, utils::gui::{get_daemon_config, make_request_async, update_daemon_config},
}; };
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@@ -85,10 +85,29 @@ impl App for SoundpadGui {
self.app_state.ignore_volume_update_until = self.app_state.ignore_volume_update_until =
Some(Instant::now() + Duration::from_millis(300)); Some(Instant::now() + Duration::from_millis(300));
if self.config.save_volume { if self.config.save_volume
let mut daemon_config = get_daemon_config(); && let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_volume = Some(self.app_state.volume_slider_value); daemon_config.default_volume = Some(self.app_state.volume_slider_value);
daemon_config.save_to_file().ok(); update_daemon_config(&daemon_config).ok();
}
}
if self.app_state.volume_multiplier_dragged {
make_request_async(Request::set_volume_multiplier(
self.app_state.volume_multiplier_slider_value,
));
self.app_state.volume_multiplier_dragged = false;
self.app_state.ignore_volume_multiplier_update_until =
Some(Instant::now() + Duration::from_millis(300));
if self.config.save_volume_multiplier
&& let Ok(mut daemon_config) = get_daemon_config()
{
daemon_config.default_volume_multiplier =
Some(self.app_state.volume_multiplier_slider_value);
update_daemon_config(&daemon_config).ok();
} }
} }
+224 -58
View File
@@ -10,7 +10,7 @@ use pwsp_lib::types::{
gui::{AppState, AudioPlayerState}, gui::{AppState, AudioPlayerState},
}; };
use rust_i18n::t; use rust_i18n::t;
use std::{cmp::Ordering, path::Path, path::PathBuf}; use std::{cmp::Ordering, collections::HashSet, path::Path, path::PathBuf};
pub(crate) enum FileAction { pub(crate) enum FileAction {
Play(PathBuf, bool), Play(PathBuf, bool),
@@ -195,7 +195,7 @@ impl SoundpadGui {
.sort_order = order; .sort_order = order;
self.config.save_to_file().ok(); self.config.save_to_file().ok();
self.app_state.dir_cache.remove(path); self.app_state.dir_cache.remove(path);
self.open_dir(path); self.open_dir(path, Some(ui.ctx().clone()), false);
} }
}); });
}); });
@@ -203,7 +203,7 @@ impl SoundpadGui {
self.app_state.dirs = dirs; self.app_state.dirs = dirs;
if let Some(path) = dir_to_open { if let Some(path) = dir_to_open {
self.open_dir(&path); self.open_dir(&path, Some(ui.ctx().clone()), false);
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
@@ -227,12 +227,41 @@ impl SoundpadGui {
fn draw_files_search_field(&mut self, ui: &mut Ui) { fn draw_files_search_field(&mut self, ui: &mut Ui) {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let is_scanning = self
.app_state
.current_dir
.as_ref()
.is_some_and(|d| self.app_state.scanning_dirs.lock().unwrap().contains(d));
let spinner_width = if is_scanning { 20.0 } else { 0.0 };
let refresh_button = Button::new(ICON_REFRESH).frame(false);
let refresh_width = 24.0;
let search_field_response = ui.add_sized( let search_field_response = ui.add_sized(
[ui.available_width(), 22.0], [
f32::max(
0.0,
ui.available_width() - spinner_width - refresh_width - 8.0,
),
22.0,
],
TextEdit::singleline(&mut self.app_state.search_query) TextEdit::singleline(&mut self.app_state.search_query)
.hint_text(t!("gui.search_placeholder")), .hint_text(t!("gui.search_placeholder")),
); );
if is_scanning {
ui.spinner();
}
let refresh_response = ui
.add_sized([refresh_width, 22.0], refresh_button)
.on_hover_text("Refresh");
if refresh_response.clicked()
&& let Some(dir) = self.app_state.current_dir.clone()
{
self.open_dir(&dir, Some(ui.ctx().clone()), true);
}
if self.app_state.force_focus_search { if self.app_state.force_focus_search {
search_field_response.request_focus(); search_field_response.request_focus();
self.app_state.force_focus_search = false; self.app_state.force_focus_search = false;
@@ -247,9 +276,54 @@ impl SoundpadGui {
ui.set_min_width(area_size.x); ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
let finished = {
let mut scans = self.app_state.finished_scans.lock().unwrap();
std::mem::take(&mut *scans)
};
for (top_dir, all_files, dir_updates) in finished {
self.app_state
.recursive_files_cache
.insert(top_dir, all_files);
self.app_state.dir_cache.extend(dir_updates);
}
ui.vertical(|ui| { ui.vertical(|ui| {
let mut actions = Vec::new(); let mut actions = Vec::new();
let files = self.get_filtered_files();
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
let mut matching_dirs = std::collections::HashSet::new();
let mut matching_files = std::collections::HashSet::new();
if !search_query.is_empty()
&& let Some(current_dir) = &self.app_state.current_dir
&& let Some(all_files) = self.app_state.recursive_files_cache.get(current_dir)
{
for file in all_files {
if let Ok(rel_path) = file.strip_prefix(current_dir) {
let rel_path_str =
rel_path.to_string_lossy().to_lowercase().replace("\\", "/");
if rel_path_str.contains(search_query) {
matching_files.insert(file.clone());
let mut p = file.parent();
while let Some(parent) = p {
if parent == current_dir {
break;
}
matching_dirs.insert(parent.to_path_buf());
p = parent.parent();
}
}
}
}
}
let max_results = 300;
let is_truncated = !search_query.is_empty() && matching_files.len() > max_results;
let files = self.get_filtered_files(&matching_dirs, &matching_files);
let mut rendered_count = 0;
for entry_path in files { for entry_path in files {
Self::draw_tree_node( Self::draw_tree_node(
ui, ui,
@@ -258,6 +332,22 @@ impl SoundpadGui {
&mut self.app_state, &mut self.app_state,
&self.audio_player_state, &self.audio_player_state,
&mut actions, &mut actions,
&matching_dirs,
&matching_files,
search_query,
&mut rendered_count,
max_results,
);
}
if is_truncated {
ui.add_space(8.0);
let text = t!(
"gui.search_truncated_warning",
count = matching_files.len() - max_results
);
ui.label(
egui::RichText::new(text).color(egui::Color32::from_rgb(200, 150, 50)),
); );
} }
@@ -286,6 +376,7 @@ impl SoundpadGui {
}); });
} }
#[allow(clippy::too_many_arguments)]
fn draw_tree_node_dir( fn draw_tree_node_dir(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
@@ -293,69 +384,112 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
matching_dirs: &std::collections::HashSet<PathBuf>,
matching_files: &std::collections::HashSet<PathBuf>,
search_query: &str,
rendered_count: &mut usize,
max_results: usize,
) { ) {
if *rendered_count >= max_results && !search_query.is_empty() {
return;
}
let dir_name = path let dir_name = path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
CollapsingHeader::new(dir_name)
.id_salt(&path)
.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone()
} else {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
let child_path = entry.path();
if !child_path.is_dir()
&& !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
{
continue;
}
read.push(child_path);
}
}
let sort_order = config.get_sort_order(&path);
read.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
sort_order.compare(a, b)
}
});
app_state.dir_cache.insert(path.clone(), read.clone());
read
};
let search_query = app_state.search_query.to_lowercase(); let id_salt = if search_query.is_empty() {
let search_query = search_query.trim(); format!("normal_{:?}", path)
} else {
format!("search_{:?}", path)
};
for child in children { let mut header = CollapsingHeader::new(dir_name).id_salt(id_salt);
if !child.is_dir() && !search_query.is_empty() {
let file_name = child let is_cached = app_state
.file_name() .current_dir
.unwrap_or_default() .as_ref()
.to_string_lossy() .is_some_and(|d| app_state.recursive_files_cache.contains_key(d));
.to_string(); if !search_query.is_empty() && is_cached {
if !file_name.to_lowercase().contains(search_query) { header = header.default_open(true);
}
header.show(ui, |ui| {
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
cached.clone()
} else {
let mut read = Vec::new();
if let Ok(entries) = std::fs::read_dir(&path) {
for entry in entries.filter_map(|e| e.ok()) {
let child_path = entry.path();
if !child_path.is_dir()
&& !crate::gui::SUPPORTED_EXTENSIONS.contains(
&child_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
)
{
continue; continue;
} }
read.push(child_path);
} }
Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions);
} }
}); let sort_order = config.get_sort_order(&path);
read.sort_by(|a, b| {
let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir();
if a_is_dir && !b_is_dir {
Ordering::Less
} else if !a_is_dir && b_is_dir {
Ordering::Greater
} else {
sort_order.compare(a, b)
}
});
app_state.dir_cache.insert(path.clone(), read.clone());
read
};
for child in children {
if *rendered_count >= max_results && !search_query.is_empty() {
break;
}
if !search_query.is_empty() && is_cached {
if child.is_dir() && !matching_dirs.contains(&child) {
continue;
}
if !child.is_dir() && !matching_files.contains(&child) {
continue;
}
} else if !search_query.is_empty() && !child.is_dir() {
let file_name = child
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_lowercase();
if !file_name.contains(search_query) {
continue;
}
}
Self::draw_tree_node(
ui,
child,
config,
app_state,
audio_player_state,
actions,
matching_dirs,
matching_files,
search_query,
rendered_count,
max_results,
);
}
});
} }
fn draw_tree_node_file( fn draw_tree_node_file(
@@ -364,7 +498,10 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
rendered_count: &mut usize,
) { ) {
*rendered_count += 1;
let file_name = path let file_name = path
.file_name() .file_name()
.unwrap_or_default() .unwrap_or_default()
@@ -500,6 +637,7 @@ impl SoundpadGui {
}); });
} }
#[allow(clippy::too_many_arguments)]
fn draw_tree_node( fn draw_tree_node(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
@@ -507,11 +645,39 @@ impl SoundpadGui {
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
matching_dirs: &HashSet<PathBuf>,
matching_files: &HashSet<PathBuf>,
search_query: &str,
rendered_count: &mut usize,
max_results: usize,
) { ) {
if *rendered_count >= max_results && !search_query.is_empty() {
return;
}
if path.is_dir() { if path.is_dir() {
Self::draw_tree_node_dir(ui, path, config, app_state, audio_player_state, actions); Self::draw_tree_node_dir(
ui,
path,
config,
app_state,
audio_player_state,
actions,
matching_dirs,
matching_files,
search_query,
rendered_count,
max_results,
);
} else { } else {
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions); Self::draw_tree_node_file(
ui,
path,
app_state,
audio_player_state,
actions,
rendered_count,
);
} }
} }
} }
+39 -1
View File
@@ -1,5 +1,5 @@
use crate::gui::SoundpadGui; use crate::gui::SoundpadGui;
use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Ui}; use egui::{Align, Button, Color32, ComboBox, Layout, RichText, Slider, Ui};
use egui_material_icons::icons::ICON_ARROW_BACK; use egui_material_icons::icons::ICON_ARROW_BACK;
use pwsp_lib::types::config::PreferredTheme; use pwsp_lib::types::config::PreferredTheme;
use rust_i18n::t; use rust_i18n::t;
@@ -34,6 +34,10 @@ impl SoundpadGui {
&mut self.config.save_volume, &mut self.config.save_volume,
t!("gui.settings.remember_volume"), t!("gui.settings.remember_volume"),
); );
let save_volume_multiplier_response = ui.checkbox(
&mut self.config.save_volume_multiplier,
t!("gui.settings.remember_volume_multiplier"),
);
let save_input_response = let save_input_response =
ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic")); ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
let save_scale_response = ui.checkbox( let save_scale_response = ui.checkbox(
@@ -46,6 +50,7 @@ impl SoundpadGui {
); );
if save_volume_response.changed() if save_volume_response.changed()
|| save_volume_multiplier_response.changed()
|| save_input_response.changed() || save_input_response.changed()
|| save_scale_response.changed() || save_scale_response.changed()
|| pause_on_exit_response.changed() || pause_on_exit_response.changed()
@@ -88,6 +93,39 @@ impl SoundpadGui {
} }
// -------------------------------- // --------------------------------
ui.separator();
// ----------- Sliders ------------
// Volume multiplier
let should_update_multiplier = !self.app_state.volume_multiplier_dragged
&& self
.app_state
.ignore_volume_multiplier_update_until
.map(|t| std::time::Instant::now() > t)
.unwrap_or(true);
if should_update_multiplier {
self.app_state.volume_multiplier_slider_value =
self.audio_player_state.volume_multiplier;
}
ui.horizontal(|ui| {
let slider = Slider::new(
&mut self.app_state.volume_multiplier_slider_value,
0.01..=3.0,
);
let response = ui.add(slider);
ui.label(t!("gui.settings.volume_multiplier"));
if response.changed() {
// This condition is required to avoid spamming requests while dragging, but to allow changing the value via TextEdit
if !response.dragged() || (response.dragged() && response.drag_stopped()) {
self.app_state.volume_multiplier_dragged = true;
}
}
});
// --------------------------------
ui.with_layout(Layout::bottom_up(Align::Min), |ui| { ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
ui.label(t!( ui.label(t!(
"gui.settings.version", "gui.settings.version",
+25 -9
View File
@@ -1,9 +1,13 @@
mod gui; mod gui;
use anyhow::{Context, Result}; use anyhow::{Context, Result, anyhow};
use pwsp_lib::utils::gui::ensure_pwsp_audio_dir; use pwsp_lib::utils::gui::ensure_pwsp_audio_dir;
use rust_i18n::i18n; use rust_i18n::i18n;
use std::{env, path::PathBuf}; use std::{
env,
path::{Path, PathBuf},
time::{SystemTime, UNIX_EPOCH},
};
i18n!("locales", fallback = "en"); i18n!("locales", fallback = "en");
@@ -31,19 +35,31 @@ async fn download_audio_from_url(uri: &str) -> Result<PathBuf> {
let target_url = uri let target_url = uri
.strip_prefix(prefix) .strip_prefix(prefix)
.ok_or_else(|| anyhow::anyhow!("URI does not containt an expected prefix: {}", prefix))?; .ok_or_else(|| anyhow!("URI does not containt an expected prefix: {}", prefix))?;
let file_name_encoded = target_url let file_name_encoded = match target_url.split('/').next_back() {
.split('/') Some(path) => path.to_string(),
.next_back() None => {
.unwrap_or("downloaded_audio.mp3"); let id = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went back")
.as_nanos();
format!("downloaded_audio_{}.mp3", id)
}
};
let file_name = percent_encoding::percent_decode_str(file_name_encoded) let file_name = percent_encoding::percent_decode_str(&file_name_encoded.clone())
.decode_utf8() .decode_utf8()
.unwrap_or_else(|_| file_name_encoded.into()) .unwrap_or_else(|_| file_name_encoded.into())
.into_owned(); .into_owned();
let save_path = ensure_pwsp_audio_dir().join(file_name); let normalized_file_name = file_name.replace('\\', "/");
let sanitized_file_name = Path::new(&normalized_file_name)
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("downloaded_audio.mp3");
let save_path = ensure_pwsp_audio_dir()?.join(sanitized_file_name);
let response = reqwest::get(target_url) let response = reqwest::get(target_url)
.await? .await?
+20 -7
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::pipewire::DeviceType, types::pipewire::DeviceType,
utils::{ utils::{
daemon::get_daemon_config, daemon::with_daemon_config,
pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic}, pipewire::{PwTerminator, create_link, get_device, link_player_to_virtual_mic},
}, },
}; };
@@ -40,6 +40,7 @@ pub struct FullState {
pub state: PlayerState, pub state: PlayerState,
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, pub volume: f32,
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
} }
@@ -63,12 +64,19 @@ pub struct AudioPlayer {
pub input_device_name: Option<String>, pub input_device_name: Option<String>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
} }
impl AudioPlayer { impl AudioPlayer {
pub async fn new() -> Result<Self> { pub async fn new() -> Result<Self> {
let daemon_config = get_daemon_config(); let (default_input_name, default_volume, default_volume_multiplier) =
let default_volume = daemon_config.default_volume.unwrap_or(1.0); with_daemon_config(|c| {
(
c.default_input_name.clone(),
c.default_volume.unwrap_or(1.0),
c.default_volume_multiplier.unwrap_or(1.0),
)
});
let mut audio_player = AudioPlayer { let mut audio_player = AudioPlayer {
stream_handle: None, stream_handle: None,
@@ -77,9 +85,10 @@ impl AudioPlayer {
input_link_sender: None, input_link_sender: None,
player_link_sender: None, player_link_sender: None,
input_device_name: daemon_config.default_input_name.clone(), input_device_name: default_input_name,
volume: default_volume, volume: default_volume,
volume_multiplier: default_volume_multiplier,
}; };
if audio_player.input_device_name.is_some() { if audio_player.input_device_name.is_some() {
@@ -262,12 +271,16 @@ impl AudioPlayer {
if let Some(id) = id { if let Some(id) = id {
if let Some(sound) = self.tracks.get_mut(&id) { if let Some(sound) = self.tracks.get_mut(&id) {
sound.volume = volume; sound.volume = volume;
sound.sink.set_volume(self.volume * volume); sound
.sink
.set_volume(self.volume * sound.volume * self.volume_multiplier);
} }
} else { } else {
self.volume = volume; self.volume = volume;
for sound in self.tracks.values_mut() { for sound in self.tracks.values_mut() {
sound.sink.set_volume(self.volume * sound.volume); sound
.sink
.set_volume(self.volume * sound.volume * self.volume_multiplier);
} }
} }
} }
@@ -347,7 +360,7 @@ impl AudioPlayer {
.ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))? .ok_or_else(|| anyhow::anyhow!("stream_handle is unexpectedly missing"))?
.mixer(); .mixer();
let sink = Player::connect_new(mixer); let sink = Player::connect_new(mixer);
sink.set_volume(self.volume); // Default volume is 1.0 * master sink.set_volume(self.volume * self.volume_multiplier); // Default volume is 1.0 * master
sink.append(source); sink.append(source);
sink.play(); sink.play();
+82 -2
View File
@@ -1,12 +1,12 @@
use crate::{ use crate::{
types::{ types::{
audio_player::{FullState, PlayerState}, audio_player::{FullState, PlayerState},
config::HotkeyConfig, config::{DaemonConfig, HotkeyConfig},
socket::{Request, Response}, socket::{Request, Response},
}, },
utils::{ utils::{
commands::parse_command, commands::parse_command,
daemon::get_audio_player, daemon::{get_audio_player, with_daemon_config},
pipewire::{get_all_devices, get_device}, pipewire::{get_all_devices, get_device},
}, },
}; };
@@ -46,11 +46,17 @@ pub struct GetVolumeCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct GetVolumeMultiplierCommand {}
pub struct SetVolumeCommand { pub struct SetVolumeCommand {
pub volume: Option<f32>, pub volume: Option<f32>,
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct SetVolumeMultiplierCommand {
pub volume_multiplier: Option<f32>,
}
pub struct GetPositionCommand { pub struct GetPositionCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
@@ -127,6 +133,14 @@ pub struct ClearHotkeyKeyCommand {
pub slot: Option<String>, pub slot: Option<String>,
} }
pub struct GetDaemonConfigCommand {}
pub struct SaveDaemonConfigCommand {}
pub struct UpdateDaemonConfigCommand {
pub new_config: DaemonConfig,
}
#[async_trait] #[async_trait]
impl Executable for PingCommand { impl Executable for PingCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -262,6 +276,18 @@ impl Executable for GetVolumeCommand {
} }
} }
#[async_trait]
impl Executable for GetVolumeMultiplierCommand {
async fn execute(&self) -> Response {
let audio_player = match get_audio_player().await {
Ok(player) => player.lock().await,
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
};
Response::new(true, audio_player.volume_multiplier.to_string())
}
}
#[async_trait] #[async_trait]
impl Executable for SetVolumeCommand { impl Executable for SetVolumeCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -278,6 +304,26 @@ impl Executable for SetVolumeCommand {
} }
} }
#[async_trait]
impl Executable for SetVolumeMultiplierCommand {
async fn execute(&self) -> Response {
if let Some(volume_multiplier) = self.volume_multiplier {
let mut audio_player = match get_audio_player().await {
Ok(player) => player.lock().await,
Err(err) => return Response::new(false, format!("Audio player error: {}", err)),
};
audio_player.volume_multiplier = volume_multiplier;
audio_player.set_volume(volume_multiplier, None); // Reset current volume for all tracks to apply multiplier
Response::new(
true,
format!("Audio volume multiplier was set to {}", volume_multiplier),
)
} else {
Response::new(false, "Invalid volume multiplier value")
}
}
}
#[async_trait] #[async_trait]
impl Executable for GetPositionCommand { impl Executable for GetPositionCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -508,6 +554,7 @@ impl Executable for GetFullStateCommand {
state: audio_player.get_state(), state: audio_player.get_state(),
tracks: audio_player.get_tracks(), tracks: audio_player.get_tracks(),
volume: audio_player.volume, volume: audio_player.volume,
volume_multiplier: audio_player.volume_multiplier,
current_input: current_input_nick, current_input: current_input_nick,
all_inputs, all_inputs,
}; };
@@ -723,3 +770,36 @@ impl Executable for ClearHotkeyKeyCommand {
} }
} }
} }
#[async_trait]
impl Executable for GetDaemonConfigCommand {
async fn execute(&self) -> Response {
let serialized = with_daemon_config(|c| serde_json::to_string(&c));
match serialized {
Ok(s) => Response::new(true, s),
Err(err) => Response::new(false, format!("Failed to serialize daemon config: {}", err)),
}
}
}
#[async_trait]
impl Executable for SaveDaemonConfigCommand {
async fn execute(&self) -> Response {
match with_daemon_config(|c| c.save_to_file()) {
Ok(_) => Response::new(true, "Daemon config saved successfully"),
Err(err) => Response::new(false, format!("Failed to save daemon config: {}", err)),
}
}
}
#[async_trait]
impl Executable for UpdateDaemonConfigCommand {
async fn execute(&self) -> Response {
with_daemon_config(|c| {
c.clone_from(&self.new_config);
});
Response::new(true, "Daemon config updated successfully")
}
}
+4 -1
View File
@@ -17,6 +17,7 @@ use std::{
pub struct DaemonConfig { pub struct DaemonConfig {
pub default_input_name: Option<String>, pub default_input_name: Option<String>,
pub default_volume: Option<f32>, pub default_volume: Option<f32>,
pub default_volume_multiplier: Option<f32>,
} }
impl DaemonConfig { impl DaemonConfig {
@@ -73,6 +74,7 @@ pub struct GuiConfig {
pub left_panel_width: f32, pub left_panel_width: f32,
pub save_volume: bool, pub save_volume: bool,
pub save_volume_multiplier: bool,
pub save_input: bool, pub save_input: bool,
pub save_scale_factor: bool, pub save_scale_factor: bool,
pub pause_on_exit: bool, pub pause_on_exit: bool,
@@ -117,11 +119,12 @@ impl Default for GuiConfig {
left_panel_width: 280.0, left_panel_width: 280.0,
save_volume: false, save_volume: false,
save_volume_multiplier: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false, pause_on_exit: false,
dirs: vec![ensure_pwsp_audio_dir()], dirs: vec![ensure_pwsp_audio_dir().unwrap()],
preferred_theme: PreferredTheme::System, preferred_theme: PreferredTheme::System,
dirs_settings: HashMap::new(), dirs_settings: HashMap::new(),
+12
View File
@@ -8,9 +8,12 @@ use egui::Id;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex},
time::Instant, time::Instant,
}; };
pub type ScanResult = (PathBuf, Vec<PathBuf>, HashMap<PathBuf, Vec<PathBuf>>);
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct TrackUiState { pub struct TrackUiState {
pub position_slider_value: f32, pub position_slider_value: f32,
@@ -31,13 +34,16 @@ pub struct AppState {
pub show_settings: bool, pub show_settings: bool,
pub volume_dragged: bool, pub volume_dragged: bool,
pub volume_multiplier_dragged: bool,
pub force_focus_search: bool, pub force_focus_search: bool,
pub volume_slider_value: f32, pub volume_slider_value: f32,
pub volume_multiplier_slider_value: f32,
pub search_field_id: Option<Id>, pub search_field_id: Option<Id>,
pub ignore_volume_update_until: Option<Instant>, pub ignore_volume_update_until: Option<Instant>,
pub ignore_volume_multiplier_update_until: Option<Instant>,
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
@@ -47,6 +53,11 @@ pub struct AppState {
pub listed_dirs: HashSet<PathBuf>, pub listed_dirs: HashSet<PathBuf>,
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>, pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
pub scanning_dirs: Arc<Mutex<HashSet<PathBuf>>>,
pub scanned_this_session: HashSet<PathBuf>,
pub finished_scans: Arc<Mutex<Vec<ScanResult>>>,
pub recursive_files_cache: HashMap<PathBuf, Vec<PathBuf>>,
pub show_hotkeys: bool, pub show_hotkeys: bool,
pub hotkey_capture_active: bool, pub hotkey_capture_active: bool,
@@ -65,6 +76,7 @@ pub struct AudioPlayerState {
pub tracks: Vec<TrackInfo>, pub tracks: Vec<TrackInfo>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
pub volume_multiplier: f32,
pub current_input: String, pub current_input: String,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<String, String>,
+30
View File
@@ -1,3 +1,4 @@
use crate::types::config::DaemonConfig;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
@@ -94,6 +95,10 @@ impl Request {
Request::new("get_volume", args) Request::new("get_volume", args)
} }
pub fn get_volume_multiplier() -> Self {
Request::new("get_volume_multiplier", vec![])
}
pub fn get_position(id: Option<u32>) -> Self { pub fn get_position(id: Option<u32>) -> Self {
let mut args = vec![]; let mut args = vec![];
let id_str; let id_str;
@@ -138,6 +143,13 @@ impl Request {
Request::new("set_volume".to_string(), args) Request::new("set_volume".to_string(), args)
} }
pub fn set_volume_multiplier(volume: f32) -> Self {
Request::new(
"set_volume_multiplier",
vec![("volume_multiplier", &volume.to_string())],
)
}
pub fn seek(position: f32, id: Option<u32>) -> Self { pub fn seek(position: f32, id: Option<u32>) -> Self {
let mut args = vec![("position".to_string(), position.to_string())]; let mut args = vec![("position".to_string(), position.to_string())];
if let Some(id) = id { if let Some(id) = id {
@@ -172,6 +184,24 @@ impl Request {
Request::new("get_daemon_version", vec![]) Request::new("get_daemon_version", vec![])
} }
pub fn get_daemon_config() -> Self {
Request::new("get_daemon_config", vec![])
}
pub fn save_daemon_config() -> Self {
Request::new("save_daemon_config", vec![])
}
pub fn update_daemon_config(new_config: &DaemonConfig) -> Self {
Request::new(
"update_daemon_config",
vec![(
"new_config",
&serde_json::to_string(&new_config).unwrap_or_default(),
)],
)
}
pub fn get_full_state() -> Self { pub fn get_full_state() -> Self {
Request::new("get_full_state", vec![]) Request::new("get_full_state", vec![])
} }
+22 -1
View File
@@ -1,4 +1,4 @@
use crate::types::{commands::*, socket::Request}; use crate::types::{commands::*, config::DaemonConfig, socket::Request};
use std::path::PathBuf; use std::path::PathBuf;
@@ -15,6 +15,7 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
"is_paused" => Some(Box::new(IsPausedCommand {})), "is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})), "get_state" => Some(Box::new(GetStateCommand {})),
"get_volume" => Some(Box::new(GetVolumeCommand { id })), "get_volume" => Some(Box::new(GetVolumeCommand { id })),
"get_volume_multiplier" => Some(Box::new(GetVolumeMultiplierCommand {})),
"set_volume" => { "set_volume" => {
let volume = request let volume = request
.args .args
@@ -24,6 +25,15 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
.ok(); .ok();
Some(Box::new(SetVolumeCommand { volume, id })) Some(Box::new(SetVolumeCommand { volume, id }))
} }
"set_volume_multiplier" => {
let volume_multiplier = request
.args
.get("volume_multiplier")
.unwrap_or(&String::new())
.parse::<f32>()
.ok();
Some(Box::new(SetVolumeMultiplierCommand { volume_multiplier }))
}
"get_position" => Some(Box::new(GetPositionCommand { id })), "get_position" => Some(Box::new(GetPositionCommand { id })),
"seek" => { "seek" => {
let position = request let position = request
@@ -119,6 +129,17 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
key_chord, key_chord,
})) }))
} }
"get_daemon_config" => Some(Box::new(GetDaemonConfigCommand {})),
"save_daemon_config" => Some(Box::new(SaveDaemonConfigCommand {})),
"update_daemon_config" => {
let new_config = request
.args
.get("new_config")
.and_then(|nc| serde_json::from_str::<DaemonConfig>(nc).ok())
.unwrap_or_default();
Some(Box::new(UpdateDaemonConfigCommand { new_config }))
}
_ => None, _ => None,
} }
} }
+2 -2
View File
@@ -1,7 +1,7 @@
use anyhow::Result; use anyhow::{Context, Result};
use std::path::PathBuf; use std::path::PathBuf;
pub fn get_config_path() -> Result<PathBuf> { pub fn get_config_path() -> Result<PathBuf> {
let config_path = dirs::config_dir().expect("Failed to obtain config dir"); let config_path = dirs::config_dir().context("Failed to obtain config dir")?;
Ok(config_path.join("pwsp")) Ok(config_path.join("pwsp"))
} }
+26 -10
View File
@@ -7,36 +7,52 @@ use crate::types::{
use anyhow::Result; use anyhow::Result;
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt}; use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
use std::path::PathBuf; use std::path::PathBuf;
use std::{env, error::Error, fs}; use std::{
env,
error::Error,
fs,
sync::{Arc, Mutex, OnceLock},
};
use tokio::{ use tokio::{
io::{AsyncReadExt, AsyncWriteExt}, io::{AsyncReadExt, AsyncWriteExt},
net::UnixStream, net::UnixStream,
sync::{Mutex, OnceCell}, sync::{Mutex as AsyncMutex, OnceCell},
time::{Duration, sleep}, time::{Duration, sleep},
}; };
static AUDIO_PLAYER: OnceCell<Mutex<AudioPlayer>> = OnceCell::const_new(); static AUDIO_PLAYER: OnceCell<AsyncMutex<AudioPlayer>> = OnceCell::const_new();
static DAEMON_CONFIG: OnceLock<Arc<Mutex<DaemonConfig>>> = OnceLock::new();
pub async fn get_audio_player() -> Result<&'static Mutex<AudioPlayer>, String> { pub async fn get_audio_player() -> Result<&'static AsyncMutex<AudioPlayer>, String> {
AUDIO_PLAYER AUDIO_PLAYER
.get_or_try_init(|| async { .get_or_try_init(|| async {
println!("Initializing audio player"); println!("Initializing audio player");
match AudioPlayer::new().await { match AudioPlayer::new().await {
Ok(player) => Ok(Mutex::new(player)), Ok(player) => Ok(AsyncMutex::new(player)),
Err(err) => Err(err.to_string()), Err(err) => Err(err.to_string()),
} }
}) })
.await .await
} }
pub fn get_daemon_config() -> DaemonConfig { pub fn get_daemon_config() -> &'static Arc<Mutex<DaemonConfig>> {
DaemonConfig::load_from_file().unwrap_or_else(|_| { DAEMON_CONFIG.get_or_init(|| {
let config = DaemonConfig::default(); Arc::new(Mutex::new(DaemonConfig::load_from_file().unwrap_or_else(
config.save_to_file().ok(); |_| {
config let config = DaemonConfig::default();
config.save_to_file().ok();
config
},
)))
}) })
} }
pub fn with_daemon_config<R>(f: impl FnOnce(&mut DaemonConfig) -> R) -> R {
let config = get_daemon_config().clone();
let mut guard = config.lock().unwrap();
f(&mut guard)
}
fn get_current_uid() -> u32 { fn get_current_uid() -> u32 {
rustix::process::geteuid().as_raw() rustix::process::geteuid().as_raw()
} }
+24 -5
View File
@@ -1,7 +1,7 @@
use crate::{ use crate::{
types::{ types::{
audio_player::FullState, audio_player::FullState,
config::{GuiConfig, HotkeyConfig}, config::{DaemonConfig, GuiConfig, HotkeyConfig},
gui::AudioPlayerState, gui::AudioPlayerState,
socket::{Request, Response}, socket::{Request, Response},
}, },
@@ -9,6 +9,7 @@ use crate::{
}; };
use anyhow::{Result, anyhow}; use anyhow::{Result, anyhow};
use std::{ use std::{
fs,
path::PathBuf, path::PathBuf,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
time::Instant, time::Instant,
@@ -23,6 +24,19 @@ pub fn get_gui_config() -> GuiConfig {
}) })
} }
pub fn update_daemon_config(new_config: &DaemonConfig) -> Result<()> {
make_request_sync(Request::update_daemon_config(new_config))?;
make_request_async(Request::save_daemon_config());
Ok(())
}
pub fn get_daemon_config() -> Result<DaemonConfig> {
let response = make_request_sync(Request::get_daemon_config())?;
let config: DaemonConfig = serde_json::from_str(&response.message)?;
Ok(config)
}
pub fn make_request_sync(request: Request) -> Result<Response> { pub fn make_request_sync(request: Request) -> Result<Response> {
tokio::task::block_in_place(|| { tokio::task::block_in_place(|| {
tokio::runtime::Handle::current() tokio::runtime::Handle::current()
@@ -37,15 +51,19 @@ pub fn make_request_async(request: Request) {
}); });
} }
pub fn ensure_pwsp_audio_dir() -> PathBuf { pub fn ensure_pwsp_audio_dir() -> Result<PathBuf> {
let audio_dir = dirs::audio_dir().unwrap_or("~/Music".into()); let audio_dir = dirs::audio_dir().unwrap_or(
dirs::home_dir()
.map(|p| p.join("Music"))
.ok_or_else(|| anyhow!("Failed to get home directory. Is your system ok?"))?,
);
let pwsp_audio_dir = audio_dir.join("PWSP"); let pwsp_audio_dir = audio_dir.join("PWSP");
if !pwsp_audio_dir.exists() { if !pwsp_audio_dir.exists() {
std::fs::create_dir_all(&pwsp_audio_dir).ok(); fs::create_dir_all(&pwsp_audio_dir)?;
} }
pwsp_audio_dir Ok(pwsp_audio_dir)
} }
pub fn format_time_pair(position: f32, duration: f32) -> String { pub fn format_time_pair(position: f32, duration: f32) -> String {
@@ -98,6 +116,7 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
}; };
guard.tracks = full_state.tracks; guard.tracks = full_state.tracks;
guard.volume = full_state.volume; guard.volume = full_state.volume;
guard.volume_multiplier = full_state.volume_multiplier;
guard.current_input = full_state guard.current_input = full_state
.current_input .current_input
.split(" - ") .split(" - ")
+8 -14
View File
@@ -27,18 +27,18 @@ with open(cargo_toml_path, "r", encoding="utf-8") as f:
# We want to match version in [workspace.package] # We want to match version in [workspace.package]
# First, let's find the [workspace.package] section # First, let's find the [workspace.package] section
workspace_package_match = re.search( workspace_package_match = re.search(r"\[workspace\.package\](.*?)(?=\n\[|$)", cargo_toml_content, re.DOTALL)
r"\[workspace\.package\](.*?)(?=\n\[|$)", cargo_toml_content, re.DOTALL
)
if not workspace_package_match: if not workspace_package_match:
fatal("Could not find [workspace.package] section in Cargo.toml.") fatal("Could not find [workspace.package] section in Cargo.toml.")
workspace_package_sec = workspace_package_match.group(1) if isinstance(workspace_package_match, re.Match):
workspace_package_sec = workspace_package_match.group(1)
version_match = re.search(r'version\s*=\s*"([^"]+)"', workspace_package_sec) version_match = re.search(r'version\s*=\s*"([^"]+)"', workspace_package_sec)
if not version_match: if not version_match:
fatal("Could not find version in [workspace.package] in Cargo.toml.") fatal("Could not find version in [workspace.package] in Cargo.toml.")
current_version = version_match.group(1) if isinstance(version_match, re.Match):
current_version = version_match.group(1)
print(f"Current version detected: {current_version}") print(f"Current version detected: {current_version}")
# Get new version # Get new version
@@ -62,9 +62,7 @@ print("Updating Cargo.toml...")
def replace_version_in_workspace(match): def replace_version_in_workspace(match):
section_content = match.group(1) section_content = match.group(1)
updated_section_content = re.sub( updated_section_content = re.sub(r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_version}\g<2>", section_content)
r'(version\s*=\s*")[^"]+(")', rf"\g<1>{new_version}\g<2>", section_content
)
return f"[workspace.package]{updated_section_content}" return f"[workspace.package]{updated_section_content}"
@@ -127,9 +125,7 @@ def update_srcinfo(directory, pkgbuild_path, srcinfo_path):
f.write(result.stdout) f.write(result.stdout)
return return
except Exception as e: except Exception as e:
print( print(f"Warning: makepkg failed in {directory}: {e}. Falling back to text replacement.")
f"Warning: makepkg failed in {directory}: {e}. Falling back to text replacement."
)
# Text replacement fallback # Text replacement fallback
with open(srcinfo_path, "r", encoding="utf-8") as f: with open(srcinfo_path, "r", encoding="utf-8") as f:
@@ -142,9 +138,7 @@ def update_srcinfo(directory, pkgbuild_path, srcinfo_path):
update_srcinfo("packages/aur/bin", pkgbuild_bin_path, "packages/aur/bin/.SRCINFO") update_srcinfo("packages/aur/bin", pkgbuild_bin_path, "packages/aur/bin/.SRCINFO")
update_srcinfo( update_srcinfo("packages/aur/standart", pkgbuild_std_path, "packages/aur/standart/.SRCINFO")
"packages/aur/standart", pkgbuild_std_path, "packages/aur/standart/.SRCINFO"
)
# 4. Update packages/flatpak/ru.arabianq.pwsp.metainfo.xml # 4. Update packages/flatpak/ru.arabianq.pwsp.metainfo.xml
flatpak_xml_path = "packages/flatpak/ru.arabianq.pwsp.metainfo.xml" flatpak_xml_path = "packages/flatpak/ru.arabianq.pwsp.metainfo.xml"