mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-06-19 20:23:33 +00:00
Compare commits
21 Commits
v1.8.0
...
660ece9866
| Author | SHA1 | Date | |
|---|---|---|---|
| 660ece9866 | |||
| f2dcf2e0fe | |||
| fe655be59a | |||
| 78960cdc10 | |||
| 0439cf815e | |||
| 5ae82ef28c | |||
| 5f69345d45 | |||
| 930857312d | |||
| e884993dba | |||
| 05dd4319cc | |||
| e320c85a6f | |||
| f02bbc1e1c | |||
| 02f1116076 | |||
| 8155cceac8 | |||
| d974a93c04 | |||
| c6d9f2d6e7 | |||
| dc1ecc81ea | |||
| 9b70bcd69d | |||
| a07025b1f6 | |||
| c1d145fbc8 | |||
| 3d4b59761b |
@@ -47,8 +47,8 @@ jobs:
|
|||||||
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
|
echo "$BIN_NAMES" >> $GITHUB_OUTPUT
|
||||||
echo "EOF" >> $GITHUB_OUTPUT
|
echo "EOF" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Build all release binaries
|
- name: Build all binaries
|
||||||
run: cargo build --release --locked
|
run: cargo build --locked
|
||||||
|
|
||||||
- name: Package all binaries into one archive
|
- name: Package all binaries into one archive
|
||||||
shell: bash
|
shell: bash
|
||||||
@@ -61,7 +61,7 @@ jobs:
|
|||||||
FILES=()
|
FILES=()
|
||||||
while IFS= read -r BIN; do
|
while IFS= read -r BIN; do
|
||||||
[ -z "$BIN" ] && continue
|
[ -z "$BIN" ] && continue
|
||||||
FILES+=("target/release/$BIN")
|
FILES+=("target/debug/$BIN")
|
||||||
done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
|
done <<< "${{ steps.cargo-meta.outputs.bin_names }}"
|
||||||
|
|
||||||
if [ "${#FILES[@]}" -eq 0 ]; then
|
if [ "${#FILES[@]}" -eq 0 ]; then
|
||||||
|
|||||||
Generated
+229
-15
@@ -140,6 +140,15 @@ dependencies = [
|
|||||||
"x11rb",
|
"x11rb",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "arc-swap"
|
||||||
|
version = "1.9.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207"
|
||||||
|
dependencies = [
|
||||||
|
"rustversion",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arrayvec"
|
name = "arrayvec"
|
||||||
version = "0.7.6"
|
version = "0.7.6"
|
||||||
@@ -309,6 +318,12 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base62"
|
||||||
|
version = "2.2.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bindgen"
|
name = "bindgen"
|
||||||
version = "0.72.1"
|
version = "0.72.1"
|
||||||
@@ -1514,6 +1529,30 @@ version = "0.3.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globset"
|
||||||
|
version = "0.4.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
|
||||||
|
dependencies = [
|
||||||
|
"aho-corasick",
|
||||||
|
"bstr",
|
||||||
|
"log",
|
||||||
|
"regex-automata",
|
||||||
|
"regex-syntax",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "globwalk"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags 1.3.2",
|
||||||
|
"ignore",
|
||||||
|
"walkdir",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.17.0"
|
version = "0.17.0"
|
||||||
@@ -1773,6 +1812,22 @@ dependencies = [
|
|||||||
"icu_properties",
|
"icu_properties",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ignore"
|
||||||
|
version = "0.4.25"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-deque",
|
||||||
|
"globset",
|
||||||
|
"log",
|
||||||
|
"memchr",
|
||||||
|
"regex-automata",
|
||||||
|
"same-file",
|
||||||
|
"walkdir",
|
||||||
|
"winapi-util",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "image"
|
name = "image"
|
||||||
version = "0.25.10"
|
version = "0.25.10"
|
||||||
@@ -1832,6 +1887,15 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itertools"
|
||||||
|
version = "0.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||||
|
dependencies = [
|
||||||
|
"either",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
@@ -3053,7 +3117,7 @@ version = "3.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"toml_edit",
|
"toml_edit 0.25.11+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3095,8 +3159,9 @@ checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.8.0"
|
version = "1.9.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
"clap",
|
"clap",
|
||||||
"dirs",
|
"dirs",
|
||||||
@@ -3111,8 +3176,11 @@ dependencies = [
|
|||||||
"pipewire",
|
"pipewire",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rodio",
|
"rodio",
|
||||||
|
"rust-i18n",
|
||||||
|
"rustix 1.1.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"sys-locale",
|
||||||
"system-fonts",
|
"system-fonts",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
@@ -3338,6 +3406,57 @@ version = "0.20.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba"
|
||||||
|
dependencies = [
|
||||||
|
"globwalk",
|
||||||
|
"regex",
|
||||||
|
"rust-i18n-macro",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"smallvec",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-macro"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52"
|
||||||
|
dependencies = [
|
||||||
|
"glob",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rust-i18n-support",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"syn",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rust-i18n-support"
|
||||||
|
version = "4.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"base62",
|
||||||
|
"globwalk",
|
||||||
|
"itertools 0.11.0",
|
||||||
|
"lazy_static",
|
||||||
|
"normpath",
|
||||||
|
"proc-macro2",
|
||||||
|
"regex",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_yaml",
|
||||||
|
"siphasher",
|
||||||
|
"toml 0.8.23",
|
||||||
|
"triomphe",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -3391,6 +3510,12 @@ version = "1.0.22"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ryu"
|
||||||
|
version = "1.0.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "same-file"
|
name = "same-file"
|
||||||
version = "1.0.6"
|
version = "1.0.6"
|
||||||
@@ -3478,6 +3603,15 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_spanned"
|
||||||
|
version = "0.6.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_spanned"
|
name = "serde_spanned"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -3487,6 +3621,19 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_yaml"
|
||||||
|
version = "0.9.34+deprecated"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
"unsafe-libyaml",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "shlex"
|
||||||
version = "1.3.0"
|
version = "1.3.0"
|
||||||
@@ -3914,7 +4061,7 @@ dependencies = [
|
|||||||
"cfg-expr",
|
"cfg-expr",
|
||||||
"heck",
|
"heck",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
"toml",
|
"toml 1.1.2+spec-1.1.0",
|
||||||
"version-compare",
|
"version-compare",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4034,9 +4181,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.52.3"
|
version = "1.52.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
|
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes",
|
"bytes",
|
||||||
"libc",
|
"libc",
|
||||||
@@ -4060,6 +4207,18 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml"
|
||||||
|
version = "0.8.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_edit 0.22.27",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "1.1.2+spec-1.1.0"
|
version = "1.1.2+spec-1.1.0"
|
||||||
@@ -4068,11 +4227,20 @@ checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
"serde_spanned",
|
"serde_spanned 1.1.1",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"toml_writer",
|
"toml_writer",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_datetime"
|
||||||
|
version = "0.6.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4084,6 +4252,20 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_edit"
|
||||||
|
version = "0.22.27"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"serde",
|
||||||
|
"serde_spanned 0.6.9",
|
||||||
|
"toml_datetime 0.6.11",
|
||||||
|
"toml_write",
|
||||||
|
"winnow 0.7.15",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.25.11+spec-1.1.0"
|
version = "0.25.11+spec-1.1.0"
|
||||||
@@ -4091,9 +4273,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"toml_datetime",
|
"toml_datetime 1.1.1+spec-1.1.0",
|
||||||
"toml_parser",
|
"toml_parser",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4102,9 +4284,15 @@ version = "1.1.2+spec-1.1.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "toml_write"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_writer"
|
name = "toml_writer"
|
||||||
version = "1.1.1+spec-1.1.0"
|
version = "1.1.1+spec-1.1.0"
|
||||||
@@ -4143,6 +4331,17 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "triomphe"
|
||||||
|
version = "0.1.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39"
|
||||||
|
dependencies = [
|
||||||
|
"arc-swap",
|
||||||
|
"serde",
|
||||||
|
"stable_deref_trait",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
@@ -4202,6 +4401,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unsafe-libyaml"
|
||||||
|
version = "0.2.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -5084,6 +5289,15 @@ dependencies = [
|
|||||||
"xkbcommon-dl",
|
"xkbcommon-dl",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "winnow"
|
||||||
|
version = "0.7.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "winnow"
|
name = "winnow"
|
||||||
version = "1.0.2"
|
version = "1.0.2"
|
||||||
@@ -5317,7 +5531,7 @@ dependencies = [
|
|||||||
"uds_windows",
|
"uds_windows",
|
||||||
"uuid",
|
"uuid",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
"zbus_macros",
|
"zbus_macros",
|
||||||
"zbus_names",
|
"zbus_names",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
@@ -5345,7 +5559,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
|
checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
"zvariant",
|
"zvariant",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -5482,7 +5696,7 @@ dependencies = [
|
|||||||
"enumflags2",
|
"enumflags2",
|
||||||
"serde",
|
"serde",
|
||||||
"url",
|
"url",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
"zvariant_derive",
|
"zvariant_derive",
|
||||||
"zvariant_utils",
|
"zvariant_utils",
|
||||||
]
|
]
|
||||||
@@ -5510,5 +5724,5 @@ dependencies = [
|
|||||||
"quote",
|
"quote",
|
||||||
"serde",
|
"serde",
|
||||||
"syn",
|
"syn",
|
||||||
"winnow",
|
"winnow 1.0.2",
|
||||||
]
|
]
|
||||||
|
|||||||
+16
-8
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "pwsp"
|
name = "pwsp"
|
||||||
version = "1.8.0"
|
version = "1.9.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
authors = ["arabian"]
|
authors = ["arabian"]
|
||||||
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
|
||||||
@@ -12,7 +12,7 @@ keywords = ["soundpad", "pipewire", "linux", "cli", "gui"]
|
|||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tokio = { version = "1.52.3", features = ["full"] }
|
tokio = { version = "1.52.1", features = ["full"] }
|
||||||
async-trait = "0.1.89"
|
async-trait = "0.1.89"
|
||||||
|
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
@@ -26,8 +26,22 @@ clap = { version = "4.6.1", default-features = false, features = [
|
|||||||
"error-context",
|
"error-context",
|
||||||
"derive",
|
"derive",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
dirs = "6.0.0"
|
dirs = "6.0.0"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
|
evdev = { version = "0.13.2", features = ["tokio"] }
|
||||||
|
rfd = { version = "0.17.2", default-features = false, features = [
|
||||||
|
|
||||||
|
"xdg-portal",
|
||||||
|
|
||||||
|
] }
|
||||||
|
opener = { version = "0.8.4", features = ["reveal"] }
|
||||||
|
system-fonts = "0.1.0"
|
||||||
|
anyhow = "1.0.102"
|
||||||
|
rustix = { version = "1.1.4", features = ["process"] }
|
||||||
|
|
||||||
|
rust-i18n = "4.0.0"
|
||||||
|
sys-locale = "0.3.2"
|
||||||
|
|
||||||
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [
|
rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622bd82b87b8731585245802d9cf", default-features = false, features = [
|
||||||
"symphonia-all",
|
"symphonia-all",
|
||||||
@@ -35,12 +49,6 @@ rodio = { git = "https://github.com/arabianq/rodio.git", rev = "1a08f281c352622b
|
|||||||
"playback",
|
"playback",
|
||||||
] }
|
] }
|
||||||
pipewire = "0.9.2"
|
pipewire = "0.9.2"
|
||||||
evdev = { version = "0.13.2", features = ["tokio"] }
|
|
||||||
rfd = { version = "0.17.2", default-features = false, features = [
|
|
||||||
"xdg-portal",
|
|
||||||
] }
|
|
||||||
opener = { version = "0.8.4", features = ["reveal"] }
|
|
||||||
system-fonts = "0.1.0"
|
|
||||||
|
|
||||||
egui = { version = "0.34.2", default-features = false, features = [
|
egui = { version = "0.34.2", default-features = false, features = [
|
||||||
"default_fonts",
|
"default_fonts",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2025 Tarasov Alexander
|
Copyright (c) 2026 arabianq
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -99,15 +99,13 @@ pwsp-cli --help # View all commands
|
|||||||
|
|
||||||
| Action | Keyboard | Mouse |
|
| Action | Keyboard | Mouse |
|
||||||
| :----------------------------------- | :--------------------- | :------------------- |
|
| :----------------------------------- | :--------------------- | :------------------- |
|
||||||
| **Play Track** (Stops others) | `Enter` | `Left Click` |
|
| **Play Track** (Stops others) | | `Left Click` |
|
||||||
| **Add Track** (Plays simultaneously) | `Ctrl + Enter` | `Ctrl + Left Click` |
|
| **Add Track** (Plays simultaneously) | | `Ctrl + Left Click` |
|
||||||
| **Replace Last Track** | `Shift + Enter` | `Shift + Left Click` |
|
| **Replace Last Track** | | `Shift + Left Click` |
|
||||||
| **Pause / Resume** | `Space` | |
|
| **Pause / Resume** | `Space` | |
|
||||||
| **Stop All Tracks** | `Backspace` | |
|
| **Stop All Tracks** | `Backspace` | |
|
||||||
| **Open / Close Settings** | `I` | |
|
| **Open / Close Settings** | `I` | |
|
||||||
| **Search** | `/` | |
|
| **Search** | `/` | |
|
||||||
| **Navigate Files** | `Ctrl + ↑ / ↓` | |
|
|
||||||
| **Navigate Directories** | `Ctrl + Shift + ↑ / ↓` | |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,376 @@
|
|||||||
|
_version = 2
|
||||||
|
|
||||||
|
# ----------------
|
||||||
|
# Main page
|
||||||
|
# ----------------
|
||||||
|
|
||||||
|
[gui.play_file_button]
|
||||||
|
en = "Play file"
|
||||||
|
ru = "Выбрать файл"
|
||||||
|
es = "Reproducir archivo"
|
||||||
|
fr = "Lire le fichier"
|
||||||
|
zh = "播放文件"
|
||||||
|
ar = "تشغيل الملف"
|
||||||
|
kz = "Файлды ойнату"
|
||||||
|
he = "נגן קובץ"
|
||||||
|
pt-BR = "Reproduzir arquivo"
|
||||||
|
|
||||||
|
[gui.choose_mic_select]
|
||||||
|
en = "Select microphone"
|
||||||
|
ru = "Выбрать микрофон"
|
||||||
|
es = "Seleccionar micrófono"
|
||||||
|
fr = "Sélectionner le microphone"
|
||||||
|
zh = "选择麦克风"
|
||||||
|
ar = "اختر الميكروفون"
|
||||||
|
kz = "Микрофонды таңдау"
|
||||||
|
he = "בחר מיקרופון"
|
||||||
|
pt-BR = "Selecionar microfone"
|
||||||
|
|
||||||
|
[gui.search_placeholder]
|
||||||
|
en = "Search files..."
|
||||||
|
ru = "Поиск файлов..."
|
||||||
|
es = "Buscar archivos..."
|
||||||
|
fr = "Rechercher des fichiers..."
|
||||||
|
zh = "搜索文件..."
|
||||||
|
ar = "البحث عن ملفات..."
|
||||||
|
kz = "Файлдарды іздеу..."
|
||||||
|
he = "חפש קבצים..."
|
||||||
|
pt-BR = "Buscar arquivos..."
|
||||||
|
|
||||||
|
[gui.context.dirs.open]
|
||||||
|
en = "Open"
|
||||||
|
ru = "Открыть"
|
||||||
|
es = "Abrir"
|
||||||
|
fr = "Ouvrir"
|
||||||
|
zh = "打开"
|
||||||
|
ar = "فتح"
|
||||||
|
kz = "Ашу"
|
||||||
|
he = "פתח"
|
||||||
|
pt-BR = "Abrir"
|
||||||
|
|
||||||
|
[gui.context.dirs.open_in_fm]
|
||||||
|
en = "Open in File Manager"
|
||||||
|
ru = "Открыть в менеджере файлов"
|
||||||
|
es = "Abrir en el gestor de archivos"
|
||||||
|
fr = "Ouvrir dans le gestionnaire de fichiers"
|
||||||
|
zh = "在文件管理器中打开"
|
||||||
|
ar = "فتح في مدير الملفات"
|
||||||
|
kz = "Файл менеджерінде ашу"
|
||||||
|
he = "פתח במנהל הקבצים"
|
||||||
|
pt-BR = "Abrir no gestor de arquivos"
|
||||||
|
|
||||||
|
[gui.context.dirs.remove]
|
||||||
|
en = "Remove"
|
||||||
|
ru = "Удалить"
|
||||||
|
es = "Eliminar"
|
||||||
|
fr = "Supprimer"
|
||||||
|
zh = "移除"
|
||||||
|
ar = "إزالة"
|
||||||
|
kz = "Жою"
|
||||||
|
he = "הסר"
|
||||||
|
pt-BR = "Remover"
|
||||||
|
|
||||||
|
[gui.context.files.play_solo]
|
||||||
|
en = "Play Solo"
|
||||||
|
ru = "Играть"
|
||||||
|
es = "Reproducir solo"
|
||||||
|
fr = "Jouer en solo"
|
||||||
|
zh = "单独播放"
|
||||||
|
ar = "تشغيل منفرد"
|
||||||
|
kz = "Жалғыз ойнату"
|
||||||
|
he = "נגן סולו"
|
||||||
|
pt-BR = "Reproduzir"
|
||||||
|
|
||||||
|
[gui.context.files.add_new]
|
||||||
|
en = "Add New"
|
||||||
|
ru = "Добавить"
|
||||||
|
es = "Añadir nuevo"
|
||||||
|
fr = "Ajouter un nouveau"
|
||||||
|
zh = "添加新项"
|
||||||
|
ar = "إضافة جديد"
|
||||||
|
kz = "Жаңасын қосу"
|
||||||
|
he = "הוסף חדש"
|
||||||
|
pt-BR = "Adicionar"
|
||||||
|
|
||||||
|
[gui.context.files.replace_last]
|
||||||
|
en = "Replace Last"
|
||||||
|
ru = "Заменить Последний"
|
||||||
|
es = "Reemplazar último"
|
||||||
|
fr = "Remplacer le dernier"
|
||||||
|
zh = "替换上一个"
|
||||||
|
ar = "استبدال الأخير"
|
||||||
|
kz = "Соңғысын ауыстыру"
|
||||||
|
he = "החלף אחרון"
|
||||||
|
pt-BR = "Substituir"
|
||||||
|
|
||||||
|
[gui.context.files.show_in_fm]
|
||||||
|
en = "Show in File Manager"
|
||||||
|
ru = "Открыть в менеджере файлов"
|
||||||
|
es = "Mostrar en el gestor de archivos"
|
||||||
|
fr = "Afficher dans le gestionnaire de fichiers"
|
||||||
|
zh = "在文件管理器中显示"
|
||||||
|
ar = "عرض في مدير الملفات"
|
||||||
|
kz = "Файл менеджерінде көрсету"
|
||||||
|
he = "הצג במנהל הקבצים"
|
||||||
|
pt-BR = "Mostrar no gestor de arquivos"
|
||||||
|
|
||||||
|
[gui.context.files.asign_hotkey]
|
||||||
|
en = "Assign Hotkey"
|
||||||
|
ru = "Назначить Горячую Клавишу"
|
||||||
|
es = "Asignar atajo"
|
||||||
|
fr = "Assigner un raccourci"
|
||||||
|
zh = "分配快捷键"
|
||||||
|
ar = "تعيين مفتاح اختصار"
|
||||||
|
kz = "Ыстық пернені тағайындау"
|
||||||
|
he = "הקצה מקש קיצור"
|
||||||
|
pt-BR = "Definir tecla de atalho"
|
||||||
|
|
||||||
|
# ----------------
|
||||||
|
# Settings
|
||||||
|
# ----------------
|
||||||
|
|
||||||
|
[gui.settings.header]
|
||||||
|
en = "Settings"
|
||||||
|
ru = "Настройки"
|
||||||
|
es = "Ajustes"
|
||||||
|
fr = "Paramètres"
|
||||||
|
zh = "设置"
|
||||||
|
ar = "الإعدادات"
|
||||||
|
kz = "Баптаулар"
|
||||||
|
he = "הגדרות"
|
||||||
|
pt-BR = "Configurações"
|
||||||
|
|
||||||
|
[gui.settings.remember_volume]
|
||||||
|
en = "Always remember volume"
|
||||||
|
ru = "Всегда запоминать громкость"
|
||||||
|
es = "Recordar siempre el volumen"
|
||||||
|
fr = "Toujours se souvenir du volume"
|
||||||
|
zh = "始终记住音量"
|
||||||
|
ar = "تذكر مستوى الصوت دائمًا"
|
||||||
|
kz = "Әрқашан дыбыс деңгейін есте сақтау"
|
||||||
|
he = "זכור תמיד עוצמת קול"
|
||||||
|
pt-BR = "Lembrar volume"
|
||||||
|
|
||||||
|
[gui.settings.remember_mic]
|
||||||
|
en = "Always remember microphone"
|
||||||
|
ru = "Всегда запоминать микрофон"
|
||||||
|
es = "Recordar siempre el micrófono"
|
||||||
|
fr = "Toujours se souvenir du microphone"
|
||||||
|
zh = "始终记住麦克风"
|
||||||
|
ar = "تذكر الميكروفون دائمًا"
|
||||||
|
kz = "Әрқашан микрофонды есте сақтау"
|
||||||
|
he = "זכור תמיד מיקרופון"
|
||||||
|
pt-BR = "Lembrar microfone"
|
||||||
|
|
||||||
|
[gui.settings.remember_ui_scale]
|
||||||
|
en = "Always remember UI scale factor"
|
||||||
|
ru = "Всегда запоминать масштаб интерфейса"
|
||||||
|
es = "Recordar siempre la escala de la interfaz"
|
||||||
|
fr = "Toujours se souvenir de l'échelle de l'interface"
|
||||||
|
zh = "始终记住界面缩放比例"
|
||||||
|
ar = "تذكر عامل تكبير الواجهة دائمًا"
|
||||||
|
kz = "Әрқашан интерфейс масштабын есте сақтау"
|
||||||
|
he = "זכור תמיד קנה מידה של ממשק משתמש"
|
||||||
|
pt-BR = "Lembrar fator de escala da interface"
|
||||||
|
|
||||||
|
[gui.settings.pause_on_window_close]
|
||||||
|
en = "Pause audio playback when the window is closed"
|
||||||
|
ru = "Останавливать воспроизведение при закрытии окна"
|
||||||
|
es = "Pausar la reproducción de audio al cerrar la ventana"
|
||||||
|
fr = "Mettre en pause la lecture audio à la fermeture de la fenêtre"
|
||||||
|
zh = "关闭窗口时暂停音频播放"
|
||||||
|
ar = "إيقاف الصوت مؤقتًا عند إغلاق النافذة"
|
||||||
|
kz = "Терезе жабылған кезде дыбысты ойнатуды кідірту"
|
||||||
|
he = "השהה השמעת שמע כאשר החלון נסגר"
|
||||||
|
pt-BR = "Pausar reprodução de aúdio ao fechar a janela"
|
||||||
|
|
||||||
|
[gui.settings.version]
|
||||||
|
en = "GUI version: %{version}"
|
||||||
|
ru = "Версия GUI: %{version}"
|
||||||
|
es = "Versión de la GUI: %{version}"
|
||||||
|
fr = "Version de l'interface : %{version}"
|
||||||
|
zh = "GUI 版本: %{version}"
|
||||||
|
ar = "إصدار الواجهة: %{version}"
|
||||||
|
kz = "GUI нұсқасы: %{version}"
|
||||||
|
he = "גרסת ממשק משתמש: %{version}"
|
||||||
|
pt-BR = "Versão da GUI: %{version}"
|
||||||
|
|
||||||
|
# ----------------
|
||||||
|
# Hotkeys
|
||||||
|
# ----------------
|
||||||
|
|
||||||
|
[gui.hotkeys.header]
|
||||||
|
en = "Hotkeys"
|
||||||
|
ru = "Горячие клавиши"
|
||||||
|
es = "Atajos de teclado"
|
||||||
|
fr = "Raccourcis clavier"
|
||||||
|
zh = "快捷键"
|
||||||
|
ar = "اختصارات لوحة المفاتيح"
|
||||||
|
kz = "Ыстық пернелер"
|
||||||
|
he = "מקשי קיצור"
|
||||||
|
pt-BR = "Atalhos"
|
||||||
|
|
||||||
|
[gui.hotkeys.search_placeholder]
|
||||||
|
en = "Search hotkeys..."
|
||||||
|
ru = "Поиск горячих клавиш..."
|
||||||
|
es = "Buscar atajos..."
|
||||||
|
fr = "Rechercher des raccourcis..."
|
||||||
|
zh = "搜索快捷键..."
|
||||||
|
ar = "البحث عن الاختصارات..."
|
||||||
|
kz = "Ыстық пернелерді іздеу..."
|
||||||
|
he = "חפש מקשי קיצור..."
|
||||||
|
pt-BR = "Buscar atalhos..."
|
||||||
|
|
||||||
|
[gui.hotkeys.add_command_select]
|
||||||
|
en = "Add Command"
|
||||||
|
ru = "Добавить команду"
|
||||||
|
es = "Añadir comando"
|
||||||
|
fr = "Ajouter une commande"
|
||||||
|
zh = "添加命令"
|
||||||
|
ar = "إضافة أمر"
|
||||||
|
kz = "Команда қосу"
|
||||||
|
he = "הוסף פקודה"
|
||||||
|
pt-BR = "Adicionar comando"
|
||||||
|
|
||||||
|
[gui.hotkeys.toggle_pause_command]
|
||||||
|
en = "Toggle Pause"
|
||||||
|
ru = "Переключить паузу"
|
||||||
|
es = "Alternar pausa"
|
||||||
|
fr = "Basculer la pause"
|
||||||
|
zh = "切换暂停"
|
||||||
|
ar = "تبديل الإيقاف المؤقت"
|
||||||
|
kz = "Кідіртуді ауыстыру"
|
||||||
|
he = "הפעל/השהה"
|
||||||
|
pt-BR = "Alternar reprodução"
|
||||||
|
|
||||||
|
[gui.hotkeys.stop_playback_command]
|
||||||
|
en = "Stop Playback"
|
||||||
|
ru = "Остановить воспроизведение"
|
||||||
|
es = "Detener reproducción"
|
||||||
|
fr = "Arrêter la lecture"
|
||||||
|
zh = "停止播放"
|
||||||
|
ar = "إيقاف التشغيل"
|
||||||
|
kz = "Ойнатуды тоқтату"
|
||||||
|
he = "עצור השמעה"
|
||||||
|
pt-BR = "Parar reprodução"
|
||||||
|
|
||||||
|
[gui.hotkeys.pause_playback_command]
|
||||||
|
en = "Pause Playback"
|
||||||
|
ru = "Поставить воспроизведение на паузу"
|
||||||
|
es = "Pausar reproducción"
|
||||||
|
fr = "Mettre en pause la lecture"
|
||||||
|
zh = "暂停播放"
|
||||||
|
ar = "إيقاف التشغيل مؤقتاً"
|
||||||
|
kz = "Ойнатуды кідірту"
|
||||||
|
he = "השהה השמעה"
|
||||||
|
pt-BR = "Pausar reprodução"
|
||||||
|
|
||||||
|
[gui.hotkeys.resume_playback_command]
|
||||||
|
en = "Resume Playback"
|
||||||
|
ru = "Продолжить воспроизведение"
|
||||||
|
es = "Reanudar reproducción"
|
||||||
|
fr = "Reprendre la lecture"
|
||||||
|
zh = "恢复播放"
|
||||||
|
ar = "استئناف التشغيل"
|
||||||
|
kz = "Ойнатуды жалғастыру"
|
||||||
|
he = "המשך השמעה"
|
||||||
|
pt-BR = "Resumir reprodução"
|
||||||
|
|
||||||
|
[gui.hotkeys.toggle_loop_command]
|
||||||
|
en = "Toggle Loop"
|
||||||
|
ru = "Переключить зацикливание"
|
||||||
|
es = "Alternar bucle"
|
||||||
|
fr = "Basculer la boucle"
|
||||||
|
zh = "切换循环"
|
||||||
|
ar = "تبديل التكرار"
|
||||||
|
kz = "Қайталауды ауыстыру"
|
||||||
|
he = "הפעל/כבה לולאה"
|
||||||
|
pt-BR = "Alternar loop"
|
||||||
|
|
||||||
|
[gui.hotkeys.column_slot]
|
||||||
|
en = "Slot"
|
||||||
|
ru = "Слот"
|
||||||
|
es = "Ranura"
|
||||||
|
fr = "Emplacement"
|
||||||
|
zh = "插槽"
|
||||||
|
ar = "الخانة"
|
||||||
|
kz = "Ұяшық"
|
||||||
|
he = "משבצת"
|
||||||
|
pt-BR = "Slot"
|
||||||
|
|
||||||
|
[gui.hotkeys.column_sound]
|
||||||
|
en = "Sound"
|
||||||
|
ru = "Звук"
|
||||||
|
es = "Sonido"
|
||||||
|
fr = "Son"
|
||||||
|
zh = "声音"
|
||||||
|
ar = "الصوت"
|
||||||
|
kz = "Дыбыс"
|
||||||
|
he = "צליל"
|
||||||
|
pt-BR = "Som"
|
||||||
|
|
||||||
|
[gui.hotkeys.column_key_chord]
|
||||||
|
en = "Key Chord"
|
||||||
|
ru = "Клавиша"
|
||||||
|
es = "Combinación de teclas"
|
||||||
|
fr = "Combinaison de touches"
|
||||||
|
zh = "组合键"
|
||||||
|
ar = "تركيبة المفاتيح"
|
||||||
|
kz = "Пернелер тіркесімі"
|
||||||
|
he = "צירוף מקשים"
|
||||||
|
pt-BR = "Combinação de teclas"
|
||||||
|
|
||||||
|
[gui.hotkeys.column_actions]
|
||||||
|
en = "Actions"
|
||||||
|
ru = "Действия"
|
||||||
|
es = "Acciones"
|
||||||
|
fr = "Actions"
|
||||||
|
zh = "操作"
|
||||||
|
ar = "الإجراءات"
|
||||||
|
kz = "Әрекеттер"
|
||||||
|
he = "פעולות"
|
||||||
|
pt-BR = "Ações"
|
||||||
|
|
||||||
|
[gui.hotkeys.no_hotkeys_configured]
|
||||||
|
en = "No hotkeys configured"
|
||||||
|
ru = "Горячие клавиши не настроены"
|
||||||
|
es = "No hay atajos configurados"
|
||||||
|
fr = "Aucun raccourci configuré"
|
||||||
|
zh = "未配置快捷键"
|
||||||
|
ar = "لا توجد اختصارات معينة"
|
||||||
|
kz = "Ыстық пернелер бапталмаған"
|
||||||
|
he = "לא הוגדרו מקשי קיצור"
|
||||||
|
pt-BR = "Nenhum atalho configurado"
|
||||||
|
|
||||||
|
[gui.hotkeys.capture.header]
|
||||||
|
en = "Press a key combination (e.g. Ctrl+Alt+1)"
|
||||||
|
ru = "Нажмите сочетание клавиш (например, Ctrl+Alt+1)"
|
||||||
|
es = "Presione una combinación de teclas (ej. Ctrl+Alt+1)"
|
||||||
|
fr = "Appuyez sur une combinaison de touches (ex. Ctrl+Alt+1)"
|
||||||
|
zh = "按下一个组合键 (例如 Ctrl+Alt+1)"
|
||||||
|
ar = "اضغط على تركيبة مفاتيح (مثلاً Ctrl+Alt+1)"
|
||||||
|
kz = "Пернелер тіркесімін басыңыз (мысалы, Ctrl+Alt+1)"
|
||||||
|
he = "לחץ על צירוף מקשים (למשל Ctrl+Alt+1)"
|
||||||
|
pt-BR = "Pressione uma combinação de tecla (ex: Ctrl+Alt+1)"
|
||||||
|
|
||||||
|
[gui.hotkeys.capture.for]
|
||||||
|
en = "for"
|
||||||
|
ru = "для"
|
||||||
|
es = "para"
|
||||||
|
fr = "pour"
|
||||||
|
zh = "用于"
|
||||||
|
ar = "لـ"
|
||||||
|
kz = "үшін"
|
||||||
|
he = "עבור"
|
||||||
|
pt-BR = "para"
|
||||||
|
|
||||||
|
[gui.hotkeys.capture.cancel]
|
||||||
|
en = "Press Escape to cancel"
|
||||||
|
ru = "Нажмите Escape для отмены"
|
||||||
|
es = "Presione Escape para cancelar"
|
||||||
|
fr = "Appuyez sur Échap pour annuler"
|
||||||
|
zh = "按 Escape 取消"
|
||||||
|
ar = "اضغط Esc للإلغاء"
|
||||||
|
kz = "Болдырмау үшін Escape пернесін басыңыз"
|
||||||
|
he = "לחץ על Escape לביטול"
|
||||||
|
pt-BR = "Pressione Esc para cancelar"
|
||||||
@@ -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.8.0
|
pkgver = 1.9.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
|
||||||
@@ -9,8 +9,8 @@ depends = pipewire
|
|||||||
depends = alsa-lib
|
depends = alsa-lib
|
||||||
provides = pwsp
|
provides = pwsp
|
||||||
conflicts = pwsp
|
conflicts = pwsp
|
||||||
source = pwsp-bin-1.8.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.8.0/pwsp-v1.8.0-linux-x64.zip
|
source = pwsp-bin-1.9.0.zip :: https://github.com/arabianq/pipewire-soundpad/releases/download/v1.9.0/pwsp-v1.9.0-linux-x64.zip
|
||||||
source = pipewire-soundpad-1.8.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.8.0.tar.gz
|
source = pipewire-soundpad-1.9.0.tar.gz :: https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
|
|
||||||
|
|||||||
@@ -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.8.0
|
pkgver=1.9.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')
|
arch=('x86_64')
|
||||||
|
|||||||
@@ -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.8.0
|
pkgver = 1.9.0
|
||||||
pkgrel = 1
|
pkgrel = 1
|
||||||
url = https://github.com/arabianq/pipewire-soundpad
|
url = https://github.com/arabianq/pipewire-soundpad
|
||||||
arch = any
|
arch = any
|
||||||
@@ -11,7 +11,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.8.0.tar.gz
|
source = https://github.com/arabianq/pipewire-soundpad/archive/refs/tags/v1.9.0.tar.gz
|
||||||
sha256sums = SKIP
|
sha256sums = SKIP
|
||||||
|
|
||||||
pkgname = pwsp
|
pkgname = pwsp
|
||||||
|
|||||||
@@ -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.8.0
|
pkgver=1.9.0
|
||||||
pkgrel=1
|
pkgrel=1
|
||||||
pkgdesc="Lets you play audio files through your microphone"
|
pkgdesc="Lets you play audio files through your microphone"
|
||||||
arch=('any')
|
arch=('any')
|
||||||
|
|||||||
@@ -193,6 +193,19 @@
|
|||||||
"dest": "cargo/vendor/arboard-3.6.1",
|
"dest": "cargo/vendor/arboard-3.6.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/arc-swap/arc-swap-1.9.1.crate",
|
||||||
|
"sha256": "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207",
|
||||||
|
"dest": "cargo/vendor/arc-swap-1.9.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/arc-swap-1.9.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -388,6 +401,19 @@
|
|||||||
"dest": "cargo/vendor/autocfg-1.5.0",
|
"dest": "cargo/vendor/autocfg-1.5.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/base62/base62-2.2.4.crate",
|
||||||
|
"sha256": "cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b",
|
||||||
|
"dest": "cargo/vendor/base62-2.2.4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"cd637ac531c60eb7fbc4684dc061c2d7d90d73d758181aa02eeff0464b9eee4b\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/base62-2.2.4",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -1966,6 +1992,32 @@
|
|||||||
"dest": "cargo/vendor/glob-0.3.3",
|
"dest": "cargo/vendor/glob-0.3.3",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/globset/globset-0.4.18.crate",
|
||||||
|
"sha256": "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3",
|
||||||
|
"dest": "cargo/vendor/globset-0.4.18"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/globset-0.4.18",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/globwalk/globwalk-0.8.1.crate",
|
||||||
|
"sha256": "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc",
|
||||||
|
"dest": "cargo/vendor/globwalk-0.8.1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/globwalk-0.8.1",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -2291,6 +2343,19 @@
|
|||||||
"dest": "cargo/vendor/idna_adapter-1.2.2",
|
"dest": "cargo/vendor/idna_adapter-1.2.2",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/ignore/ignore-0.4.25.crate",
|
||||||
|
"sha256": "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a",
|
||||||
|
"dest": "cargo/vendor/ignore-0.4.25"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/ignore-0.4.25",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -2356,6 +2421,19 @@
|
|||||||
"dest": "cargo/vendor/indexmap-2.14.0",
|
"dest": "cargo/vendor/indexmap-2.14.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/itertools/itertools-0.11.0.crate",
|
||||||
|
"sha256": "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57",
|
||||||
|
"dest": "cargo/vendor/itertools-0.11.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/itertools-0.11.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4298,6 +4376,45 @@
|
|||||||
"dest": "cargo/vendor/roxmltree-0.20.0",
|
"dest": "cargo/vendor/roxmltree-0.20.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rust-i18n/rust-i18n-4.0.0.crate",
|
||||||
|
"sha256": "21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-4.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"21031bf5e6f2c0ae745d831791c403608e99a8bd3776c7e5e5535acd70c3b7ba\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-4.0.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rust-i18n-macro/rust-i18n-macro-4.0.0.crate",
|
||||||
|
"sha256": "51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-macro-4.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"51fe5295763b358606f7ca26a564e20f4469775a57ec1f09431249a33849ff52\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-macro-4.0.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/rust-i18n-support/rust-i18n-support-4.0.0.crate",
|
||||||
|
"sha256": "69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-support-4.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"69bcc115c8eea2803aa3d85362e339776f4988a0349f2f475af572e497443f6f\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/rust-i18n-support-4.0.0",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4376,6 +4493,19 @@
|
|||||||
"dest": "cargo/vendor/rustversion-1.0.22",
|
"dest": "cargo/vendor/rustversion-1.0.22",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/ryu/ryu-1.0.23.crate",
|
||||||
|
"sha256": "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f",
|
||||||
|
"dest": "cargo/vendor/ryu-1.0.23"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/ryu-1.0.23",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4506,6 +4636,19 @@
|
|||||||
"dest": "cargo/vendor/serde_repr-0.1.20",
|
"dest": "cargo/vendor/serde_repr-0.1.20",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/serde_spanned/serde_spanned-0.6.9.crate",
|
||||||
|
"sha256": "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3",
|
||||||
|
"dest": "cargo/vendor/serde_spanned-0.6.9"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/serde_spanned-0.6.9",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -4519,6 +4662,19 @@
|
|||||||
"dest": "cargo/vendor/serde_spanned-1.1.1",
|
"dest": "cargo/vendor/serde_spanned-1.1.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/serde_yaml/serde_yaml-0.9.34+deprecated.crate",
|
||||||
|
"sha256": "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47",
|
||||||
|
"dest": "cargo/vendor/serde_yaml-0.9.34+deprecated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/serde_yaml-0.9.34+deprecated",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5198,14 +5354,14 @@
|
|||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
"url": "https://static.crates.io/crates/tokio/tokio-1.52.3.crate",
|
"url": "https://static.crates.io/crates/tokio/tokio-1.52.1.crate",
|
||||||
"sha256": "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe",
|
"sha256": "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6",
|
||||||
"dest": "cargo/vendor/tokio-1.52.3"
|
"dest": "cargo/vendor/tokio-1.52.1"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "inline",
|
"type": "inline",
|
||||||
"contents": "{\"package\": \"8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe\", \"files\": {}}",
|
"contents": "{\"package\": \"b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6\", \"files\": {}}",
|
||||||
"dest": "cargo/vendor/tokio-1.52.3",
|
"dest": "cargo/vendor/tokio-1.52.1",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -5221,6 +5377,19 @@
|
|||||||
"dest": "cargo/vendor/tokio-macros-2.7.0",
|
"dest": "cargo/vendor/tokio-macros-2.7.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/toml/toml-0.8.23.crate",
|
||||||
|
"sha256": "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362",
|
||||||
|
"dest": "cargo/vendor/toml-0.8.23"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/toml-0.8.23",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5234,6 +5403,19 @@
|
|||||||
"dest": "cargo/vendor/toml-1.1.2+spec-1.1.0",
|
"dest": "cargo/vendor/toml-1.1.2+spec-1.1.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/toml_datetime/toml_datetime-0.6.11.crate",
|
||||||
|
"sha256": "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c",
|
||||||
|
"dest": "cargo/vendor/toml_datetime-0.6.11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/toml_datetime-0.6.11",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5247,6 +5429,19 @@
|
|||||||
"dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0",
|
"dest": "cargo/vendor/toml_datetime-1.1.1+spec-1.1.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/toml_edit/toml_edit-0.22.27.crate",
|
||||||
|
"sha256": "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a",
|
||||||
|
"dest": "cargo/vendor/toml_edit-0.22.27"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/toml_edit-0.22.27",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5273,6 +5468,19 @@
|
|||||||
"dest": "cargo/vendor/toml_parser-1.1.2+spec-1.1.0",
|
"dest": "cargo/vendor/toml_parser-1.1.2+spec-1.1.0",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/toml_write/toml_write-0.1.2.crate",
|
||||||
|
"sha256": "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801",
|
||||||
|
"dest": "cargo/vendor/toml_write-0.1.2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/toml_write-0.1.2",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5325,6 +5533,19 @@
|
|||||||
"dest": "cargo/vendor/tracing-core-0.1.36",
|
"dest": "cargo/vendor/tracing-core-0.1.36",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/triomphe/triomphe-0.1.15.crate",
|
||||||
|
"sha256": "dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39",
|
||||||
|
"dest": "cargo/vendor/triomphe-0.1.15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"dd69c5aa8f924c7519d6372789a74eac5b94fb0f8fcf0d4a97eb0bfc3e785f39\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/triomphe-0.1.15",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -5429,6 +5650,19 @@
|
|||||||
"dest": "cargo/vendor/unicode-xid-0.2.6",
|
"dest": "cargo/vendor/unicode-xid-0.2.6",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/unsafe-libyaml/unsafe-libyaml-0.2.11.crate",
|
||||||
|
"sha256": "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861",
|
||||||
|
"dest": "cargo/vendor/unsafe-libyaml-0.2.11"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/unsafe-libyaml-0.2.11",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
@@ -6521,6 +6755,19 @@
|
|||||||
"dest": "cargo/vendor/winit-0.30.13",
|
"dest": "cargo/vendor/winit-0.30.13",
|
||||||
"dest-filename": ".cargo-checksum.json"
|
"dest-filename": ".cargo-checksum.json"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "archive",
|
||||||
|
"archive-type": "tar-gzip",
|
||||||
|
"url": "https://static.crates.io/crates/winnow/winnow-0.7.15.crate",
|
||||||
|
"sha256": "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945",
|
||||||
|
"dest": "cargo/vendor/winnow-0.7.15"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "inline",
|
||||||
|
"contents": "{\"package\": \"df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945\", \"files\": {}}",
|
||||||
|
"dest": "cargo/vendor/winnow-0.7.15",
|
||||||
|
"dest-filename": ".cargo-checksum.json"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "archive",
|
"type": "archive",
|
||||||
"archive-type": "tar-gzip",
|
"archive-type": "tar-gzip",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<name>arabian</name>
|
<name>arabian</name>
|
||||||
</developer>
|
</developer>
|
||||||
<releases>
|
<releases>
|
||||||
<release version="1.8.0" date="2026-05-13" />
|
<release version="1.9.0" date="2026-05-15" />
|
||||||
</releases>
|
</releases>
|
||||||
<content_rating type="oars-1.1" />
|
<content_rating type="oars-1.1" />
|
||||||
</component>
|
</component>
|
||||||
@@ -4,7 +4,7 @@
|
|||||||
%global cargo_install_lib 0
|
%global cargo_install_lib 0
|
||||||
|
|
||||||
Name: pwsp
|
Name: pwsp
|
||||||
Version: 1.8.0
|
Version: 1.9.0
|
||||||
Release: %autorelease
|
Release: %autorelease
|
||||||
Summary: Lets you play audio files through your microphone
|
Summary: Lets you play audio files through your microphone
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@ BuildRequires: pipewire-devel
|
|||||||
BuildRequires: alsa-lib-devel
|
BuildRequires: alsa-lib-devel
|
||||||
BuildRequires: clang-devel
|
BuildRequires: clang-devel
|
||||||
BuildRequires: cmake
|
BuildRequires: cmake
|
||||||
|
BuildRequires: dbus-devel
|
||||||
|
BuildRequires: pkgconf-pkg-config
|
||||||
|
|
||||||
%global _description %{expand:
|
%global _description %{expand:
|
||||||
PWSP lets you play audio files through your microphone. Has both CLI and
|
PWSP lets you play audio files through your microphone. Has both CLI and
|
||||||
|
|||||||
+4
-5
@@ -1,9 +1,10 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use pwsp::{
|
use pwsp::{
|
||||||
types::socket::Request,
|
types::socket::Request,
|
||||||
utils::daemon::{make_request, wait_for_daemon},
|
utils::daemon::{make_request, wait_for_daemon},
|
||||||
};
|
};
|
||||||
use std::{error::Error, path::PathBuf};
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
#[command(version, about, long_about = None)]
|
#[command(version, about, long_about = None)]
|
||||||
@@ -146,7 +147,7 @@ enum SetCommands {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
wait_for_daemon().await?;
|
wait_for_daemon().await?;
|
||||||
@@ -204,9 +205,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
let response = make_request(request)
|
let response = make_request(request).await.map_err(|e| anyhow!(e))?;
|
||||||
.await
|
|
||||||
.map_err(|e| e as Box<dyn Error>)?;
|
|
||||||
println!("{} : {}", response.status, response.message);
|
println!("{} : {}", response.status, response.message);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
+10
-5
@@ -1,3 +1,4 @@
|
|||||||
|
use anyhow::{Result, anyhow};
|
||||||
use pwsp::{
|
use pwsp::{
|
||||||
types::socket::{MAX_MESSAGE_SIZE, Request, Response},
|
types::socket::{MAX_MESSAGE_SIZE, Request, Response},
|
||||||
utils::{
|
utils::{
|
||||||
@@ -11,7 +12,7 @@ use pwsp::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
use std::{error::Error, fs, time::Duration};
|
use std::{fs, time::Duration};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::UnixListener,
|
net::UnixListener,
|
||||||
@@ -19,11 +20,11 @@ use tokio::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<()> {
|
||||||
create_runtime_dir()?;
|
create_runtime_dir()?;
|
||||||
|
|
||||||
if is_daemon_running()? {
|
if is_daemon_running()? {
|
||||||
return Err("Another instance is already running.".into());
|
return Err(anyhow!("Another instance is already running."));
|
||||||
}
|
}
|
||||||
|
|
||||||
get_daemon_config(); // Initialize daemon config
|
get_daemon_config(); // Initialize daemon config
|
||||||
@@ -38,7 +39,11 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
|
|
||||||
let runtime_dir = get_runtime_dir();
|
let runtime_dir = get_runtime_dir();
|
||||||
|
|
||||||
let lock_file = fs::File::create(runtime_dir.join("daemon.lock"))?;
|
let lock_file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(runtime_dir.join("daemon.lock"))?;
|
||||||
lock_file.lock()?;
|
lock_file.lock()?;
|
||||||
|
|
||||||
let socket_path = runtime_dir.join("daemon.sock");
|
let socket_path = runtime_dir.join("daemon.sock");
|
||||||
@@ -76,7 +81,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn commands_loop(listener: UnixListener) -> Result<(), Box<dyn Error>> {
|
async fn commands_loop(listener: UnixListener) -> Result<()> {
|
||||||
loop {
|
loop {
|
||||||
let (mut stream, _addr) = listener.accept().await?;
|
let (mut stream, _addr) = listener.accept().await?;
|
||||||
|
|
||||||
|
|||||||
-953
@@ -1,953 +0,0 @@
|
|||||||
use crate::gui::SoundpadGui;
|
|
||||||
use egui::{
|
|
||||||
Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Label,
|
|
||||||
Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2,
|
|
||||||
};
|
|
||||||
use egui_dnd::dnd;
|
|
||||||
use egui_extras::{Column, TableBuilder};
|
|
||||||
use egui_material_icons::icons::*;
|
|
||||||
use pwsp::types::socket::Request;
|
|
||||||
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
|
|
||||||
use pwsp::utils::gui::{format_time_pair, make_request_async};
|
|
||||||
use std::{path::Path, time::Instant};
|
|
||||||
|
|
||||||
enum TrackAction {
|
|
||||||
Pause(u32),
|
|
||||||
Resume(u32),
|
|
||||||
ToggleLoop(u32),
|
|
||||||
Stop(u32),
|
|
||||||
}
|
|
||||||
|
|
||||||
enum HotkeyAction {
|
|
||||||
Remove(String),
|
|
||||||
Capture(String),
|
|
||||||
ClearChord(String),
|
|
||||||
Play(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SoundpadGui {
|
|
||||||
fn get_volume_icon(volume: f32) -> &'static str {
|
|
||||||
if volume > 0.7 {
|
|
||||||
ICON_VOLUME_UP.codepoint
|
|
||||||
} else if volume <= 0.0 {
|
|
||||||
ICON_VOLUME_OFF.codepoint
|
|
||||||
} else if volume < 0.3 {
|
|
||||||
ICON_VOLUME_MUTE.codepoint
|
|
||||||
} else {
|
|
||||||
ICON_VOLUME_DOWN.codepoint
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw(&mut self, ui: &mut Ui) {
|
|
||||||
self.draw_header(ui);
|
|
||||||
self.draw_body(ui);
|
|
||||||
ui.separator();
|
|
||||||
self.draw_footer(ui);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
|
|
||||||
ui.centered_and_justified(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Waiting for PWSP daemon to start...")
|
|
||||||
.size(34.0)
|
|
||||||
.monospace(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_hotkey_capture(&mut self, ui: &mut Ui) {
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.add_space(ui.available_height() / 3.0);
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Press a key combination (e.g. Ctrl+Alt+1)")
|
|
||||||
.size(18.0)
|
|
||||||
.color(Color32::YELLOW)
|
|
||||||
.monospace(),
|
|
||||||
);
|
|
||||||
ui.add_space(10.0);
|
|
||||||
let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot {
|
|
||||||
format!("for slot '{}'", slot)
|
|
||||||
} else if let Some(path) = &self.app_state.assigning_hotkey_for_file {
|
|
||||||
format!(
|
|
||||||
"for '{}'",
|
|
||||||
path.file_name().unwrap_or_default().to_string_lossy()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
ui.label(RichText::new(target).size(16.0));
|
|
||||||
ui.add_space(10.0);
|
|
||||||
ui.label("Press Escape to cancel");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_settings(&mut self, ui: &mut Ui) {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.spacing_mut().item_spacing.y = 5.0;
|
|
||||||
// --------- Back Button and Title ----------
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
|
||||||
let back_button_response = ui.add(back_button);
|
|
||||||
if back_button_response.clicked() {
|
|
||||||
self.app_state.show_settings = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.add_space(ui.available_width() / 2.0 - 40.0);
|
|
||||||
|
|
||||||
ui.label(RichText::new("Settings").color(Color32::WHITE).monospace());
|
|
||||||
});
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(20.0);
|
|
||||||
|
|
||||||
// --------- Checkboxes ----------
|
|
||||||
let save_volume_response =
|
|
||||||
ui.checkbox(&mut self.config.save_volume, "Always remember volume");
|
|
||||||
let save_input_response =
|
|
||||||
ui.checkbox(&mut self.config.save_input, "Always remember microphone");
|
|
||||||
let save_scale_response = ui.checkbox(
|
|
||||||
&mut self.config.save_scale_factor,
|
|
||||||
"Always remember UI scale factor",
|
|
||||||
);
|
|
||||||
let pause_on_exit_response = ui.checkbox(
|
|
||||||
&mut self.config.pause_on_exit,
|
|
||||||
"Pause audio playback when the window is closed",
|
|
||||||
);
|
|
||||||
|
|
||||||
if save_volume_response.changed()
|
|
||||||
|| save_input_response.changed()
|
|
||||||
|| save_scale_response.changed()
|
|
||||||
|| pause_on_exit_response.changed()
|
|
||||||
{
|
|
||||||
self.config.save_to_file().ok();
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
|
||||||
ui.label(format!("GUI version: {}", env!("CARGO_PKG_VERSION")));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn draw_hotkeys(&mut self, ui: &mut Ui) {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.spacing_mut().item_spacing.y = 5.0;
|
|
||||||
|
|
||||||
self.draw_hotkeys_header(ui);
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
self.draw_hotkeys_search(ui);
|
|
||||||
ui.separator();
|
|
||||||
ui.add_space(5.0);
|
|
||||||
|
|
||||||
let action = self.draw_hotkeys_table(ui);
|
|
||||||
|
|
||||||
if let Some(action) = action {
|
|
||||||
self.handle_hotkey_action(action);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
|
||||||
if ui.add(back_button).clicked() {
|
|
||||||
self.app_state.show_hotkeys = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.vertical_centered(|ui| {
|
|
||||||
ui.label(RichText::new("Hotkeys").color(Color32::WHITE).monospace());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
ui.menu_button(format!("{} Add Command", ICON_ADD.codepoint), |ui| {
|
|
||||||
let mut selected_cmd = None;
|
|
||||||
if ui.button("Toggle Pause").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Stop Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_stop", Request::stop(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Pause Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_pause", Request::pause(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Resume Playback").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_resume", Request::resume(None)));
|
|
||||||
}
|
|
||||||
if ui.button("Toggle Loop").clicked() {
|
|
||||||
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((slot_name, req)) = selected_cmd {
|
|
||||||
make_request_async(Request::set_hotkey_action(slot_name, &req));
|
|
||||||
self.app_state
|
|
||||||
.hotkey_config
|
|
||||||
.set_slot(slot_name.to_string(), req);
|
|
||||||
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.add_space(10.0);
|
|
||||||
|
|
||||||
ui.add(
|
|
||||||
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
|
|
||||||
.hint_text("Search hotkeys...")
|
|
||||||
.desired_width(f32::INFINITY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
|
|
||||||
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
|
||||||
let conflict_slots: std::collections::HashSet<&str> =
|
|
||||||
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
|
||||||
|
|
||||||
let search = self.app_state.hotkey_search_query.to_lowercase();
|
|
||||||
let mut action: Option<HotkeyAction> = None;
|
|
||||||
|
|
||||||
let slots: Vec<_> = self
|
|
||||||
.app_state
|
|
||||||
.hotkey_config
|
|
||||||
.slots
|
|
||||||
.iter()
|
|
||||||
.filter(|s| {
|
|
||||||
if search.is_empty() {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
s.slot.to_lowercase().contains(&search)
|
|
||||||
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|
|
||||||
|| s.key_chord
|
|
||||||
.as_deref()
|
|
||||||
.unwrap_or("")
|
|
||||||
.to_lowercase()
|
|
||||||
.contains(&search)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let available_width = ui.available_width();
|
|
||||||
let col_width = (available_width / 4.0).max(80.0);
|
|
||||||
|
|
||||||
TableBuilder::new(ui)
|
|
||||||
.striped(true)
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Slot
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Sound / Action name
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Key Chord
|
|
||||||
.column(Column::exact(col_width).clip(true)) // Actions
|
|
||||||
.header(30.0, |mut header| {
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Slot")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Sound")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Key Chord")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
header.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("Actions")
|
|
||||||
.strong()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::LIGHT_GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.body(|mut body| {
|
|
||||||
if slots.is_empty() {
|
|
||||||
body.row(30.0, |mut row| {
|
|
||||||
row.col(|_| {});
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.label(
|
|
||||||
RichText::new("No hotkey slots configured.").color(Color32::GRAY),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
row.col(|_| {});
|
|
||||||
row.col(|_| {});
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for slot in &slots {
|
|
||||||
body.row(30.0, |mut row| {
|
|
||||||
// Column 1: Slot
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if conflict_slots.contains(slot.slot.as_str()) {
|
|
||||||
ui.label(
|
|
||||||
RichText::new(ICON_WARNING.codepoint)
|
|
||||||
.color(Color32::from_rgb(255, 165, 0)),
|
|
||||||
)
|
|
||||||
.on_hover_text("Key chord conflict");
|
|
||||||
}
|
|
||||||
ui.add(
|
|
||||||
Label::new(RichText::new(&slot.slot).monospace()).truncate(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 2: Sound / Action name
|
|
||||||
row.col(|ui| {
|
|
||||||
let action_name = match slot.action.name.as_str() {
|
|
||||||
"play" => {
|
|
||||||
if let Some(file_path_str) = slot.action.args.get("file_path") {
|
|
||||||
Path::new(file_path_str)
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
"Play".to_string()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"toggle_pause" => "Toggle Pause".to_string(),
|
|
||||||
"pause" => "Pause Playback".to_string(),
|
|
||||||
"resume" => "Resume Playback".to_string(),
|
|
||||||
"stop" => "Stop Playback".to_string(),
|
|
||||||
"toggle_loop" => "Toggle Loop".to_string(),
|
|
||||||
other => other.to_string(),
|
|
||||||
};
|
|
||||||
ui.add(Label::new(RichText::new(action_name).monospace()).truncate());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 3: Key Chord
|
|
||||||
row.col(|ui| {
|
|
||||||
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
|
|
||||||
ui.add(
|
|
||||||
Label::new(RichText::new(chord_text).monospace().color(
|
|
||||||
if slot.key_chord.is_some() {
|
|
||||||
Color32::from_rgb(100, 200, 100)
|
|
||||||
} else {
|
|
||||||
Color32::GRAY
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.truncate(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Column 4: Actions
|
|
||||||
row.col(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_DELETE).frame(false))
|
|
||||||
.on_hover_text("Remove slot")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Remove(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_KEYBOARD).frame(false))
|
|
||||||
.on_hover_text("Set key chord")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Capture(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if slot.key_chord.is_some()
|
|
||||||
&& ui
|
|
||||||
.add(Button::new(ICON_BACKSPACE).frame(false))
|
|
||||||
.on_hover_text("Clear key chord")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
if ui
|
|
||||||
.add(Button::new(ICON_PLAY_ARROW).frame(false))
|
|
||||||
.on_hover_text("Play")
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
action = Some(HotkeyAction::Play(slot.slot.clone()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
|
|
||||||
match action {
|
|
||||||
HotkeyAction::Remove(slot) => {
|
|
||||||
make_request_async(Request::clear_hotkey(&slot));
|
|
||||||
self.app_state.hotkey_config.remove_slot(&slot);
|
|
||||||
}
|
|
||||||
HotkeyAction::Capture(slot) => {
|
|
||||||
self.app_state.assigning_hotkey_slot = Some(slot);
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
}
|
|
||||||
HotkeyAction::ClearChord(slot) => {
|
|
||||||
make_request_async(Request::clear_hotkey_key(&slot));
|
|
||||||
self.app_state.hotkey_config.set_key_chord(&slot, None);
|
|
||||||
}
|
|
||||||
HotkeyAction::Play(slot) => {
|
|
||||||
self.play_hotkey_slot(&slot);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_header(&mut self, ui: &mut Ui) {
|
|
||||||
ui.vertical_centered_justified(|ui| {
|
|
||||||
if self.audio_player_state.tracks.is_empty() {
|
|
||||||
ui.label("No tracks playing");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut action = None;
|
|
||||||
|
|
||||||
for track in &self.audio_player_state.tracks {
|
|
||||||
CollapsingHeader::new(
|
|
||||||
RichText::new(
|
|
||||||
track
|
|
||||||
.path
|
|
||||||
.file_stem()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default(),
|
|
||||||
)
|
|
||||||
.color(Color32::WHITE)
|
|
||||||
.family(FontFamily::Monospace),
|
|
||||||
)
|
|
||||||
.default_open(true)
|
|
||||||
.show(ui, |ui| {
|
|
||||||
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, track) {
|
|
||||||
action = Some(act);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ui.separator();
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(action) = action {
|
|
||||||
match action {
|
|
||||||
TrackAction::Pause(id) => self.pause(Some(id)),
|
|
||||||
TrackAction::Resume(id) => self.resume(Some(id)),
|
|
||||||
TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)),
|
|
||||||
TrackAction::Stop(id) => self.stop(Some(id)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
|
||||||
let mut action = None;
|
|
||||||
|
|
||||||
let play_button = Button::new(if track.paused {
|
|
||||||
ICON_PLAY_ARROW
|
|
||||||
} else {
|
|
||||||
ICON_PAUSE
|
|
||||||
})
|
|
||||||
.corner_radius(15.0);
|
|
||||||
|
|
||||||
if ui.add_sized([30.0, 30.0], play_button).clicked() {
|
|
||||||
action = Some(if track.paused {
|
|
||||||
TrackAction::Resume(track.id)
|
|
||||||
} else {
|
|
||||||
TrackAction::Pause(track.id)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let loop_button = Button::new(
|
|
||||||
RichText::new(if track.looped {
|
|
||||||
ICON_REPEAT_ONE
|
|
||||||
} else {
|
|
||||||
ICON_REPEAT
|
|
||||||
})
|
|
||||||
.size(18.0),
|
|
||||||
)
|
|
||||||
.frame(false);
|
|
||||||
|
|
||||||
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
|
|
||||||
action = Some(TrackAction::ToggleLoop(track.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_position_control(
|
|
||||||
ui: &mut Ui,
|
|
||||||
ui_state: &mut pwsp::types::gui::TrackUiState,
|
|
||||||
track: &TrackInfo,
|
|
||||||
default_slider_width: f32,
|
|
||||||
) {
|
|
||||||
let duration = track.duration.unwrap_or(1.0);
|
|
||||||
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
let position_slider_width = ui.available_width()
|
|
||||||
- (30.0 * 3.0)
|
|
||||||
- default_slider_width
|
|
||||||
- (ui.spacing().item_spacing.x * 6.0);
|
|
||||||
|
|
||||||
ui.spacing_mut().slider_width = position_slider_width;
|
|
||||||
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
|
|
||||||
ui_state.position_dragged = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
let time_label =
|
|
||||||
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
|
||||||
ui.add_sized([30.0, 30.0], time_label);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_volume_control(
|
|
||||||
ui: &mut Ui,
|
|
||||||
ui_state: &mut pwsp::types::gui::TrackUiState,
|
|
||||||
track: &TrackInfo,
|
|
||||||
default_slider_width: f32,
|
|
||||||
) {
|
|
||||||
let volume_icon = Self::get_volume_icon(track.volume);
|
|
||||||
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
|
||||||
ui.add_sized([30.0, 30.0], volume_label)
|
|
||||||
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
|
||||||
|
|
||||||
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
|
|
||||||
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
|
||||||
ui.spacing_mut().item_spacing.x = 0.0;
|
|
||||||
|
|
||||||
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
|
|
||||||
ui_state.volume_dragged = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
|
||||||
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
|
||||||
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
|
|
||||||
Some(TrackAction::Stop(track.id))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_track_control(
|
|
||||||
ui: &mut Ui,
|
|
||||||
app_state: &mut AppState,
|
|
||||||
track: &TrackInfo,
|
|
||||||
) -> Option<TrackAction> {
|
|
||||||
let ui_state = app_state.track_ui_states.entry(track.id).or_default();
|
|
||||||
|
|
||||||
let should_update_position = !ui_state.position_dragged
|
|
||||||
&& ui_state
|
|
||||||
.ignore_position_update_until
|
|
||||||
.map(|t| Instant::now() > t)
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if should_update_position {
|
|
||||||
ui_state.position_slider_value = track.position;
|
|
||||||
}
|
|
||||||
|
|
||||||
let should_update_volume = !ui_state.volume_dragged
|
|
||||||
&& ui_state
|
|
||||||
.ignore_volume_update_until
|
|
||||||
.map(|t| Instant::now() > t)
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if should_update_volume {
|
|
||||||
ui_state.volume_slider_value = track.volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut action = None;
|
|
||||||
|
|
||||||
ui.horizontal_top(|ui| {
|
|
||||||
if let Some(act) = Self::draw_playback_controls(ui, track) {
|
|
||||||
action = Some(act);
|
|
||||||
}
|
|
||||||
|
|
||||||
let default_slider_width = ui.spacing().slider_width;
|
|
||||||
Self::draw_position_control(ui, ui_state, track, default_slider_width);
|
|
||||||
Self::draw_volume_control(ui, ui_state, track, default_slider_width);
|
|
||||||
|
|
||||||
if let Some(act) = Self::draw_stop_control(ui, track) {
|
|
||||||
action = Some(act);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_body(&mut self, ui: &mut Ui) {
|
|
||||||
let left_panel_width = self
|
|
||||||
.config
|
|
||||||
.left_panel_width
|
|
||||||
.max(100.0)
|
|
||||||
.min(ui.available_width() - 100.0);
|
|
||||||
let dirs_size = Vec2::new(left_panel_width, ui.available_height() - 40.0);
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
self.draw_dirs(ui, dirs_size);
|
|
||||||
|
|
||||||
let (rect, response) = ui.allocate_at_least(
|
|
||||||
Vec2::new(ui.spacing().item_spacing.x, ui.available_height()),
|
|
||||||
Sense::click_and_drag(),
|
|
||||||
);
|
|
||||||
|
|
||||||
if ui.is_rect_visible(rect) {
|
|
||||||
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
|
||||||
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
|
|
||||||
}
|
|
||||||
|
|
||||||
let vertical_separator_response =
|
|
||||||
response.on_hover_and_drag_cursor(CursorIcon::ResizeHorizontal);
|
|
||||||
|
|
||||||
if vertical_separator_response.dragged() {
|
|
||||||
self.config.left_panel_width += vertical_separator_response.drag_delta().x;
|
|
||||||
self.config.left_panel_width = self.config.left_panel_width.clamp(100.0, 500.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if vertical_separator_response.drag_stopped() {
|
|
||||||
self.config.save_to_file().ok();
|
|
||||||
}
|
|
||||||
|
|
||||||
let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0);
|
|
||||||
self.draw_files(ui, files_size);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_dirs(&mut self, ui: &mut Ui, area_size: Vec2) {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.set_min_width(area_size.x);
|
|
||||||
ui.set_min_height(area_size.y);
|
|
||||||
|
|
||||||
ScrollArea::vertical().id_salt(0).show(ui, |ui| {
|
|
||||||
ui.set_min_width(area_size.x);
|
|
||||||
|
|
||||||
let mut dirs = self.app_state.dirs.clone();
|
|
||||||
|
|
||||||
dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
|
|
||||||
let path = item.clone();
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
handle.ui(ui, |ui| {
|
|
||||||
ui.label(ICON_DRAG_INDICATOR.codepoint);
|
|
||||||
});
|
|
||||||
let name = path
|
|
||||||
.file_name()
|
|
||||||
.map(|s| s.to_string_lossy().to_string())
|
|
||||||
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
|
||||||
|
|
||||||
let mut dir_button_text = RichText::new(name.clone());
|
|
||||||
if let Some(current_dir) = &self.app_state.current_dir
|
|
||||||
&& current_dir.eq(&path)
|
|
||||||
{
|
|
||||||
dir_button_text = dir_button_text.color(Color32::WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let dir_button =
|
|
||||||
Button::new(dir_button_text.atom_max_width(area_size.x)).frame(false);
|
|
||||||
|
|
||||||
let dir_button_response = ui.add(dir_button);
|
|
||||||
if dir_button_response.clicked() {
|
|
||||||
self.open_dir(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
let delete_dir_button = Button::new(ICON_DELETE).frame(false);
|
|
||||||
let delete_dir_button_response =
|
|
||||||
ui.add_sized([18.0, 18.0], delete_dir_button);
|
|
||||||
if delete_dir_button_response.clicked() {
|
|
||||||
self.app_state.dirs_to_remove.insert(path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context menu
|
|
||||||
dir_button_response.context_menu(|ui| {
|
|
||||||
if ui
|
|
||||||
.button(format!("{} {}", ICON_OPEN_IN_NEW.codepoint, "Show"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.open_dir(&path);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_OPEN_IN_BROWSER.codepoint, "Open in File Manager"
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
&& let Err(e) = opener::open(&path)
|
|
||||||
{
|
|
||||||
eprintln!("Failed to open file manager: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!("{} {}", ICON_DELETE.codepoint, "Remove"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.app_state.dirs_to_remove.insert(path.clone());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
self.app_state.dirs = dirs;
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let add_dirs_button = Button::new(ICON_ADD).frame(false);
|
|
||||||
let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button);
|
|
||||||
if add_dirs_button_response.clicked() {
|
|
||||||
self.add_dirs();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
|
||||||
let play_file_button = Button::new("Play file");
|
|
||||||
let play_file_button_response = ui.add(play_file_button);
|
|
||||||
if play_file_button_response.clicked() {
|
|
||||||
self.open_file();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
let search_field_response = ui.add_sized(
|
|
||||||
[ui.available_width(), 22.0],
|
|
||||||
TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."),
|
|
||||||
);
|
|
||||||
|
|
||||||
if self.app_state.force_focus_search {
|
|
||||||
search_field_response.request_focus();
|
|
||||||
self.app_state.force_focus_search = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
self.app_state.search_field_id = Some(search_field_response.id);
|
|
||||||
});
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
|
|
||||||
ui.set_min_width(area_size.x);
|
|
||||||
ui.set_min_height(area_size.y);
|
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
|
||||||
let files = self.get_filtered_files();
|
|
||||||
|
|
||||||
for entry_path in files {
|
|
||||||
let file_name = entry_path
|
|
||||||
.file_name()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
// Hotkey badge
|
|
||||||
let hotkey_badge = self.get_hotkey_badge(&entry_path);
|
|
||||||
if let Some(badge) = &hotkey_badge {
|
|
||||||
ui.label(
|
|
||||||
RichText::new(badge)
|
|
||||||
.small()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::from_rgb(100, 200, 100)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut file_button_text = RichText::new(&file_name);
|
|
||||||
if let Some(current_file) = &self.app_state.selected_file
|
|
||||||
&& current_file.eq(&entry_path)
|
|
||||||
{
|
|
||||||
file_button_text = file_button_text.color(Color32::WHITE);
|
|
||||||
}
|
|
||||||
|
|
||||||
let file_button = Button::new(file_button_text).frame(false).truncate();
|
|
||||||
let file_button_response = ui.add(file_button);
|
|
||||||
if file_button_response.clicked() {
|
|
||||||
ui.input(|i| {
|
|
||||||
if i.modifiers.ctrl {
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
} else if i.modifiers.shift
|
|
||||||
&& let Some(last_track) =
|
|
||||||
self.audio_player_state.tracks.last()
|
|
||||||
{
|
|
||||||
self.stop(Some(last_track.id));
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
} else {
|
|
||||||
self.play_file(&entry_path, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Context menu
|
|
||||||
file_button_response.context_menu(|ui| {
|
|
||||||
if ui
|
|
||||||
.button(format!("{} {}", ICON_BOLT.codepoint, "Play Solo"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.play_file(&entry_path, false);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!("{} {}", ICON_ADD.codepoint, "Add New"))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_SWAP_HORIZ.codepoint, "Replace Last"
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
|
||||||
{
|
|
||||||
self.stop(Some(last_track.id));
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_OPEN_IN_BROWSER.codepoint, "Show in File Manager"
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
&& let Err(e) = opener::reveal(&entry_path)
|
|
||||||
{
|
|
||||||
eprintln!("Failed to open file manager: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_KEYBOARD.codepoint, "Assign Hotkey"
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.app_state.assigning_hotkey_for_file =
|
|
||||||
Some(entry_path.clone());
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_hotkey_badge(&self, path: &Path) -> Option<String> {
|
|
||||||
for slot in &self.app_state.hotkey_config.slots {
|
|
||||||
if slot.action.name == "play"
|
|
||||||
&& let Some(file_path_str) = slot.action.args.get("file_path")
|
|
||||||
&& Path::new(file_path_str) == path
|
|
||||||
{
|
|
||||||
if let Some(chord) = &slot.key_chord {
|
|
||||||
return Some(format!("[{}]", chord));
|
|
||||||
} else {
|
|
||||||
return Some(format!("[{}]", slot.slot));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn draw_footer(&mut self, ui: &mut Ui) {
|
|
||||||
ui.add_space(5.0);
|
|
||||||
ui.horizontal(|ui| {
|
|
||||||
// ---------- Microphone selection ----------
|
|
||||||
let mics = &self.audio_player_state.all_inputs_sorted;
|
|
||||||
|
|
||||||
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
|
||||||
let prev_input = selected_input.to_owned();
|
|
||||||
ComboBox::from_label("Choose microphone")
|
|
||||||
.height(30.0)
|
|
||||||
.selected_text(
|
|
||||||
self.audio_player_state
|
|
||||||
.all_inputs
|
|
||||||
.get(&selected_input)
|
|
||||||
.unwrap_or(&String::new()),
|
|
||||||
)
|
|
||||||
.show_ui(ui, |ui| {
|
|
||||||
for (name, nick) in mics {
|
|
||||||
ui.selectable_value(&mut selected_input, name.clone(), nick);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if selected_input != prev_input {
|
|
||||||
self.set_input(selected_input);
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Master Volume Slider ----------
|
|
||||||
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
|
|
||||||
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
|
||||||
ui.add_sized([18.0, 18.0], volume_label)
|
|
||||||
.on_hover_text(format!(
|
|
||||||
"Master Volume: {:.0}%",
|
|
||||||
self.audio_player_state.volume * 100.0
|
|
||||||
));
|
|
||||||
|
|
||||||
let should_update_volume = !self.app_state.volume_dragged
|
|
||||||
&& self
|
|
||||||
.app_state
|
|
||||||
.ignore_volume_update_until
|
|
||||||
.map(|t| Instant::now() > t)
|
|
||||||
.unwrap_or(true);
|
|
||||||
|
|
||||||
if should_update_volume {
|
|
||||||
self.app_state.volume_slider_value = self.audio_player_state.volume;
|
|
||||||
}
|
|
||||||
|
|
||||||
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
|
|
||||||
.show_value(false)
|
|
||||||
.step_by(0.01);
|
|
||||||
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
|
|
||||||
if volume_slider_response.drag_stopped() {
|
|
||||||
self.app_state.volume_dragged = true;
|
|
||||||
}
|
|
||||||
// ------------------------------------------
|
|
||||||
|
|
||||||
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
|
|
||||||
|
|
||||||
// ---------- Hotkeys button ----------
|
|
||||||
let hotkeys_button =
|
|
||||||
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
|
||||||
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
|
|
||||||
if hotkeys_button_response.clicked() {
|
|
||||||
self.app_state.show_hotkeys = true;
|
|
||||||
}
|
|
||||||
hotkeys_button_response.on_hover_text("Hotkeys (H)");
|
|
||||||
// --------------------------------
|
|
||||||
|
|
||||||
// ---------- Settings button ----------
|
|
||||||
let settings_button =
|
|
||||||
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
|
||||||
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
|
|
||||||
if settings_button_response.clicked() {
|
|
||||||
self.app_state.show_settings = true;
|
|
||||||
}
|
|
||||||
// --------------------------------
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+1
-71
@@ -3,8 +3,6 @@ use egui::{Context, Id, Key, Modifiers};
|
|||||||
use pwsp::types::socket::Request;
|
use pwsp::types::socket::Request;
|
||||||
use pwsp::utils::gui::make_request_async;
|
use pwsp::utils::gui::make_request_async;
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
|
/// Convert an egui Key + Modifiers to a normalized chord string like "Ctrl+Shift+A".
|
||||||
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
fn chord_from_event(modifiers: &Modifiers, key: &Key) -> Option<String> {
|
||||||
let key_name = key.name();
|
let key_name = key.name();
|
||||||
@@ -94,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
|
||||||
@@ -197,74 +195,6 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Play selected file on Enter
|
|
||||||
if self.key_pressed(ctx, Key::Enter)
|
|
||||||
&& let Some(path) = self.app_state.selected_file.clone()
|
|
||||||
{
|
|
||||||
if modifiers.ctrl {
|
|
||||||
self.play_file(&path, true);
|
|
||||||
} else if modifiers.shift
|
|
||||||
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
|
||||||
{
|
|
||||||
self.stop(Some(last_track.id));
|
|
||||||
self.play_file(&path, true);
|
|
||||||
} else {
|
|
||||||
self.play_file(&path, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Iterate through dirs and files with Ctrl + Up/Down
|
|
||||||
let arrow_up_pressed = self.key_pressed(ctx, Key::ArrowUp);
|
|
||||||
let arrow_down_pressed = self.key_pressed(ctx, Key::ArrowDown);
|
|
||||||
if modifiers.ctrl && (arrow_up_pressed || arrow_down_pressed) {
|
|
||||||
if modifiers.shift && !self.app_state.dirs.is_empty() {
|
|
||||||
let mut dirs: Vec<PathBuf> = self.app_state.dirs.to_vec();
|
|
||||||
dirs.sort();
|
|
||||||
|
|
||||||
let current_dir_index = self
|
|
||||||
.app_state
|
|
||||||
.current_dir
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|cd| dirs.iter().position(|x| x == cd));
|
|
||||||
|
|
||||||
let new_dir_index =
|
|
||||||
match (current_dir_index, arrow_up_pressed, arrow_down_pressed) {
|
|
||||||
(Some(i), true, false) => (i + dirs.len() - 1) % dirs.len(),
|
|
||||||
(Some(i), false, true) => (i + 1) % dirs.len(),
|
|
||||||
(Some(i), true, true) => i,
|
|
||||||
(None, true, _) => dirs.len() - 1,
|
|
||||||
(None, false, true) => 0,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.open_dir(&dirs[new_dir_index]);
|
|
||||||
} else if self.app_state.current_dir.is_some() {
|
|
||||||
let files = self.get_filtered_files();
|
|
||||||
|
|
||||||
if files.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let current_files_index = self
|
|
||||||
.app_state
|
|
||||||
.selected_file
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|f| files.iter().position(|x| x == f));
|
|
||||||
|
|
||||||
let new_files_index =
|
|
||||||
match (current_files_index, arrow_up_pressed, arrow_down_pressed) {
|
|
||||||
(Some(i), true, false) => (i + files.len() - 1) % files.len(),
|
|
||||||
(Some(i), false, true) => (i + 1) % files.len(),
|
|
||||||
(Some(i), true, true) => i,
|
|
||||||
(None, true, _) => files.len() - 1,
|
|
||||||
(None, false, true) => 0,
|
|
||||||
_ => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
self.app_state.selected_file = Some(files[new_files_index].clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for hotkey chord triggers
|
// Check for hotkey chord triggers
|
||||||
let slots_to_play: Vec<String> = ctx.input(|i| {
|
let slots_to_play: Vec<String> = ctx.input(|i| {
|
||||||
let mut result = vec![];
|
let mut result = vec![];
|
||||||
|
|||||||
+24
-16
@@ -1,7 +1,8 @@
|
|||||||
mod draw;
|
|
||||||
mod input;
|
mod input;
|
||||||
mod update;
|
mod update;
|
||||||
|
mod views;
|
||||||
|
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
|
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
|
||||||
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
|
use egui::{Context, FontData, FontDefinitions, FontFamily, FontTweak, Vec2, ViewportBuilder};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@@ -20,7 +21,7 @@ use pwsp::{
|
|||||||
};
|
};
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
cmp::Ordering,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -110,14 +111,14 @@ impl SoundpadGui {
|
|||||||
self.app_state.current_dir = Some(path.clone());
|
self.app_state.current_dir = Some(path.clone());
|
||||||
match path.read_dir() {
|
match path.read_dir() {
|
||||||
Ok(read_dir) => {
|
Ok(read_dir) => {
|
||||||
self.app_state.files = read_dir
|
self.app_state.listed_files = read_dir
|
||||||
.filter_map(|res| res.ok())
|
.filter_map(|res| res.ok())
|
||||||
.map(|entry| entry.path())
|
.map(|entry| entry.path())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to read directory {:?}: {}", path, e);
|
eprintln!("Failed to read directory {:?}: {}", path, e);
|
||||||
self.app_state.files.clear();
|
self.app_state.listed_files.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,8 +158,18 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
||||||
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
|
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
||||||
files.sort();
|
files.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 {
|
||||||
|
a.cmp(b)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let search_query = self.app_state.search_query.to_lowercase();
|
let search_query = self.app_state.search_query.to_lowercase();
|
||||||
let search_query = search_query.trim();
|
let search_query = search_query.trim();
|
||||||
@@ -167,7 +178,7 @@ impl SoundpadGui {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|entry_path| {
|
.filter(|entry_path| {
|
||||||
if entry_path.is_dir() {
|
if entry_path.is_dir() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !SUPPORTED_EXTENSIONS.contains(
|
if !SUPPORTED_EXTENSIONS.contains(
|
||||||
@@ -198,11 +209,7 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_font(
|
fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
|
||||||
font_name: &str,
|
|
||||||
font_bytes: &[u8],
|
|
||||||
fonts: &mut FontDefinitions,
|
|
||||||
) -> Result<(), Box<dyn Error>> {
|
|
||||||
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
|
let font_data = FontData::from_owned(font_bytes.to_vec()).tweak(FontTweak {
|
||||||
scale: 1.0,
|
scale: 1.0,
|
||||||
hinting_override: Some(true),
|
hinting_override: Some(true),
|
||||||
@@ -227,12 +234,13 @@ fn add_font(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<(), Box<dyn Error>> {
|
fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<()> {
|
||||||
let (_, en_sans) = find_for_locale("en", FontStyle::Sans);
|
let (_, en_sans) = find_for_locale("en", FontStyle::Sans);
|
||||||
let (_, en_serif) = find_for_locale("en", FontStyle::Serif);
|
let (_, en_serif) = find_for_locale("en", FontStyle::Serif);
|
||||||
let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans);
|
let (_, ja_sans) = find_for_locale("ja", FontStyle::Sans);
|
||||||
|
let (_, ar_sans) = find_for_locale("ar", FontStyle::Sans);
|
||||||
|
|
||||||
let system_fonts = [en_sans, en_serif, ja_sans].concat();
|
let system_fonts = [en_sans, en_serif, ja_sans, ar_sans].concat();
|
||||||
|
|
||||||
for font in system_fonts.iter().rev() {
|
for font in system_fonts.iter().rev() {
|
||||||
let font_bytes = match &font.source {
|
let font_bytes = match &font.source {
|
||||||
@@ -246,7 +254,7 @@ fn load_system_fonts(fonts: &mut FontDefinitions) -> Result<(), Box<dyn Error>>
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn run() -> Result<(), Box<dyn Error>> {
|
pub async fn run() -> Result<()> {
|
||||||
const ICON: &[u8] = include_bytes!("../../assets/icon.png");
|
const ICON: &[u8] = include_bytes!("../../assets/icon.png");
|
||||||
|
|
||||||
let options = NativeOptions {
|
let options = NativeOptions {
|
||||||
@@ -283,6 +291,6 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Err(e) => Err(e.into()),
|
Err(e) => Err(anyhow!(e.to_string())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ impl App for SoundpadGui {
|
|||||||
&& current_dir == &path
|
&& current_dir == &path
|
||||||
{
|
{
|
||||||
self.app_state.current_dir = None;
|
self.app_state.current_dir = None;
|
||||||
self.app_state.files.clear();
|
self.app_state.listed_files.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{
|
||||||
|
Align, AtomExt, Button, CollapsingHeader, Color32, CursorIcon, Layout, RichText, ScrollArea,
|
||||||
|
Sense, TextEdit, Ui, Vec2,
|
||||||
|
};
|
||||||
|
use egui_dnd::dnd;
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::{gui::AppState, gui::AudioPlayerState};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::{cmp::Ordering, path::Path, path::PathBuf};
|
||||||
|
|
||||||
|
pub(crate) enum FileAction {
|
||||||
|
Play(PathBuf, bool),
|
||||||
|
StopAndPlay(u32, PathBuf, bool),
|
||||||
|
AssignHotkey(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_body(&mut self, ui: &mut Ui) {
|
||||||
|
let left_panel_width = self
|
||||||
|
.config
|
||||||
|
.left_panel_width
|
||||||
|
.max(100.0)
|
||||||
|
.min(ui.available_width() - 100.0);
|
||||||
|
let dirs_size = Vec2::new(left_panel_width, ui.available_height() - 40.0);
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
self.draw_dirs(ui, dirs_size);
|
||||||
|
|
||||||
|
let (rect, response) = ui.allocate_at_least(
|
||||||
|
Vec2::new(ui.spacing().item_spacing.x, ui.available_height()),
|
||||||
|
Sense::click_and_drag(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ui.is_rect_visible(rect) {
|
||||||
|
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
|
||||||
|
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
|
||||||
|
}
|
||||||
|
|
||||||
|
let vertical_separator_response =
|
||||||
|
response.on_hover_and_drag_cursor(CursorIcon::ResizeHorizontal);
|
||||||
|
|
||||||
|
if vertical_separator_response.dragged() {
|
||||||
|
self.config.left_panel_width += vertical_separator_response.drag_delta().x;
|
||||||
|
self.config.left_panel_width = self.config.left_panel_width.clamp(100.0, 500.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if vertical_separator_response.drag_stopped() {
|
||||||
|
self.config.save_to_file().ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0);
|
||||||
|
self.draw_files(ui, files_size);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_dirs(&mut self, ui: &mut Ui, area_size: Vec2) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.set_min_width(area_size.x);
|
||||||
|
ui.set_min_height(area_size.y);
|
||||||
|
|
||||||
|
ScrollArea::vertical().id_salt(0).show(ui, |ui| {
|
||||||
|
ui.set_min_width(area_size.x);
|
||||||
|
|
||||||
|
let mut dirs = std::mem::take(&mut self.app_state.dirs);
|
||||||
|
let mut dir_to_open = None;
|
||||||
|
|
||||||
|
dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
|
||||||
|
let path = item;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
handle.ui(ui, |ui| {
|
||||||
|
ui.label(ICON_DRAG_INDICATOR.codepoint);
|
||||||
|
});
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
|
.unwrap_or_else(|| path.to_string_lossy().to_string());
|
||||||
|
|
||||||
|
let mut dir_button_text = RichText::new(name.clone());
|
||||||
|
if let Some(current_dir) = &self.app_state.current_dir
|
||||||
|
&& current_dir.eq(&*path)
|
||||||
|
{
|
||||||
|
dir_button_text = dir_button_text.color(Color32::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_button =
|
||||||
|
Button::new(dir_button_text.atom_max_width(area_size.x)).frame(false);
|
||||||
|
|
||||||
|
let dir_button_response = ui.add(dir_button);
|
||||||
|
if dir_button_response.clicked() {
|
||||||
|
dir_to_open = Some(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let delete_dir_button = Button::new(ICON_DELETE).frame(false);
|
||||||
|
let delete_dir_button_response =
|
||||||
|
ui.add_sized([18.0, 18.0], delete_dir_button);
|
||||||
|
if delete_dir_button_response.clicked() {
|
||||||
|
self.app_state.dirs_to_remove.insert(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
dir_button_response.context_menu(|ui| {
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_NEW.codepoint,
|
||||||
|
t!("gui.context.dirs.open")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
dir_to_open = Some(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_BROWSER.codepoint,
|
||||||
|
t!("gui.context.dirs.open_in_fm")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Err(e) = opener::open(&path)
|
||||||
|
{
|
||||||
|
eprintln!("Failed to open file manager: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_DELETE.codepoint,
|
||||||
|
t!("gui.context.dirs.remove")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.app_state.dirs_to_remove.insert(path.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.app_state.dirs = dirs;
|
||||||
|
|
||||||
|
if let Some(path) = dir_to_open {
|
||||||
|
self.open_dir(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let add_dirs_button = Button::new(ICON_ADD).frame(false);
|
||||||
|
let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button);
|
||||||
|
if add_dirs_button_response.clicked() {
|
||||||
|
self.add_dirs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||||
|
let play_file_button = Button::new(t!("gui.play_file_button"));
|
||||||
|
let play_file_button_response = ui.add(play_file_button);
|
||||||
|
if play_file_button_response.clicked() {
|
||||||
|
self.open_file();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files_search_field(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let search_field_response = ui.add_sized(
|
||||||
|
[ui.available_width(), 22.0],
|
||||||
|
TextEdit::singleline(&mut self.app_state.search_query)
|
||||||
|
.hint_text(t!("gui.search_placeholder")),
|
||||||
|
);
|
||||||
|
|
||||||
|
if self.app_state.force_focus_search {
|
||||||
|
search_field_response.request_focus();
|
||||||
|
self.app_state.force_focus_search = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.app_state.search_field_id = Some(search_field_response.id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files_list(&mut self, ui: &mut Ui, area_size: Vec2) {
|
||||||
|
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
|
||||||
|
ui.set_min_width(area_size.x);
|
||||||
|
ui.set_min_height(area_size.y);
|
||||||
|
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
let files = self.get_filtered_files();
|
||||||
|
for entry_path in files {
|
||||||
|
Self::draw_tree_node(
|
||||||
|
ui,
|
||||||
|
entry_path,
|
||||||
|
&mut self.app_state,
|
||||||
|
&self.audio_player_state,
|
||||||
|
&mut actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
match action {
|
||||||
|
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent),
|
||||||
|
FileAction::StopAndPlay(id, path, concurrent) => {
|
||||||
|
self.stop(Some(id));
|
||||||
|
self.play_file(&path, concurrent);
|
||||||
|
}
|
||||||
|
FileAction::AssignHotkey(path) => {
|
||||||
|
self.app_state.assigning_hotkey_for_file = Some(path);
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
self.draw_files_search_field(ui);
|
||||||
|
ui.separator();
|
||||||
|
self.draw_files_list(ui, area_size);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node_dir(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
let dir_name = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.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()) {
|
||||||
|
read.push(entry.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 {
|
||||||
|
a.cmp(b)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app_state.dir_cache.insert(path.clone(), read.clone());
|
||||||
|
read
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_query = app_state.search_query.to_lowercase();
|
||||||
|
let search_query = search_query.trim();
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
if !child.is_dir() {
|
||||||
|
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
|
||||||
|
&child
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !search_query.is_empty() {
|
||||||
|
let file_name = child
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
if !file_name.to_lowercase().contains(search_query) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node_file(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Hotkey badge
|
||||||
|
let mut hotkey_badge = None;
|
||||||
|
for slot in &app_state.hotkey_config.slots {
|
||||||
|
if slot.action.name == "play"
|
||||||
|
&& let Some(file_path_str) = slot.action.args.get("file_path")
|
||||||
|
&& Path::new(file_path_str) == path
|
||||||
|
{
|
||||||
|
if let Some(chord) = &slot.key_chord {
|
||||||
|
hotkey_badge = Some(format!("[{}]", chord));
|
||||||
|
} else {
|
||||||
|
hotkey_badge = Some(format!("[{}]", slot.slot));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(badge) = &hotkey_badge {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(badge)
|
||||||
|
.small()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::from_rgb(100, 200, 100)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_button_text = RichText::new(&file_name);
|
||||||
|
|
||||||
|
let file_button = Button::new(file_button_text).frame(false).truncate();
|
||||||
|
let file_button_response = ui.add(file_button);
|
||||||
|
if file_button_response.clicked() {
|
||||||
|
ui.input(|i| {
|
||||||
|
if i.modifiers.ctrl {
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
} else if i.modifiers.shift
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
|
||||||
|
} else {
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
file_button_response.context_menu(|ui| {
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_BOLT.codepoint,
|
||||||
|
t!("gui.context.files.play_solo")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_ADD.codepoint,
|
||||||
|
t!("gui.context.files.add_new")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_SWAP_HORIZ.codepoint,
|
||||||
|
t!("gui.context.files.replace_last")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_BROWSER.codepoint,
|
||||||
|
t!("gui.context.files.show_in_fm")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Err(e) = opener::reveal(&path)
|
||||||
|
{
|
||||||
|
eprintln!("Failed to open file manager: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_KEYBOARD.codepoint,
|
||||||
|
t!("gui.context.files.asign_hotkey")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::AssignHotkey(path.clone()));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
if path.is_dir() {
|
||||||
|
Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions);
|
||||||
|
} else {
|
||||||
|
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{AtomExt, Button, ComboBox, Label, RichText, Slider, Ui, Vec2};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_footer(&mut self, ui: &mut Ui) {
|
||||||
|
ui.add_space(5.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// ---------- Microphone selection ----------
|
||||||
|
let mics = &self.audio_player_state.all_inputs_sorted;
|
||||||
|
|
||||||
|
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
||||||
|
let prev_input = selected_input.to_owned();
|
||||||
|
ComboBox::from_label(t!("gui.choose_mic_select"))
|
||||||
|
.height(30.0)
|
||||||
|
.selected_text(
|
||||||
|
self.audio_player_state
|
||||||
|
.all_inputs
|
||||||
|
.get(&selected_input)
|
||||||
|
.unwrap_or(&String::new()),
|
||||||
|
)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (name, nick) in mics {
|
||||||
|
ui.selectable_value(&mut selected_input, name.clone(), nick);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if selected_input != prev_input {
|
||||||
|
self.set_input(selected_input);
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
// ---------- Master Volume Slider ----------
|
||||||
|
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([18.0, 18.0], volume_label)
|
||||||
|
.on_hover_text(format!(
|
||||||
|
"Master Volume: {:.0}%",
|
||||||
|
self.audio_player_state.volume * 100.0
|
||||||
|
));
|
||||||
|
|
||||||
|
let should_update_volume = !self.app_state.volume_dragged
|
||||||
|
&& self
|
||||||
|
.app_state
|
||||||
|
.ignore_volume_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_volume {
|
||||||
|
self.app_state.volume_slider_value = self.audio_player_state.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
|
||||||
|
if volume_slider_response.drag_stopped() {
|
||||||
|
self.app_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
|
||||||
|
|
||||||
|
// ---------- Hotkeys button ----------
|
||||||
|
let hotkeys_button =
|
||||||
|
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
||||||
|
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
|
||||||
|
if hotkeys_button_response.clicked() {
|
||||||
|
self.app_state.show_hotkeys = true;
|
||||||
|
}
|
||||||
|
hotkeys_button_response.on_hover_text("Hotkeys (H)");
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
// ---------- Settings button ----------
|
||||||
|
let settings_button =
|
||||||
|
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
||||||
|
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
|
||||||
|
if settings_button_response.clicked() {
|
||||||
|
self.app_state.show_settings = true;
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Button, CollapsingHeader, Color32, FontFamily, Label, RichText, Slider, Ui};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
|
||||||
|
use pwsp::utils::gui::format_time_pair;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub(crate) enum TrackAction {
|
||||||
|
Pause(u32),
|
||||||
|
Resume(u32),
|
||||||
|
ToggleLoop(u32),
|
||||||
|
Stop(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_header(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
if self.audio_player_state.tracks.is_empty() {
|
||||||
|
ui.label("No tracks playing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
for track in &self.audio_player_state.tracks {
|
||||||
|
CollapsingHeader::new(
|
||||||
|
RichText::new(
|
||||||
|
track
|
||||||
|
.path
|
||||||
|
.file_stem()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.family(FontFamily::Monospace),
|
||||||
|
)
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
match action {
|
||||||
|
TrackAction::Pause(id) => self.pause(Some(id)),
|
||||||
|
TrackAction::Resume(id) => self.resume(Some(id)),
|
||||||
|
TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)),
|
||||||
|
TrackAction::Stop(id) => self.stop(Some(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
let play_button = Button::new(if track.paused {
|
||||||
|
ICON_PLAY_ARROW
|
||||||
|
} else {
|
||||||
|
ICON_PAUSE
|
||||||
|
})
|
||||||
|
.corner_radius(15.0);
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], play_button).clicked() {
|
||||||
|
action = Some(if track.paused {
|
||||||
|
TrackAction::Resume(track.id)
|
||||||
|
} else {
|
||||||
|
TrackAction::Pause(track.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let loop_button = Button::new(
|
||||||
|
RichText::new(if track.looped {
|
||||||
|
ICON_REPEAT_ONE
|
||||||
|
} else {
|
||||||
|
ICON_REPEAT
|
||||||
|
})
|
||||||
|
.size(18.0),
|
||||||
|
)
|
||||||
|
.frame(false);
|
||||||
|
|
||||||
|
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
|
||||||
|
action = Some(TrackAction::ToggleLoop(track.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_position_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let duration = track.duration.unwrap_or(1.0);
|
||||||
|
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
let position_slider_width = ui.available_width()
|
||||||
|
- (30.0 * 3.0)
|
||||||
|
- default_slider_width
|
||||||
|
- (ui.spacing().item_spacing.x * 6.0);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = position_slider_width;
|
||||||
|
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
|
||||||
|
ui_state.position_dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_label =
|
||||||
|
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
||||||
|
ui.add_sized([30.0, 30.0], time_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_volume_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let volume_icon = Self::get_volume_icon(track.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([30.0, 30.0], volume_label)
|
||||||
|
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
|
||||||
|
ui_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
||||||
|
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
|
||||||
|
Some(TrackAction::Stop(track.id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_track_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
) -> Option<TrackAction> {
|
||||||
|
let ui_state = app_state.track_ui_states.entry(track.id).or_default();
|
||||||
|
|
||||||
|
let should_update_position = !ui_state.position_dragged
|
||||||
|
&& ui_state
|
||||||
|
.ignore_position_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_position {
|
||||||
|
ui_state.position_slider_value = track.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_update_volume = !ui_state.volume_dragged
|
||||||
|
&& ui_state
|
||||||
|
.ignore_volume_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_volume {
|
||||||
|
ui_state.volume_slider_value = track.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
if let Some(act) = Self::draw_playback_controls(ui, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_slider_width = ui.spacing().slider_width;
|
||||||
|
Self::draw_position_control(ui, ui_state, track, default_slider_width);
|
||||||
|
Self::draw_volume_control(ui, ui_state, track, default_slider_width);
|
||||||
|
|
||||||
|
if let Some(act) = Self::draw_stop_control(ui, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Color32, RichText, Ui};
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_hotkey_capture(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(ui.available_height() / 3.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.capture.header"))
|
||||||
|
.size(18.0)
|
||||||
|
.color(Color32::YELLOW)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot {
|
||||||
|
format!("{} '{}'", t!("gui.hotkeys.capture.for"), slot)
|
||||||
|
} else if let Some(path) = &self.app_state.assigning_hotkey_for_file {
|
||||||
|
format!(
|
||||||
|
"{} '{}'",
|
||||||
|
t!("gui.hotkeys.capture.for"),
|
||||||
|
path.file_name().unwrap_or_default().to_string_lossy()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(target).size(16.0));
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(t!("gui.hotkeys.capture.cancel"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Button, Color32, Label, RichText, TextEdit, Ui};
|
||||||
|
use egui_extras::{Column, TableBuilder};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::socket::Request;
|
||||||
|
use pwsp::utils::gui::make_request_async;
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub(crate) enum HotkeyAction {
|
||||||
|
Remove(String),
|
||||||
|
Capture(String),
|
||||||
|
ClearChord(String),
|
||||||
|
Play(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_hotkeys(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.y = 5.0;
|
||||||
|
|
||||||
|
self.draw_hotkeys_header(ui);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
self.draw_hotkeys_search(ui);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
let action = self.draw_hotkeys_table(ui);
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
self.handle_hotkey_action(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
||||||
|
if ui.add(back_button).clicked() {
|
||||||
|
self.app_state.show_hotkeys = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.header"))
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.menu_button(
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_ADD.codepoint,
|
||||||
|
t!("gui.hotkeys.add_command_select")
|
||||||
|
),
|
||||||
|
|ui| {
|
||||||
|
let mut selected_cmd = None;
|
||||||
|
if ui.button(t!("gui.hotkeys.toggle_pause_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
|
||||||
|
}
|
||||||
|
if ui.button(t!("gui.hotkeys.stop_playback_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_stop", Request::stop(None)));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button(t!("gui.hotkeys.pause_playback_command"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
selected_cmd = Some(("cmd_pause", Request::pause(None)));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button(t!("gui.hotkeys.resume_playback_command"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
selected_cmd = Some(("cmd_resume", Request::resume(None)));
|
||||||
|
}
|
||||||
|
if ui.button(t!("gui.hotkeys.toggle_loop_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((slot_name, req)) = selected_cmd {
|
||||||
|
make_request_async(Request::set_hotkey_action(slot_name, &req));
|
||||||
|
self.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.set_slot(slot_name.to_string(), req);
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
|
||||||
|
.hint_text(t!("gui.hotkeys.search_placeholder"))
|
||||||
|
.desired_width(f32::INFINITY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
|
||||||
|
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
||||||
|
let conflict_slots: std::collections::HashSet<&str> =
|
||||||
|
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
||||||
|
|
||||||
|
let search = self.app_state.hotkey_search_query.to_lowercase();
|
||||||
|
let mut action: Option<HotkeyAction> = None;
|
||||||
|
|
||||||
|
let slots: Vec<_> = self
|
||||||
|
.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.filter(|s| {
|
||||||
|
if search.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
s.slot.to_lowercase().contains(&search)
|
||||||
|
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|
||||||
|
|| s.key_chord
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&search)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let col_width = (available_width / 4.0).max(80.0);
|
||||||
|
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.striped(true)
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Slot
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Sound / Action name
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Key Chord
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Actions
|
||||||
|
.header(30.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_slot"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_sound"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_key_chord"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_actions"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
if slots.is_empty() {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.no_hotkeys_configured"))
|
||||||
|
.color(Color32::GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|_| {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for slot in &slots {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
// Column 1: Slot
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if conflict_slots.contains(slot.slot.as_str()) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(ICON_WARNING.codepoint)
|
||||||
|
.color(Color32::from_rgb(255, 165, 0)),
|
||||||
|
)
|
||||||
|
.on_hover_text("Key chord conflict");
|
||||||
|
}
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(&slot.slot).monospace()).truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 2: Sound / Action name
|
||||||
|
row.col(|ui| {
|
||||||
|
let action_name = match slot.action.name.as_str() {
|
||||||
|
"play" => {
|
||||||
|
if let Some(file_path_str) = slot.action.args.get("file_path") {
|
||||||
|
Path::new(file_path_str)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
"Play".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"toggle_pause" => "Toggle Pause".to_string(),
|
||||||
|
"pause" => "Pause Playback".to_string(),
|
||||||
|
"resume" => "Resume Playback".to_string(),
|
||||||
|
"stop" => "Stop Playback".to_string(),
|
||||||
|
"toggle_loop" => "Toggle Loop".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
ui.add(Label::new(RichText::new(action_name).monospace()).truncate());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 3: Key Chord
|
||||||
|
row.col(|ui| {
|
||||||
|
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(chord_text).monospace().color(
|
||||||
|
if slot.key_chord.is_some() {
|
||||||
|
Color32::from_rgb(100, 200, 100)
|
||||||
|
} else {
|
||||||
|
Color32::GRAY
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 4: Actions
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_DELETE).frame(false))
|
||||||
|
.on_hover_text("Remove slot")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Remove(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_KEYBOARD).frame(false))
|
||||||
|
.on_hover_text("Set key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Capture(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if slot.key_chord.is_some()
|
||||||
|
&& ui
|
||||||
|
.add(Button::new(ICON_BACKSPACE).frame(false))
|
||||||
|
.on_hover_text("Clear key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_PLAY_ARROW).frame(false))
|
||||||
|
.on_hover_text("Play")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Play(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
|
||||||
|
match action {
|
||||||
|
HotkeyAction::Remove(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey(&slot));
|
||||||
|
self.app_state.hotkey_config.remove_slot(&slot);
|
||||||
|
}
|
||||||
|
HotkeyAction::Capture(slot) => {
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot);
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
}
|
||||||
|
HotkeyAction::ClearChord(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey_key(&slot));
|
||||||
|
self.app_state.hotkey_config.set_key_chord(&slot, None);
|
||||||
|
}
|
||||||
|
HotkeyAction::Play(slot) => {
|
||||||
|
self.play_hotkey_slot(&slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::Ui;
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
|
||||||
|
mod body;
|
||||||
|
mod footer;
|
||||||
|
mod header;
|
||||||
|
mod hotkey_capture;
|
||||||
|
mod hotkeys;
|
||||||
|
mod settings;
|
||||||
|
mod waiting_for_daemon;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub(crate) fn get_volume_icon(volume: f32) -> &'static str {
|
||||||
|
if volume > 0.7 {
|
||||||
|
ICON_VOLUME_UP.codepoint
|
||||||
|
} else if volume <= 0.0 {
|
||||||
|
ICON_VOLUME_OFF.codepoint
|
||||||
|
} else if volume < 0.3 {
|
||||||
|
ICON_VOLUME_MUTE.codepoint
|
||||||
|
} else {
|
||||||
|
ICON_VOLUME_DOWN.codepoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, ui: &mut Ui) {
|
||||||
|
self.draw_header(ui);
|
||||||
|
self.draw_body(ui);
|
||||||
|
ui.separator();
|
||||||
|
self.draw_footer(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Align, Button, Color32, Layout, RichText, Ui};
|
||||||
|
use egui_material_icons::icons::ICON_ARROW_BACK;
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_settings(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.y = 5.0;
|
||||||
|
// --------- Back Button and Title ----------
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
||||||
|
let back_button_response = ui.add(back_button);
|
||||||
|
if back_button_response.clicked() {
|
||||||
|
self.app_state.show_settings = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.add_space(ui.available_width() / 2.0 - 40.0);
|
||||||
|
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.settings.header"))
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(20.0);
|
||||||
|
|
||||||
|
// --------- Checkboxes ----------
|
||||||
|
let save_volume_response = ui.checkbox(
|
||||||
|
&mut self.config.save_volume,
|
||||||
|
t!("gui.settings.remember_volume"),
|
||||||
|
);
|
||||||
|
let save_input_response =
|
||||||
|
ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
|
||||||
|
let save_scale_response = ui.checkbox(
|
||||||
|
&mut self.config.save_scale_factor,
|
||||||
|
t!("gui.settings.remember_ui_scale"),
|
||||||
|
);
|
||||||
|
let pause_on_exit_response = ui.checkbox(
|
||||||
|
&mut self.config.pause_on_exit,
|
||||||
|
t!("gui.settings.pause_on_window_close"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if save_volume_response.changed()
|
||||||
|
|| save_input_response.changed()
|
||||||
|
|| save_scale_response.changed()
|
||||||
|
|| pause_on_exit_response.changed()
|
||||||
|
{
|
||||||
|
self.config.save_to_file().ok();
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||||
|
ui.label(t!(
|
||||||
|
"gui.settings.version",
|
||||||
|
version = env!("CARGO_PKG_VERSION")
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{RichText, Ui};
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
|
||||||
|
ui.centered_and_justified(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new("Waiting for PWSP daemon to start...")
|
||||||
|
.size(34.0)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-2
@@ -1,8 +1,14 @@
|
|||||||
mod gui;
|
mod gui;
|
||||||
|
|
||||||
use std::error::Error;
|
use anyhow::Result;
|
||||||
|
use rust_i18n::i18n;
|
||||||
|
|
||||||
|
i18n!("locales", fallback = "en");
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn Error>> {
|
async fn main() -> Result<()> {
|
||||||
|
let locale = sys_locale::get_locale().unwrap_or(String::from("en-US"));
|
||||||
|
rust_i18n::set_locale(&locale);
|
||||||
|
|
||||||
gui::run().await
|
gui::run().await
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-18
@@ -5,6 +5,7 @@ use crate::{
|
|||||||
pipewire::{create_link, get_device, link_player_to_virtual_mic},
|
pipewire::{create_link, get_device, link_player_to_virtual_mic},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
|
use rodio::{Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
@@ -65,7 +66,7 @@ pub struct AudioPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl AudioPlayer {
|
impl AudioPlayer {
|
||||||
pub async fn new() -> Result<Self, Box<dyn Error>> {
|
pub async fn new() -> Result<Self> {
|
||||||
let daemon_config = get_daemon_config();
|
let daemon_config = get_daemon_config();
|
||||||
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
|
let default_volume = daemon_config.default_volume.unwrap_or(1.0);
|
||||||
|
|
||||||
@@ -88,13 +89,15 @@ impl AudioPlayer {
|
|||||||
Ok(audio_player)
|
Ok(audio_player)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ensure_stream(&mut self) -> Result<&MixerDeviceSink, Box<dyn Error>> {
|
fn ensure_stream(&mut self) -> Result<&MixerDeviceSink> {
|
||||||
if self.stream_handle.is_none() {
|
if self.stream_handle.is_none() {
|
||||||
let mut sink = DeviceSinkBuilder::open_default_sink()?;
|
let mut sink = DeviceSinkBuilder::open_default_sink()?;
|
||||||
sink.log_on_drop(false);
|
sink.log_on_drop(false);
|
||||||
self.stream_handle = Some(sink);
|
self.stream_handle = Some(sink);
|
||||||
}
|
}
|
||||||
Ok(self.stream_handle.as_ref().unwrap())
|
self.stream_handle
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| anyhow!("Failed to initialize stream_handle"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn drop_stream(&mut self) {
|
fn drop_stream(&mut self) {
|
||||||
@@ -126,7 +129,7 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn link_player(&mut self) -> Result<(), Box<dyn Error>> {
|
async fn link_player(&mut self) -> Result<()> {
|
||||||
if self.player_link_sender.is_some() {
|
if self.player_link_sender.is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
@@ -140,7 +143,7 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
|
async fn link_devices(&mut self) -> Result<()> {
|
||||||
self.abort_link_thread();
|
self.abort_link_thread();
|
||||||
|
|
||||||
let input_device;
|
let input_device;
|
||||||
@@ -289,7 +292,7 @@ impl AudioPlayer {
|
|||||||
0.0
|
0.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn seek(&mut self, position: f32, id: Option<u32>) -> Result<(), Box<dyn Error>> {
|
pub fn seek(&mut self, position: f32, id: Option<u32>) -> Result<()> {
|
||||||
let position = if position < 0.0 { 0.0 } else { position };
|
let position = if position < 0.0 { 0.0 } else { position };
|
||||||
|
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
@@ -305,22 +308,18 @@ impl AudioPlayer {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_duration(&mut self, id: Option<u32>) -> Result<f32, Box<dyn Error>> {
|
pub fn get_duration(&mut self, id: Option<u32>) -> Result<f32> {
|
||||||
if let Some(id) = id {
|
if let Some(id) = id {
|
||||||
if let Some(sound) = self.tracks.get(&id) {
|
if let Some(sound) = self.tracks.get(&id) {
|
||||||
return sound.duration.ok_or("Unknown duration".into());
|
return sound.duration.ok_or(anyhow!("Unknown duration"));
|
||||||
}
|
}
|
||||||
} else if let Some(sound) = self.tracks.values().last() {
|
} else if let Some(sound) = self.tracks.values().last() {
|
||||||
return sound.duration.ok_or("Unknown duration".into());
|
return sound.duration.ok_or(anyhow!("Unknown duration"));
|
||||||
}
|
}
|
||||||
Err("No track playing".into())
|
Err(anyhow!("No track playing"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn play(
|
pub async fn play(&mut self, file_path: &Path, concurrent: bool) -> Result<u32> {
|
||||||
&mut self,
|
|
||||||
file_path: &Path,
|
|
||||||
concurrent: bool,
|
|
||||||
) -> Result<u32, Box<dyn Error>> {
|
|
||||||
let path_buf = file_path.to_path_buf();
|
let path_buf = file_path.to_path_buf();
|
||||||
|
|
||||||
let decoder_result =
|
let decoder_result =
|
||||||
@@ -369,7 +368,7 @@ impl AudioPlayer {
|
|||||||
|
|
||||||
Ok(id)
|
Ok(id)
|
||||||
}
|
}
|
||||||
Err(err) => Err(err as Box<dyn Error>),
|
Err(err) => Err(anyhow!(err)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -472,11 +471,11 @@ impl AudioPlayer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> {
|
pub async fn set_current_input_device(&mut self, name: &str) -> Result<()> {
|
||||||
let input_device = get_device(name).await?;
|
let input_device = get_device(name).await?;
|
||||||
|
|
||||||
if input_device.device_type != DeviceType::Input {
|
if input_device.device_type != DeviceType::Input {
|
||||||
return Err("Selected device is not an input device".into());
|
return Err(anyhow!("Selected device is not an input device"));
|
||||||
}
|
}
|
||||||
|
|
||||||
self.input_device_name = Some(name.to_string());
|
self.input_device_name = Some(name.to_string());
|
||||||
|
|||||||
+9
-8
@@ -1,6 +1,7 @@
|
|||||||
use crate::{types::socket::Request, utils::config::get_config_path};
|
use crate::{types::socket::Request, utils::config::get_config_path};
|
||||||
|
use anyhow::Result;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{collections::HashMap, error::Error, fs, path::PathBuf};
|
use std::{collections::HashMap, fs, path::PathBuf};
|
||||||
|
|
||||||
#[derive(Default, Clone, Serialize, Deserialize)]
|
#[derive(Default, Clone, Serialize, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -10,7 +11,7 @@ pub struct DaemonConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl DaemonConfig {
|
impl DaemonConfig {
|
||||||
pub fn save_to_file(&self) -> Result<(), Box<dyn Error>> {
|
pub fn save_to_file(&self) -> Result<()> {
|
||||||
let config_path = get_config_path()?.join("daemon.json");
|
let config_path = get_config_path()?.join("daemon.json");
|
||||||
|
|
||||||
if let Some(config_dir) = config_path.parent()
|
if let Some(config_dir) = config_path.parent()
|
||||||
@@ -24,7 +25,7 @@ impl DaemonConfig {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_from_file() -> Result<DaemonConfig, Box<dyn Error>> {
|
pub fn load_from_file() -> Result<DaemonConfig> {
|
||||||
let config_path = get_config_path()?.join("daemon.json");
|
let config_path = get_config_path()?.join("daemon.json");
|
||||||
let bytes = fs::read(config_path)?;
|
let bytes = fs::read(config_path)?;
|
||||||
match serde_json::from_slice::<DaemonConfig>(&bytes) {
|
match serde_json::from_slice::<DaemonConfig>(&bytes) {
|
||||||
@@ -65,7 +66,7 @@ impl Default for GuiConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl GuiConfig {
|
impl GuiConfig {
|
||||||
pub fn save_to_file(&mut self) -> Result<(), Box<dyn Error>> {
|
pub fn save_to_file(&mut self) -> Result<()> {
|
||||||
let config_path = get_config_path()?.join("gui.json");
|
let config_path = get_config_path()?.join("gui.json");
|
||||||
|
|
||||||
if let Some(config_dir) = config_path.parent()
|
if let Some(config_dir) = config_path.parent()
|
||||||
@@ -84,7 +85,7 @@ impl GuiConfig {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_from_file() -> Result<GuiConfig, Box<dyn Error>> {
|
pub fn load_from_file() -> Result<GuiConfig> {
|
||||||
let config_path = get_config_path()?.join("gui.json");
|
let config_path = get_config_path()?.join("gui.json");
|
||||||
let bytes = fs::read(config_path)?;
|
let bytes = fs::read(config_path)?;
|
||||||
match serde_json::from_slice::<GuiConfig>(&bytes) {
|
match serde_json::from_slice::<GuiConfig>(&bytes) {
|
||||||
@@ -108,11 +109,11 @@ pub struct HotkeyConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl HotkeyConfig {
|
impl HotkeyConfig {
|
||||||
pub fn config_path() -> Result<PathBuf, Box<dyn Error>> {
|
pub fn config_path() -> Result<PathBuf> {
|
||||||
Ok(get_config_path()?.join("hotkeys.json"))
|
Ok(get_config_path()?.join("hotkeys.json"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load() -> Result<HotkeyConfig, Box<dyn Error>> {
|
pub fn load() -> Result<HotkeyConfig> {
|
||||||
let path = Self::config_path()?;
|
let path = Self::config_path()?;
|
||||||
if !path.exists() {
|
if !path.exists() {
|
||||||
return Ok(HotkeyConfig::default());
|
return Ok(HotkeyConfig::default());
|
||||||
@@ -124,7 +125,7 @@ impl HotkeyConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<(), Box<dyn Error>> {
|
pub fn save(&self) -> Result<()> {
|
||||||
let path = Self::config_path()?;
|
let path = Self::config_path()?;
|
||||||
if let Some(dir) = path.parent()
|
if let Some(dir) = path.parent()
|
||||||
&& !dir.exists()
|
&& !dir.exists()
|
||||||
|
|||||||
+6
-3
@@ -43,15 +43,18 @@ pub struct AppState {
|
|||||||
pub dirs: Vec<PathBuf>,
|
pub dirs: Vec<PathBuf>,
|
||||||
pub dirs_to_remove: HashSet<PathBuf>,
|
pub dirs_to_remove: HashSet<PathBuf>,
|
||||||
|
|
||||||
pub selected_file: Option<PathBuf>,
|
pub listed_files: HashSet<PathBuf>,
|
||||||
pub files: HashSet<PathBuf>,
|
pub listed_dirs: HashSet<PathBuf>,
|
||||||
|
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
|
||||||
|
|
||||||
pub show_hotkeys: bool,
|
pub show_hotkeys: bool,
|
||||||
|
pub hotkey_capture_active: bool,
|
||||||
|
|
||||||
pub hotkey_config: HotkeyConfig,
|
pub hotkey_config: HotkeyConfig,
|
||||||
pub hotkey_search_query: String,
|
pub hotkey_search_query: String,
|
||||||
|
|
||||||
pub assigning_hotkey_slot: Option<String>,
|
pub assigning_hotkey_slot: Option<String>,
|
||||||
pub assigning_hotkey_for_file: Option<PathBuf>,
|
pub assigning_hotkey_for_file: Option<PathBuf>,
|
||||||
pub hotkey_capture_active: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
|
|||||||
+3
-2
@@ -1,6 +1,7 @@
|
|||||||
use std::{error::Error, path::PathBuf};
|
use anyhow::Result;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
pub fn get_config_path() -> Result<PathBuf, Box<dyn Error>> {
|
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().expect("Failed to obtain config dir");
|
||||||
Ok(config_path.join("pwsp"))
|
Ok(config_path.join("pwsp"))
|
||||||
}
|
}
|
||||||
|
|||||||
+42
-11
@@ -4,9 +4,10 @@ use crate::types::{
|
|||||||
socket::{MAX_MESSAGE_SIZE, Request, Response},
|
socket::{MAX_MESSAGE_SIZE, Request, Response},
|
||||||
};
|
};
|
||||||
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use anyhow::Result;
|
||||||
|
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{error::Error, fs};
|
use std::{env, error::Error, fs};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::UnixStream,
|
net::UnixStream,
|
||||||
@@ -36,29 +37,59 @@ pub fn get_daemon_config() -> DaemonConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_runtime_dir() -> PathBuf {
|
fn get_current_uid() -> u32 {
|
||||||
dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp"))
|
rustix::process::geteuid().as_raw()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_runtime_dir() -> Result<(), Box<dyn Error>> {
|
pub fn get_runtime_dir() -> PathBuf {
|
||||||
|
dirs::runtime_dir().unwrap_or_else(|| {
|
||||||
|
let uid = get_current_uid();
|
||||||
|
env::temp_dir().join(format!("pwsp-{}", uid))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn create_runtime_dir() -> Result<()> {
|
||||||
let runtime_dir = get_runtime_dir();
|
let runtime_dir = get_runtime_dir();
|
||||||
if !runtime_dir.exists() {
|
|
||||||
fs::create_dir_all(&runtime_dir)?;
|
if runtime_dir.exists() {
|
||||||
|
let meta = fs::symlink_metadata(&runtime_dir)?;
|
||||||
|
if meta.is_symlink() {
|
||||||
|
return Err(anyhow::anyhow!("Runtime directory is a symlink"));
|
||||||
|
}
|
||||||
|
let uid = get_current_uid();
|
||||||
|
if meta.uid() != uid {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Runtime directory is owned by another user"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if meta.permissions().mode() & 0o777 != 0o700 {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Runtime directory has incorrect permissions"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.mode(0o700)
|
||||||
|
.create(&runtime_dir)?;
|
||||||
}
|
}
|
||||||
fs::set_permissions(&runtime_dir, fs::Permissions::from_mode(0o700))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_daemon_running() -> Result<bool, Box<dyn Error>> {
|
pub fn is_daemon_running() -> Result<bool> {
|
||||||
let lock_file = fs::File::create(get_runtime_dir().join("daemon.lock"))?;
|
let lock_file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(get_runtime_dir().join("daemon.lock"))?;
|
||||||
match lock_file.try_lock() {
|
match lock_file.try_lock() {
|
||||||
Ok(_) => Ok(false),
|
Ok(_) => Ok(false),
|
||||||
Err(_) => Ok(true),
|
Err(_) => Ok(true),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn wait_for_daemon() -> Result<(), Box<dyn Error>> {
|
pub async fn wait_for_daemon() -> Result<()> {
|
||||||
if is_daemon_running()? {
|
if is_daemon_running()? {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -7,8 +7,8 @@ use crate::{
|
|||||||
},
|
},
|
||||||
utils::daemon::{is_daemon_running, make_request},
|
utils::daemon::{is_daemon_running, make_request},
|
||||||
};
|
};
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
@@ -22,11 +22,11 @@ pub fn get_gui_config() -> GuiConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_request_sync(request: Request) -> Result<Response, Box<dyn Error>> {
|
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()
|
||||||
.block_on(make_request(request))
|
.block_on(make_request(request))
|
||||||
.map_err(|e| e as Box<dyn Error>)
|
.map_err(|e| anyhow!(e))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+21
-19
@@ -1,9 +1,10 @@
|
|||||||
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
|
use crate::types::pipewire::{AudioDevice, DeviceType, Port, Terminate};
|
||||||
|
use anyhow::{Result, anyhow};
|
||||||
use pipewire::{
|
use pipewire::{
|
||||||
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
|
context::ContextRc, link::Link, main_loop::MainLoopRc, properties::properties,
|
||||||
registry::GlobalObject, spa::utils::dict::DictRef,
|
registry::GlobalObject, spa::utils::dict::DictRef,
|
||||||
};
|
};
|
||||||
use std::{collections::HashMap, error::Error, thread};
|
use std::{collections::HashMap, thread};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
sync::mpsc,
|
sync::mpsc,
|
||||||
time::{Duration, timeout},
|
time::{Duration, timeout},
|
||||||
@@ -142,7 +143,7 @@ async fn pw_get_global_objects_thread(
|
|||||||
main_loop.run();
|
main_loop.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), Box<dyn Error>> {
|
pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>)> {
|
||||||
// Channels to communicate with pipewire thread
|
// Channels to communicate with pipewire thread
|
||||||
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
let (main_sender, mut main_receiver) = mpsc::channel(10);
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel();
|
||||||
@@ -155,7 +156,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
|
|
||||||
// Wait for initialization to complete
|
// Wait for initialization to complete
|
||||||
if let Err(e) = init_receiver.recv()? {
|
if let Err(e) = init_receiver.recv()? {
|
||||||
return Err(e.into());
|
return Err(anyhow!(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
let mut input_devices: HashMap<u32, AudioDevice> = HashMap::new();
|
||||||
@@ -238,7 +239,7 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>> {
|
pub async fn get_device(device_name: &str) -> Result<AudioDevice> {
|
||||||
let (input_devices, output_devices) = get_all_devices().await?;
|
let (input_devices, output_devices) = get_all_devices().await?;
|
||||||
|
|
||||||
input_devices
|
input_devices
|
||||||
@@ -250,10 +251,10 @@ pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>
|
|||||||
|| device.name.contains(device_name)
|
|| device.name.contains(device_name)
|
||||||
|| device.nick.contains(device_name)
|
|| device.nick.contains(device_name)
|
||||||
})
|
})
|
||||||
.ok_or_else(|| "Device not found".into())
|
.ok_or_else(|| anyhow!("Device not found"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
@@ -305,45 +306,46 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = init_receiver.recv()? {
|
if let Err(e) = init_receiver.recv()? {
|
||||||
return Err(e.into());
|
return Err(anyhow!(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn link_player_to_virtual_mic()
|
pub async fn link_player_to_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>> {
|
||||||
-> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
|
||||||
let pwsp_daemon_output = match get_device("pwsp-daemon").await {
|
let pwsp_daemon_output = match get_device("pwsp-daemon").await {
|
||||||
Ok(device) => device,
|
Ok(device) => device,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err(
|
return Err(anyhow!(
|
||||||
"Could not find alsa_playback.pwsp-daemon device, skipping device linking".into(),
|
"Could not find alsa_playback.pwsp-daemon device, skipping device linking"
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let pwsp_daemon_input = match get_device("pwsp-virtual-mic").await {
|
let pwsp_daemon_input = match get_device("pwsp-virtual-mic").await {
|
||||||
Ok(device) => device,
|
Ok(device) => device,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
return Err("Could not find pwsp-virtual-mic device, skipping device linking".into());
|
return Err(anyhow!(
|
||||||
|
"Could not find pwsp-virtual-mic device, skipping device linking"
|
||||||
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let output_fl = match pwsp_daemon_output.output_fl {
|
let output_fl = match pwsp_daemon_output.output_fl {
|
||||||
Some(port) => port,
|
Some(port) => port,
|
||||||
None => return Err("Failed to get pwsp-daemon output_fl".into()),
|
None => return Err(anyhow!("Failed to get pwsp-daemon output_fl")),
|
||||||
};
|
};
|
||||||
let output_fr = match pwsp_daemon_output.output_fr {
|
let output_fr = match pwsp_daemon_output.output_fr {
|
||||||
Some(port) => port,
|
Some(port) => port,
|
||||||
None => return Err("Failed to get pwsp-daemon output_fr".into()),
|
None => return Err(anyhow!("Failed to get pwsp-daemon output_fr")),
|
||||||
};
|
};
|
||||||
let input_fl = match pwsp_daemon_input.input_fl {
|
let input_fl = match pwsp_daemon_input.input_fl {
|
||||||
Some(port) => port,
|
Some(port) => port,
|
||||||
None => return Err("Failed to get pwsp-virtual-mic input_fl".into()),
|
None => return Err(anyhow!("Failed to get pwsp-virtual-mic input_fl")),
|
||||||
};
|
};
|
||||||
let input_fr = match pwsp_daemon_input.input_fr {
|
let input_fr = match pwsp_daemon_input.input_fr {
|
||||||
Some(port) => port,
|
Some(port) => port,
|
||||||
None => return Err("Failed to get pwsp-virtual-mic input_fr".into()),
|
None => return Err(anyhow!("Failed to get pwsp-virtual-mic input_fr")),
|
||||||
};
|
};
|
||||||
|
|
||||||
create_link(output_fl, output_fr, input_fl, input_fr)
|
create_link(output_fl, output_fr, input_fl, input_fr)
|
||||||
@@ -354,7 +356,7 @@ pub fn create_link(
|
|||||||
output_fr: Port,
|
output_fr: Port,
|
||||||
input_fl: Port,
|
input_fl: Port,
|
||||||
input_fr: Port,
|
input_fr: Port,
|
||||||
) -> Result<pipewire::channel::Sender<Terminate>, Box<dyn Error>> {
|
) -> Result<pipewire::channel::Sender<Terminate>> {
|
||||||
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
|
||||||
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
let (init_sender, init_receiver) = std::sync::mpsc::sync_channel(0);
|
||||||
|
|
||||||
@@ -419,7 +421,7 @@ pub fn create_link(
|
|||||||
});
|
});
|
||||||
|
|
||||||
if let Err(e) = init_receiver.recv()? {
|
if let Err(e) = init_receiver.recv()? {
|
||||||
return Err(e.into());
|
return Err(anyhow!(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(pw_sender)
|
Ok(pw_sender)
|
||||||
|
|||||||
Reference in New Issue
Block a user