mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Compare commits
4 Commits
0d39e32f2e
...
b13d18ca9a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b13d18ca9a | ||
|
|
a09a7c8976 | ||
|
|
abeae2144c | ||
|
|
5e65508fed |
14
.github/workflows/spotube-release-binary.yml
vendored
14
.github/workflows/spotube-release-binary.yml
vendored
@ -61,7 +61,7 @@ jobs:
|
||||
java-version: 21
|
||||
|
||||
- name: Set up Android SDK
|
||||
uses: android-actions/setup-android@v3
|
||||
uses: android-actions/setup-android@v4
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
@ -112,6 +112,9 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
@ -170,6 +173,9 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
@ -231,6 +237,9 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
@ -268,6 +277,9 @@ jobs:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
|
||||
- name: Set up Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Set up Gradle
|
||||
uses: gradle/actions/setup-gradle@v4
|
||||
|
||||
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@ -108,3 +108,10 @@ tm.json
|
||||
|
||||
android/build
|
||||
android/app/.cxx
|
||||
|
||||
/node_modules
|
||||
/package.json
|
||||
/package-lock.json
|
||||
/bun.lock
|
||||
|
||||
**/target/
|
||||
@ -30,4 +30,6 @@ plugins {
|
||||
alias(libs.plugins.zipline.gradle.plugin) apply false
|
||||
alias(libs.plugins.spotubeGradle) apply false
|
||||
alias(libs.plugins.vlcjBundler) apply false
|
||||
alias(libs.plugins.uniffi) apply false
|
||||
alias(libs.plugins.cargo) apply false
|
||||
}
|
||||
723
composeApp/Cargo.lock
generated
Normal file
723
composeApp/Cargo.lock
generated
Normal file
@ -0,0 +1,723 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.103"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3"
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d4744ed2eef2645831b441d8f5459689ade2ab27c854488fbab1fbe94fce1a7"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"itoa",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d661e0f57be36a5c14c48f78d09011e67e0cb618f269cca9f2fd8d15b68c46ac"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"memchr",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf315ce6524c857bb129ff794935cf6d42c82a6cff60526fe2a63593de4d0d4f"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
|
||||
[[package]]
|
||||
name = "camino"
|
||||
version = "1.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f2d30e4173c4026932d51d31d6b0613b1fd3014bf3f9f8943d4ba139c437ba0"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo-platform"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cargo_metadata"
|
||||
version = "0.19.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd5eb614ed4c27c5d706420e4320fbe3216ab31fa1c33cd8246ac36dae4479ba"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"cargo-platform",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "compose-app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"lofty",
|
||||
"uniffi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fs-err"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "goblin"
|
||||
version = "0.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b363a30c165f666402fe6a3024d3bec7ebc898f96a4a23bd1c99f8dbf3f4f47"
|
||||
dependencies = [
|
||||
"log",
|
||||
"plain",
|
||||
"scroll",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "lofty"
|
||||
version = "0.24.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dec4feeff6c7d75093278133a06e827d7af6d2bfe20b0f331f9d10338a5ec7ca"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"data-encoding",
|
||||
"flate2",
|
||||
"lofty_attr",
|
||||
"log",
|
||||
"ogg_pager",
|
||||
"paste",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lofty_attr"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "458ace39169e4b83c4f77ae3d42d5d1d11c422feef590219a97c973d3b524557"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ogg_pager"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d36b1d6964c3ac92b7aea701057e02b6b91143d70d83b20abf75a231a3c0216"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "plain"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.46"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scroll"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ab8598aa408498679922eff7fa985c25d58a90771bd6be794434c5277eab1a6"
|
||||
dependencies = [
|
||||
"scroll_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scroll_derive"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1783eabc414609e28a5ba76aee5ddd52199f7107a0b24c2e9746a1ecc34a683d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.150"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"serde",
|
||||
"serde_core",
|
||||
"zmij",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "smawk"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100"
|
||||
|
||||
[[package]]
|
||||
name = "static_assertions"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.119"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "textwrap"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
|
||||
dependencies = [
|
||||
"smawk",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "uniffi"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3291800a6b06569f7d3e15bdb6dc235e0f0c8bd3eb07177f430057feb076415f"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cargo_metadata",
|
||||
"uniffi_bindgen",
|
||||
"uniffi_core",
|
||||
"uniffi_macros",
|
||||
"uniffi_pipeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_bindgen"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a04b99fa7796eaaa7b87976a0dbdd1178dc1ee702ea00aca2642003aef9b669e"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"askama",
|
||||
"camino",
|
||||
"cargo_metadata",
|
||||
"fs-err",
|
||||
"glob",
|
||||
"goblin",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"once_cell",
|
||||
"serde",
|
||||
"tempfile",
|
||||
"textwrap",
|
||||
"toml",
|
||||
"uniffi_internal_macros",
|
||||
"uniffi_meta",
|
||||
"uniffi_pipeline",
|
||||
"uniffi_udl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_core"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f38a9a27529ccff732f8efddb831b65b1e07f7dea3fd4cacd4a35a8c4b253b98"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bytes",
|
||||
"once_cell",
|
||||
"static_assertions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_internal_macros"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09acd2ce09c777dd65ee97c251d33c8a972afc04873f1e3b21eb3492ade16933"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_macros"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5596f178c4f7aafa1a501c4e0b96236a96bc2ef92bdb453d83e609dad0040152"
|
||||
dependencies = [
|
||||
"camino",
|
||||
"fs-err",
|
||||
"once_cell",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
"toml",
|
||||
"uniffi_meta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_meta"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beadc1f460eb2e209263c49c4f5b19e9a02e00a3b2b393f78ad10d766346ecff"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"siphasher",
|
||||
"uniffi_internal_macros",
|
||||
"uniffi_pipeline",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_pipeline"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd76b3ac8a2d964ca9fce7df21c755afb4c77b054a85ad7a029ad179cc5abb8a"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"heck",
|
||||
"indexmap",
|
||||
"tempfile",
|
||||
"uniffi_internal_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uniffi_udl"
|
||||
version = "0.29.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4319cf905911d70d5b97ce0f46f101619a22e9a189c8c46d797a9955e9233716"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"textwrap",
|
||||
"uniffi_meta",
|
||||
"weedle2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weedle2"
|
||||
version = "5.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "998d2c24ec099a87daf9467808859f9d82b61f1d9c9701251aea037f514eae0e"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
|
||||
12
composeApp/Cargo.toml
Normal file
12
composeApp/Cargo.toml
Normal file
@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "compose-app"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[dependencies]
|
||||
uniffi = "0.29.4"
|
||||
lofty = "0.24.0"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "staticlib"]
|
||||
path = "src/commonMain/rust/lib.rs"
|
||||
@ -15,6 +15,8 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import gobley.gradle.GobleyHost
|
||||
import gobley.gradle.cargo.dsl.jvm
|
||||
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.compose.reload.gradle.ComposeHotRun
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
@ -30,6 +32,9 @@ plugins {
|
||||
alias(libs.plugins.kotlinSerialization)
|
||||
alias(libs.plugins.zipline.gradle.plugin)
|
||||
alias(libs.plugins.vlcjBundler)
|
||||
alias(libs.plugins.uniffi)
|
||||
alias(libs.plugins.cargo)
|
||||
kotlin("plugin.atomicfu") version libs.versions.kotlin
|
||||
}
|
||||
|
||||
vlcjBundler {
|
||||
@ -334,3 +339,9 @@ tasks.withType<ComposeHotRun>().configureEach {
|
||||
project.layout.buildDirectory.dir("compose/tmp/prepareAppResources").get()
|
||||
)
|
||||
}
|
||||
|
||||
cargo {
|
||||
builds.jvm {
|
||||
embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget)
|
||||
}
|
||||
}
|
||||
@ -25,6 +25,7 @@ import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.MediaItem as Media3MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
@ -76,6 +77,16 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Spotube")
|
||||
.setContentText("Playback service")
|
||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||
.setContentIntent(sessionActivity)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
|
||||
librarySession = MediaLibrarySession.Builder(this, audioPlayer.player, LibrarySessionCallback())
|
||||
.setSessionActivity(sessionActivity)
|
||||
.build()
|
||||
@ -388,5 +399,6 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
companion object {
|
||||
private const val TAG = "PlaybackService"
|
||||
private const val CHANNEL_ID = "spotube_playback"
|
||||
private const val NOTIFICATION_ID = 1
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,6 +17,8 @@
|
||||
|
||||
package dev.krtirtho.spotube
|
||||
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -34,6 +36,7 @@ import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES
|
||||
import dev.krtirtho.spotube.core.navigation.rememberNavigationState
|
||||
import dev.krtirtho.spotube.core.navigation.toEntries
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
||||
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
@ -86,7 +89,7 @@ val tabs = listOf(
|
||||
)
|
||||
)
|
||||
|
||||
@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class)
|
||||
@OptIn(KoinExperimentalAPI::class, ExperimentalCoroutinesApi::class, ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun App(
|
||||
content: @Composable () -> Unit = {}
|
||||
@ -106,6 +109,10 @@ fun App(
|
||||
val baseUITheme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) {
|
||||
AppShell(navigator, navigationState) {
|
||||
SharedTransitionLayout {
|
||||
CompositionLocalProvider(
|
||||
LocalSharedTransitionScope provides this@SharedTransitionLayout,
|
||||
) {
|
||||
Column {
|
||||
NavDisplay(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
@ -114,6 +121,8 @@ fun App(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@ -60,5 +60,6 @@ fun AlbumCard(
|
||||
scope.launch { playbackHelper.addAlbumToQueue(album.id) }
|
||||
},
|
||||
modifier = modifier.width(160.dp),
|
||||
sharedElementKey = "album_art_${album.id}",
|
||||
)
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@ -30,9 +31,8 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
@ -41,16 +41,17 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroup
|
||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
import dev.krtirtho.spotube.core.ui.base.GroupIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryButton
|
||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||
import dev.krtirtho.spotube.resources.iconsax.ArrowLeft3
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxEdit
|
||||
@ -64,6 +65,7 @@ import org.jetbrains.compose.resources.DrawableResource
|
||||
import org.jetbrains.compose.resources.painterResource
|
||||
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun CollectionDetails(
|
||||
modifier: Modifier = Modifier,
|
||||
@ -82,12 +84,14 @@ fun CollectionDetails(
|
||||
onFollowClick: () -> Unit = { },
|
||||
showFollowButton: Boolean = true,
|
||||
onEdit: (() -> Unit)? = null,
|
||||
sharedElementKey: String? = null,
|
||||
) {
|
||||
|
||||
|
||||
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
|
||||
val isCompact = maxWidth < 600.dp
|
||||
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
|
||||
|
||||
val playPauseButton = @Composable {
|
||||
|
||||
Row(
|
||||
@ -207,6 +211,11 @@ fun CollectionDetails(
|
||||
modifier = Modifier
|
||||
.size(if (isCompact) 100.dp else 200.dp)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.sharedElementOrNone(
|
||||
key = sharedElementKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
.shimmerApply(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
@ -260,10 +269,6 @@ fun CollectionDetails(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
shape = RoundedCornerShape(20.dp),
|
||||
) {
|
||||
if (isCompact) {
|
||||
Column(
|
||||
@ -314,8 +319,104 @@ fun CollectionDetails(
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
@Preview
|
||||
fun CollapsedCollectionHeader(
|
||||
title: String,
|
||||
imageURL: String,
|
||||
imageResource: DrawableResource? = null,
|
||||
isPlaying: Boolean,
|
||||
onPlay: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
onBack: (() -> Unit)? = null,
|
||||
sharedElementKey: String? = null,
|
||||
) {
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
if (onBack != null) {
|
||||
IconButton(
|
||||
onClick = onBack,
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Iconsax.ArrowLeft3,
|
||||
contentDescription = "Back",
|
||||
modifier = Modifier.size(20.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(36.dp)
|
||||
.clip(RoundedCornerShape(6.dp))
|
||||
.sharedElementOrNone(
|
||||
key = sharedElementKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
.shimmerApply(),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (imageURL.isNotBlank()) {
|
||||
AsyncImage(
|
||||
model = imageURL,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else if (imageResource != null) {
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(imageResource),
|
||||
contentDescription = title,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
} else {
|
||||
Text(
|
||||
text = title.take(1).ifBlank { "?" }.uppercase(),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
TextWithShimmer(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleSmall,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
IconButton(
|
||||
onClick = onPlay,
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isPlaying) Iconsax.IconsaxPauseCircle else Iconsax.IconsaxPlayCircle2,
|
||||
contentDescription = if (isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(24.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@androidx.compose.ui.tooling.preview.Preview
|
||||
fun CollectionDetailsPreview() {
|
||||
CollectionDetails(
|
||||
title = "My Playlist",
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -55,6 +56,9 @@ import dev.krtirtho.spotube.core.audioplayer.QueueCollectionEntry
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
||||
import dev.krtirtho.spotube.core.ui.component.sharedElementOrNone
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle
|
||||
@ -66,6 +70,7 @@ import org.koin.compose.koinInject
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
import spotube.composeapp.generated.resources.liked_tracks
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun LikedTracksCard(
|
||||
modifier: Modifier = Modifier,
|
||||
@ -79,6 +84,9 @@ fun LikedTracksCard(
|
||||
val currentCollectionEntry by audioPlayerQueue.currentCollectionEntryFlow.collectAsStateWithLifecycle()
|
||||
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks
|
||||
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
@ -91,8 +99,15 @@ fun LikedTracksCard(
|
||||
androidx.compose.foundation.Image(
|
||||
painter = painterResource(Res.drawable.liked_tracks),
|
||||
contentDescription = "Liked Tracks",
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.sharedElementOrNone(
|
||||
key = "saved_tracks_art",
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
|
||||
|
||||
@ -57,6 +57,7 @@ fun PlaylistCard(
|
||||
onAddToQueue = {
|
||||
scope.launch { playbackHelper.addPlaylistToQueue(playlist.id) }
|
||||
},
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
sharedElementKey = "playlist_art_${playlist.id}",
|
||||
)
|
||||
}
|
||||
|
||||
@ -0,0 +1,47 @@
|
||||
/*
|
||||
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibilityScope
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
val LocalSharedTransitionScope = compositionLocalOf<SharedTransitionScope?> { null }
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
val LocalAnimatedVisibilityScope = compositionLocalOf<AnimatedVisibilityScope?> { null }
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun Modifier.sharedElementOrNone(
|
||||
key: String?,
|
||||
sharedTransitionScope: SharedTransitionScope?,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope?,
|
||||
): Modifier {
|
||||
if (sharedTransitionScope == null || animatedVisibilityScope == null || key == null) return this
|
||||
with(sharedTransitionScope) {
|
||||
return this@sharedElementOrNone.sharedElement(
|
||||
sharedContentState = rememberSharedContentState(key = key),
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -18,8 +18,11 @@
|
||||
package dev.krtirtho.spotube.core.ui.component
|
||||
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInVertically
|
||||
import androidx.compose.animation.slideOutVertically
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
@ -42,6 +45,7 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
@ -149,6 +153,9 @@ fun TrackList(
|
||||
contentPadding: PaddingValues = PaddingValues(bottom = LocalAppShellBottomInset.current),
|
||||
simplified: Boolean = false,
|
||||
scrollable: Boolean = true,
|
||||
state: LazyListState? = null,
|
||||
collapsedHeader: (@Composable () -> Unit)? = null,
|
||||
isCollapsedHeaderShown: Boolean = false,
|
||||
) {
|
||||
var filterQuery by rememberSaveable { mutableStateOf("") }
|
||||
var sortBy by rememberSaveable { mutableStateOf(TrackSortOption.None) }
|
||||
@ -183,7 +190,7 @@ fun TrackList(
|
||||
}
|
||||
}
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val listState = state ?: rememberLazyListState()
|
||||
val density = LocalDensity.current
|
||||
val shouldLoadMore = remember(density) {
|
||||
derivedStateOf {
|
||||
@ -484,6 +491,17 @@ fun TrackList(
|
||||
}
|
||||
}
|
||||
|
||||
if (collapsedHeader != null) {
|
||||
AnimatedVisibility(
|
||||
visible = isCollapsedHeaderShown,
|
||||
enter = fadeIn() + slideInVertically { -it },
|
||||
exit = fadeOut() + slideOutVertically { -it },
|
||||
modifier = Modifier.align(Alignment.TopCenter).padding(horizontal = if (isCompact) 6.dp else 16.dp),
|
||||
) {
|
||||
collapsedHeader()
|
||||
}
|
||||
}
|
||||
|
||||
if (!useDropdownForOptions) {
|
||||
selectedTrackForOptions?.let { track ->
|
||||
TrackOptionsBottomSheet(
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
package dev.krtirtho.spotube.core.ui.component.cards
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalSharedTransitionApi
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.clickable
|
||||
@ -53,6 +54,9 @@ import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalAnimatedVisibilityScope
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
||||
import dev.krtirtho.spotube.core.ui.component.sharedElementOrNone
|
||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
@ -60,6 +64,7 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||
|
||||
@OptIn(ExperimentalSharedTransitionApi::class)
|
||||
@Composable
|
||||
fun PlayableCard(
|
||||
title: String,
|
||||
@ -70,8 +75,11 @@ fun PlayableCard(
|
||||
onPlay: (() -> Unit)? = null,
|
||||
onAddToQueue: (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
sharedElementKey: String? = null,
|
||||
) {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val sharedTransitionScope = LocalSharedTransitionScope.current
|
||||
val animatedVisibilityScope = LocalAnimatedVisibilityScope.current
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
@ -85,8 +93,15 @@ fun PlayableCard(
|
||||
AsyncImage(
|
||||
model = imageURL,
|
||||
contentDescription = title,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.clip(RoundedCornerShape(8.dp))
|
||||
.sharedElementOrNone(
|
||||
key = sharedElementKey,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
),
|
||||
contentScale = ContentScale.Crop,
|
||||
)
|
||||
|
||||
@ -98,7 +113,6 @@ fun PlayableCard(
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
this@Column.AnimatedVisibility(
|
||||
visible = isHovered,
|
||||
//fade in/out animation when hovered
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
|
||||
@ -52,12 +52,17 @@ class AlbumRepository(
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.loggedInFlow }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
.collect { loggedIn ->
|
||||
isLoggedIn = loggedIn
|
||||
invalidateCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isLoggedIn: Boolean = false
|
||||
|
||||
private val authPlugin get() = if (isLoggedIn) plugin else null
|
||||
|
||||
fun invalidateCaches() {
|
||||
albumInfoCache.invalidateAll()
|
||||
albumTracksCache.invalidateAll()
|
||||
@ -68,17 +73,15 @@ class AlbumRepository(
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
val album = metadataAlbumAPI.getAlbum(albumId)
|
||||
val isSaved = metadataAlbumAPI.isSavedAlbums(listOf(albumId)).firstOrNull() ?: false
|
||||
val isSaved = libraryRepository.isSavedAlbums(listOf(albumId)).firstOrNull() ?: false
|
||||
album to isSaved
|
||||
}
|
||||
}
|
||||
}?.also {
|
||||
libraryRepository.isSavedAlbums(listOf(albumId))
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getAlbumTracks(albumId: String, paginationStrategy: PaginationStrategy? = null) =
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
albumTracksCache.get(albumId to (paginationStrategy ?: PaginationStrategy.Offset(0, 20))) {
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
|
||||
@ -18,9 +18,11 @@
|
||||
package dev.krtirtho.spotube.modules.album
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -37,6 +39,7 @@ import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.share.ShareService
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader
|
||||
import dev.krtirtho.spotube.core.ui.component.CollectionDetails
|
||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||
import dev.krtirtho.spotube.core.ui.component.TrackList
|
||||
@ -75,6 +78,14 @@ fun AlbumScreen(albumId: String) {
|
||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val isCollapsedHeaderShown by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0 ||
|
||||
listState.firstVisibleItemScrollOffset > 400
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
currentUserId = libraryRepository.currentUser()?.id
|
||||
}
|
||||
@ -110,7 +121,7 @@ fun AlbumScreen(albumId: String) {
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ApplicationMainBar() }
|
||||
topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) }
|
||||
) { innerPadding ->
|
||||
when (state) {
|
||||
is AlbumScreenState.Loading -> {
|
||||
@ -165,14 +176,28 @@ fun AlbumScreen(albumId: String) {
|
||||
album?.artists?.joinToString { it.name }.orEmpty().ifBlank { "Unknown artist" }
|
||||
val ownerImageURL = album?.artists?.firstOrNull()?.thumbnails?.firstOrNull()?.url
|
||||
val firstArtistId = album?.artists?.firstOrNull()?.id
|
||||
val artworkUrl = album?.thumbnails?.firstOrNull()?.url.orEmpty()
|
||||
val isAlbumPlaying = currentCollectionEntry?.id == albumId &&
|
||||
playerState == PlayerState.PLAYING
|
||||
|
||||
TrackList(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
state = listState,
|
||||
collapsedHeader = {
|
||||
CollapsedCollectionHeader(
|
||||
title = album?.title ?: "Album",
|
||||
imageURL = artworkUrl,
|
||||
isPlaying = isAlbumPlaying,
|
||||
onPlay = viewModel::playAlbum,
|
||||
onBack = { navigationCommands.pop() },
|
||||
)
|
||||
},
|
||||
isCollapsedHeaderShown = isCollapsedHeaderShown,
|
||||
headerContent = {
|
||||
CollectionDetails(
|
||||
title = album?.title ?: "Loading album...",
|
||||
description = album?.description ?: "${album?.albumType?.name.orEmpty()} • ${album?.releaseDate.orEmpty()}",
|
||||
imageURL = album?.thumbnails?.firstOrNull()?.url.orEmpty(),
|
||||
imageURL = artworkUrl,
|
||||
ownerName = ownerName,
|
||||
ownerImageURL = ownerImageURL,
|
||||
onOwnerClick = {
|
||||
@ -185,9 +210,7 @@ fun AlbumScreen(albumId: String) {
|
||||
onPlay = viewModel::playAlbum,
|
||||
onShufflePlay = {},
|
||||
onAddToQueue = viewModel::addAlbumToQueue,
|
||||
isPlaying =
|
||||
currentCollectionEntry?.id == albumId &&
|
||||
playerState == PlayerState.PLAYING,
|
||||
isPlaying = isAlbumPlaying,
|
||||
isFollowing = savedAlbumIds.contains(albumId),
|
||||
onFollowClick = viewModel::toggleSavedAlbum,
|
||||
)
|
||||
|
||||
@ -392,25 +392,42 @@ class DownloadManager(
|
||||
}
|
||||
}
|
||||
|
||||
private fun applyMetadata(item: DownloadItem, filename: String, downloadDir: Path) {
|
||||
private suspend fun applyMetadata(item: DownloadItem, filename: String, downloadDir: Path) {
|
||||
val filePath = downloadDir / filename.toPath()
|
||||
if (!fileSystem.exists(filePath)) return
|
||||
|
||||
// TODO: Write metadata/audio tags to the downloaded file.
|
||||
// This is the dedicated placeholder for metadata tagging.
|
||||
// Implementation should use a platform-specific audio tagging library
|
||||
// to write ID3 tags (MP3), MP4 atoms (M4A/AAC), Vorbis comments (OGG/FLAC), etc.
|
||||
//
|
||||
// Tags to write from the track metadata:
|
||||
// - Title: item.title
|
||||
// - Artist: item.artists
|
||||
// - Album: item.album
|
||||
// - Track number: item.track?.trackNumber
|
||||
// - Disc number: item.track?.discNumber
|
||||
// - Duration: item.track?.durationMs
|
||||
// - Album art: item.track?.thumbnails (download and embed)
|
||||
// - ISRC: item.track?.isrcCode
|
||||
// - External URI: item.track?.externalUri
|
||||
val track = item.track ?: return
|
||||
|
||||
val coverBytes = try {
|
||||
val thumbnail = track.thumbnails?.maxByOrNull { it.width * it.height }
|
||||
if (thumbnail != null) {
|
||||
val response = httpClient.get(thumbnail.url)
|
||||
val channel = response.bodyAsChannel()
|
||||
val packet = channel.readRemaining()
|
||||
val bytes = packet.readByteArray()
|
||||
packet.close()
|
||||
bytes
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logger.w(e) { "Failed to download cover art for ${item.title}" }
|
||||
null
|
||||
}
|
||||
|
||||
try {
|
||||
uniffi.compose_app.writeAudioMetadata(
|
||||
filePath = filePath.toString(),
|
||||
title = item.title,
|
||||
artists = item.artists,
|
||||
album = item.album,
|
||||
trackNumber = track.trackNumber,
|
||||
discNumber = track.discNumber,
|
||||
coverBytes = coverBytes,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.w(e) { "Failed to write audio metadata for ${item.title}" }
|
||||
}
|
||||
}
|
||||
|
||||
private fun resolveDownloadDir(): Path {
|
||||
|
||||
@ -68,12 +68,17 @@ class LibraryRepository(
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.loggedInFlow }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
.collect { loggedIn ->
|
||||
isLoggedIn = loggedIn
|
||||
invalidateCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isLoggedIn: Boolean = false
|
||||
|
||||
private val authPlugin get() = if (isLoggedIn) plugin else null
|
||||
|
||||
fun invalidateCaches() {
|
||||
playlistCache.invalidateAll()
|
||||
albumCache.invalidateAll()
|
||||
@ -84,7 +89,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun savedPlaylists(paginationStrategy: PaginationStrategy? = null) =
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
playlistCache.get(
|
||||
key = paginationStrategy ?: PaginationStrategy.Offset(0, 20)
|
||||
) {
|
||||
@ -99,7 +104,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun savedAlbums(paginationStrategy: PaginationStrategy? = null) =
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
albumCache.get(
|
||||
key = paginationStrategy ?: PaginationStrategy.Offset(0, 20)
|
||||
) {
|
||||
@ -114,7 +119,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun savedArtists(paginationStrategy: PaginationStrategy? = null) =
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
artistCache.get(
|
||||
key = paginationStrategy ?: PaginationStrategy.Offset(0, 20)
|
||||
) {
|
||||
@ -131,11 +136,11 @@ class LibraryRepository(
|
||||
suspend fun isSavedPlaylists(ids: List<String>): List<Boolean> {
|
||||
val unknownIds = ids.filterNot { savedPlaylistIds.value.contains(it) }
|
||||
|
||||
if(unknownIds.isEmpty()) {
|
||||
if (unknownIds.isEmpty()) {
|
||||
return ids.map { true }
|
||||
}
|
||||
|
||||
val unknownStates = plugin?.let { plugin ->
|
||||
val unknownStates = authPlugin?.let { plugin ->
|
||||
val savedStates = pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataPlaylistAPI.isSavedPlaylists(unknownIds)
|
||||
@ -155,7 +160,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun savePlaylists(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataPlaylistAPI.savePlaylists(ids)
|
||||
@ -167,7 +172,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun removeSavedPlaylists(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataPlaylistAPI.removeSavedPlaylists(ids)
|
||||
@ -181,11 +186,11 @@ class LibraryRepository(
|
||||
suspend fun isSavedAlbums(ids: List<String>): List<Boolean> {
|
||||
val unknownIds = ids.filterNot { savedAlbumIds.value.contains(it) }
|
||||
|
||||
if(unknownIds.isEmpty()) {
|
||||
if (unknownIds.isEmpty()) {
|
||||
return ids.map { true }
|
||||
}
|
||||
|
||||
val unknownStates = plugin?.let { plugin ->
|
||||
val unknownStates = authPlugin?.let { plugin ->
|
||||
val savedStates = pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataAlbumAPI.isSavedAlbums(unknownIds)
|
||||
@ -205,7 +210,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun saveAlbums(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataAlbumAPI.saveAlbums(ids)
|
||||
@ -217,7 +222,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun removeSavedAlbums(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataAlbumAPI.removeSavedAlbums(ids)
|
||||
@ -231,11 +236,11 @@ class LibraryRepository(
|
||||
suspend fun isSavedArtists(ids: List<String>): List<Boolean> {
|
||||
val unknownIds = ids.filterNot { savedArtistIds.value.contains(it) }
|
||||
|
||||
if(unknownIds.isEmpty()) {
|
||||
if (unknownIds.isEmpty()) {
|
||||
return ids.map { true }
|
||||
}
|
||||
|
||||
val unknownStates = plugin?.let { plugin ->
|
||||
val unknownStates = authPlugin?.let { plugin ->
|
||||
val savedStates = pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataArtistAPI.isSavedArtists(unknownIds)
|
||||
@ -255,7 +260,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun saveArtists(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataArtistAPI.saveArtists(ids)
|
||||
@ -267,7 +272,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun removeSavedArtists(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataArtistAPI.removeSavedArtists(ids)
|
||||
@ -279,7 +284,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun currentUser(): MetadataUser? {
|
||||
return plugin?.let { plugin ->
|
||||
return authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataUserAPI.getUser("")
|
||||
@ -296,7 +301,7 @@ class LibraryRepository(
|
||||
imageBase64: String,
|
||||
trackIds: List<String>,
|
||||
): MetadataPlaylist? {
|
||||
return plugin?.let { plugin ->
|
||||
return authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
val result = metadataPlaylistAPI.createPlaylist(
|
||||
@ -324,7 +329,7 @@ class LibraryRepository(
|
||||
imageBase64: String?,
|
||||
trackIds: List<String>?,
|
||||
): MetadataPlaylist? {
|
||||
return plugin?.let { plugin ->
|
||||
return authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
val result = metadataPlaylistAPI.updatePlaylist(
|
||||
@ -344,7 +349,7 @@ class LibraryRepository(
|
||||
}
|
||||
|
||||
suspend fun addTracksToPlaylist(playlistId: String, trackIds: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataPlaylistAPI.addTracksToPlaylist(playlistId, trackIds)
|
||||
|
||||
@ -53,12 +53,15 @@ class PlaylistRepository(
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.loggedInFlow }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
.collect { loggedIn ->
|
||||
isLoggedIn = loggedIn
|
||||
invalidateCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isLoggedIn: Boolean = false
|
||||
|
||||
fun invalidateCaches() {
|
||||
playlistInfoCache.invalidateAll()
|
||||
playlistTracksCache.invalidateAll()
|
||||
@ -69,12 +72,10 @@ class PlaylistRepository(
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
val playlist = metadataPlaylistAPI.getPlaylist(playlistId)
|
||||
val isSaved = metadataPlaylistAPI.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false
|
||||
val isSaved = libraryRepository.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false
|
||||
playlist to isSaved
|
||||
}
|
||||
}
|
||||
}.also {
|
||||
libraryRepository.isSavedPlaylists(listOf(playlistId))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,10 +21,12 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -43,6 +45,7 @@ import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.share.ShareService
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader
|
||||
import dev.krtirtho.spotube.core.ui.component.CollectionDetails
|
||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||
import dev.krtirtho.spotube.core.ui.component.TrackList
|
||||
@ -87,6 +90,14 @@ fun PlaylistScreen(playlistId: String) {
|
||||
var showAddTracksDialog by remember { mutableStateOf(false) }
|
||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val isCollapsedHeaderShown by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0 ||
|
||||
listState.firstVisibleItemScrollOffset > 400
|
||||
}
|
||||
}
|
||||
|
||||
fun handleTrackOptionsAction(track: MetadataTrack, action: TrackOptionsAction) {
|
||||
viewModel.handleTrackOptionsAction(track, action)
|
||||
if (action is TrackOptionsAction.Share) {
|
||||
@ -105,7 +116,7 @@ fun PlaylistScreen(playlistId: String) {
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ApplicationMainBar() }
|
||||
topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) }
|
||||
) { innerPadding ->
|
||||
when (state) {
|
||||
is PlaylistScreenState.Loading -> {
|
||||
@ -164,6 +175,9 @@ fun PlaylistScreen(playlistId: String) {
|
||||
|
||||
val savedTrackIds by viewModel.savedTrackIds.collectAsStateWithLifecycle()
|
||||
|
||||
val artworkUrl = playlist?.thumbnails?.firstOrNull()?.url.orEmpty()
|
||||
val isPlaying = currentCollectionEntry?.id == playlistId &&
|
||||
playerState == PlayerState.PLAYING
|
||||
|
||||
fun getTrackOptionsState(track: MetadataTrack): TrackOptionsState {
|
||||
val currentTrackId = (currentQueueEntry as? QueueEntry.StreamingTrack)?.track?.id
|
||||
@ -180,20 +194,29 @@ fun PlaylistScreen(playlistId: String) {
|
||||
|
||||
TrackList(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
state = listState,
|
||||
collapsedHeader = {
|
||||
CollapsedCollectionHeader(
|
||||
title = playlist?.title ?: "Playlist",
|
||||
imageURL = artworkUrl,
|
||||
isPlaying = isPlaying,
|
||||
onPlay = viewModel::playPlaylist,
|
||||
onBack = { navigationCommands.pop() },
|
||||
)
|
||||
},
|
||||
isCollapsedHeaderShown = isCollapsedHeaderShown,
|
||||
headerContent = {
|
||||
CollectionDetails(
|
||||
title = playlist?.title ?: "Loading playlist...",
|
||||
description = playlist?.description.orEmpty(),
|
||||
imageURL = playlist?.thumbnails?.firstOrNull()?.url.orEmpty(),
|
||||
imageURL = artworkUrl,
|
||||
ownerName = ownerName,
|
||||
ownerImageURL = ownerImageURL,
|
||||
onOwnerClick = {},
|
||||
onPlay = viewModel::playPlaylist,
|
||||
onShufflePlay = {},
|
||||
onAddToQueue = viewModel::addPlaylistToQueue,
|
||||
isPlaying =
|
||||
currentCollectionEntry?.id == playlistId &&
|
||||
playerState == PlayerState.PLAYING,
|
||||
isPlaying = isPlaying,
|
||||
isFollowing = savedPlaylistIds.contains(playlistId),
|
||||
onFollowClick = viewModel::toggleSavedPlaylist,
|
||||
showFollowButton = playlistId != "saved_tracks",
|
||||
|
||||
@ -111,7 +111,11 @@ class PlaylistViewModel(
|
||||
}
|
||||
|
||||
private suspend fun loadCurrentUser() {
|
||||
runCatching {
|
||||
_currentUserId.value = libraryRepository.currentUser()?.id
|
||||
}.onFailure { e ->
|
||||
logger.e(e) { "Failed to load current user" }
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadInitialData() = runCatching {
|
||||
@ -159,12 +163,16 @@ class PlaylistViewModel(
|
||||
|
||||
fun toggleSavedPlaylist() {
|
||||
viewModelScope.launch {
|
||||
runCatching {
|
||||
val isLiked = libraryRepository.isSavedPlaylists(listOf(playlistId)).firstOrNull() ?: false
|
||||
if (isLiked) {
|
||||
libraryRepository.removeSavedPlaylists(listOf(playlistId))
|
||||
} else {
|
||||
libraryRepository.savePlaylists(listOf(playlistId))
|
||||
}
|
||||
}.onFailure { e ->
|
||||
logger.e(e) { "Failed to toggle saved playlist" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,12 +56,17 @@ class SavedTracksRepository(
|
||||
.filterNotNull()
|
||||
.flatMapLatest { it.loggedInFlow }
|
||||
.distinctUntilChanged()
|
||||
.collect {
|
||||
.collect { loggedIn ->
|
||||
isLoggedIn = loggedIn
|
||||
invalidateCaches()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var isLoggedIn: Boolean = false
|
||||
|
||||
private val authPlugin get() = if (isLoggedIn) plugin else null
|
||||
|
||||
fun invalidateCaches() {
|
||||
savedTracksCache.invalidateAll()
|
||||
totalCountCache.invalidateAll()
|
||||
@ -69,7 +74,7 @@ class SavedTracksRepository(
|
||||
}
|
||||
|
||||
suspend fun getSavedTracks(paginationStrategy: PaginationStrategy? = null) =
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
val strategy = paginationStrategy ?: PaginationStrategy.Offset(0, 50)
|
||||
savedTracksCache.get(strategy) {
|
||||
val tracks = pluginManager.withScope {
|
||||
@ -95,7 +100,7 @@ class SavedTracksRepository(
|
||||
}
|
||||
|
||||
suspend fun saveTracks(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataTrackAPI.saveTracks(ids)
|
||||
@ -108,7 +113,7 @@ class SavedTracksRepository(
|
||||
}
|
||||
|
||||
suspend fun removeSavedTracks(ids: List<String>) {
|
||||
plugin?.let { plugin ->
|
||||
authPlugin?.let { plugin ->
|
||||
pluginManager.withScope {
|
||||
plugin.use {
|
||||
metadataTrackAPI.removeSavedTracks(ids)
|
||||
|
||||
@ -18,9 +18,11 @@
|
||||
package dev.krtirtho.spotube.modules.saved_tracks
|
||||
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@ -38,6 +40,7 @@ import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.share.ShareService
|
||||
import dev.krtirtho.spotube.core.ui.component.ApplicationMainBar
|
||||
import dev.krtirtho.spotube.core.ui.component.CollapsedCollectionHeader
|
||||
import dev.krtirtho.spotube.core.ui.component.CollectionDetails
|
||||
import dev.krtirtho.spotube.core.ui.component.ErrorDisplay
|
||||
import dev.krtirtho.spotube.core.ui.component.TrackList
|
||||
@ -77,6 +80,14 @@ fun SavedTracksScreen() {
|
||||
var tracksToAddToPlaylist by remember { mutableStateOf<List<MetadataTrack>>(emptyList()) }
|
||||
var currentUserId by remember { mutableStateOf<String?>(null) }
|
||||
|
||||
val listState = rememberLazyListState()
|
||||
val isCollapsedHeaderShown by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0 ||
|
||||
listState.firstVisibleItemScrollOffset > 400
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
currentUserId = libraryRepository.currentUser()?.id
|
||||
}
|
||||
@ -112,7 +123,7 @@ fun SavedTracksScreen() {
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = { ApplicationMainBar() }
|
||||
topBar = { ApplicationMainBar(backButton = !isCollapsedHeaderShown) }
|
||||
) { innerPadding ->
|
||||
when (state) {
|
||||
is SavedTracksScreenState.Loading -> {
|
||||
@ -165,8 +176,23 @@ fun SavedTracksScreen() {
|
||||
is SavedTracksScreenState.Data -> {
|
||||
val dataState = state as SavedTracksScreenState.Data
|
||||
|
||||
val isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
||||
playerState == PlayerState.PLAYING
|
||||
|
||||
TrackList(
|
||||
modifier = Modifier.padding(innerPadding),
|
||||
state = listState,
|
||||
collapsedHeader = {
|
||||
CollapsedCollectionHeader(
|
||||
title = "Saved Tracks",
|
||||
imageURL = "",
|
||||
imageResource = Res.drawable.liked_tracks,
|
||||
isPlaying = isPlaying,
|
||||
onPlay = viewModel::playSavedTracks,
|
||||
onBack = { navigationCommands.pop() },
|
||||
)
|
||||
},
|
||||
isCollapsedHeaderShown = isCollapsedHeaderShown,
|
||||
headerContent = {
|
||||
CollectionDetails(
|
||||
title = "Saved Tracks",
|
||||
@ -179,8 +205,7 @@ fun SavedTracksScreen() {
|
||||
onPlay = viewModel::playSavedTracks,
|
||||
onShufflePlay = {},
|
||||
onAddToQueue = viewModel::addSavedTracksToQueue,
|
||||
isPlaying = currentCollectionEntry is QueueCollectionEntry.SavedTracks &&
|
||||
playerState == PlayerState.PLAYING,
|
||||
isPlaying = isPlaying,
|
||||
isFollowing = false,
|
||||
onFollowClick = {},
|
||||
showFollowButton = false,
|
||||
|
||||
160
composeApp/src/commonMain/rust/lib.rs
Normal file
160
composeApp/src/commonMain/rust/lib.rs
Normal file
@ -0,0 +1,160 @@
|
||||
use lofty::config::WriteOptions;
|
||||
use lofty::file::{AudioFile, FileType, TaggedFileExt};
|
||||
use lofty::picture::Picture;
|
||||
use lofty::probe::Probe;
|
||||
use lofty::tag::{Accessor, Tag, TagExt};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(uniffi::Record)]
|
||||
pub struct AudioMetadata {
|
||||
pub title: Option<String>,
|
||||
pub artists: Vec<String>,
|
||||
pub album: Option<String>,
|
||||
pub duration_ms: i64,
|
||||
pub track_number: Option<u32>,
|
||||
pub disc_number: Option<u32>,
|
||||
pub cover_bytes: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, uniffi::Error)]
|
||||
pub enum AudioTagError {
|
||||
FileNotFound,
|
||||
ReadError { reason: String },
|
||||
WriteError { reason: String },
|
||||
}
|
||||
|
||||
impl std::fmt::Display for AudioTagError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
AudioTagError::FileNotFound => write!(f, "File not found"),
|
||||
AudioTagError::ReadError { reason } => write!(f, "Read error: {}", reason),
|
||||
AudioTagError::WriteError { reason } => write!(f, "Write error: {}", reason),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn read_audio_metadata(file_path: String) -> Result<AudioMetadata, AudioTagError> {
|
||||
let path = Path::new(&file_path);
|
||||
if !path.exists() {
|
||||
return Err(AudioTagError::FileNotFound);
|
||||
}
|
||||
|
||||
let tagged_file = Probe::open(path)
|
||||
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
|
||||
.read()
|
||||
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
|
||||
|
||||
let duration = tagged_file.properties().duration();
|
||||
let duration_ms = duration.as_millis() as i64;
|
||||
|
||||
let tag = tagged_file.primary_tag().or_else(|| tagged_file.first_tag());
|
||||
|
||||
let mut title = None;
|
||||
let mut artists = Vec::new();
|
||||
let mut album = None;
|
||||
let mut track_number = None;
|
||||
let mut disc_number = None;
|
||||
let mut cover_bytes = None;
|
||||
|
||||
if let Some(tag) = tag {
|
||||
title = tag.title().map(|s| s.to_string());
|
||||
|
||||
if let Some(artist) = tag.artist() {
|
||||
artists = artist
|
||||
.split(';')
|
||||
.map(|s| s.trim().to_string())
|
||||
.filter(|s| !s.is_empty())
|
||||
.collect();
|
||||
}
|
||||
|
||||
album = tag.album().map(|s| s.to_string());
|
||||
track_number = tag.track();
|
||||
disc_number = tag.disk();
|
||||
|
||||
if let Some(pic) = tag.pictures().first() {
|
||||
cover_bytes = Some(pic.data().to_vec());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AudioMetadata {
|
||||
title,
|
||||
artists,
|
||||
album,
|
||||
duration_ms,
|
||||
track_number,
|
||||
disc_number,
|
||||
cover_bytes,
|
||||
})
|
||||
}
|
||||
|
||||
#[uniffi::export]
|
||||
fn write_audio_metadata(
|
||||
file_path: String,
|
||||
title: String,
|
||||
artists: String,
|
||||
album: Option<String>,
|
||||
track_number: Option<i32>,
|
||||
disc_number: Option<i32>,
|
||||
cover_bytes: Option<Vec<u8>>,
|
||||
) -> Result<(), AudioTagError> {
|
||||
let path = Path::new(&file_path);
|
||||
if !path.exists() {
|
||||
return Err(AudioTagError::FileNotFound);
|
||||
}
|
||||
|
||||
let mut tagged_file = Probe::open(path)
|
||||
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?
|
||||
.read()
|
||||
.map_err(|e| AudioTagError::ReadError { reason: e.to_string() })?;
|
||||
|
||||
let file_type = tagged_file.file_type();
|
||||
|
||||
if tagged_file.primary_tag().is_none() && tagged_file.first_tag().is_none() {
|
||||
let new_tag = match file_type {
|
||||
FileType::Mpeg => Tag::new(lofty::tag::TagType::Id3v2),
|
||||
FileType::Mp4 => Tag::new(lofty::tag::TagType::Mp4Ilst),
|
||||
FileType::Flac => Tag::new(lofty::tag::TagType::VorbisComments),
|
||||
FileType::Opus | FileType::Vorbis => Tag::new(lofty::tag::TagType::VorbisComments),
|
||||
_ => Tag::new(lofty::tag::TagType::Id3v2),
|
||||
};
|
||||
tagged_file.insert_tag(new_tag);
|
||||
}
|
||||
|
||||
let tag = if tagged_file.primary_tag().is_some() {
|
||||
tagged_file.primary_tag_mut().unwrap()
|
||||
} else if tagged_file.first_tag().is_some() {
|
||||
tagged_file.first_tag_mut().unwrap()
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
tag.set_title(title);
|
||||
tag.set_artist(artists);
|
||||
|
||||
if let Some(album_name) = album {
|
||||
tag.set_album(album_name);
|
||||
}
|
||||
|
||||
if let Some(num) = track_number {
|
||||
tag.set_track(num as u32);
|
||||
}
|
||||
|
||||
if let Some(num) = disc_number {
|
||||
tag.set_disk(num as u32);
|
||||
}
|
||||
|
||||
if let Some(bytes) = cover_bytes {
|
||||
if !bytes.is_empty() {
|
||||
let picture = Picture::unchecked(bytes).build();
|
||||
tag.push_picture(picture);
|
||||
}
|
||||
}
|
||||
|
||||
tag.save_to_path(path, WriteOptions::default())
|
||||
.map_err(|e| AudioTagError::WriteError { reason: e.to_string() })?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
uniffi::setup_scaffolding!();
|
||||
@ -54,16 +54,23 @@ class JvmLocalMediaDiscoveryService : LocalMediaDiscoveryService {
|
||||
if (scanned.incrementAndGet() > MAX_FILES_PER_SCAN) return@forEach
|
||||
val parent = filePath.parent?.absolutePathString() ?: return@forEach
|
||||
val trackPath = filePath.toAbsolutePath().toString()
|
||||
println("Debug: Found track path = $trackPath")
|
||||
|
||||
val metadata = try {
|
||||
uniffi.compose_app.readAudioMetadata(trackPath)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val track = LocalMediaTrack(
|
||||
path = trackPath,
|
||||
name = filePath.nameWithoutExtension.ifBlank {
|
||||
name = metadata?.title?.takeIf { it.isNotBlank() }
|
||||
?: filePath.nameWithoutExtension.ifBlank {
|
||||
filePath.fileName.toString()
|
||||
},
|
||||
artists = emptyList(),
|
||||
durationMs = 0L,
|
||||
album = null,
|
||||
coverBytes = null,
|
||||
artists = metadata?.artists?.takeIf { it.isNotEmpty() } ?: emptyList(),
|
||||
durationMs = metadata?.durationMs ?: 0L,
|
||||
album = metadata?.album,
|
||||
coverBytes = metadata?.coverBytes,
|
||||
)
|
||||
folders.getOrPut(parent) { mutableListOf() }.add(track)
|
||||
}
|
||||
|
||||
@ -72,6 +72,8 @@ spotube-gradle = "0.1.0"
|
||||
cryptography = "0.6.0"
|
||||
reorderable = "3.1.0"
|
||||
vlcjBundler = "0.1.0"
|
||||
uniffi = "0.3.7"
|
||||
kotlinx-atomicfu = "0.33.0"
|
||||
|
||||
[libraries]
|
||||
appdirs = { module = "net.harawata:appdirs", version.ref = "appdirs" }
|
||||
@ -175,3 +177,6 @@ zipline-gradle-plugin = { id = "app.cash.zipline", version.ref = "ziplineVersion
|
||||
mavenPublish = { id = "maven-publish" }
|
||||
spotubeGradle = { id = "dev.krtirtho.spotube.gradle-plugin", version.ref = "spotube-gradle" }
|
||||
vlcjBundler = { id = "dev.krtirtho.vlcj-bundler.gradle-plugin", version.ref = "vlcjBundler" }
|
||||
kotlinxAtomicFu = { id = "org.jetbrains.kotlinx.atomicfu", version.ref = "kotlinx-atomicfu" }
|
||||
uniffi = { id = "dev.gobley.uniffi", version.ref = "uniffi" }
|
||||
cargo = { id = "dev.gobley.cargo", version.ref = "uniffi" }
|
||||
@ -23,8 +23,4 @@ class RealMetadataUserAPI : MetadataUserAPI {
|
||||
override suspend fun getUser(id: String): MetadataUser? {
|
||||
return FakeMetadataStore.getUser(id)
|
||||
}
|
||||
|
||||
override suspend fun me(): MetadataUser? {
|
||||
return FakeMetadataStore.getCurrentUser()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user