Compare commits

...

14 Commits

Author SHA1 Message Date
Jonatan Nyberg
cf6253a1d8
Merge b0327fd224 into aa83708499 2026-10-17 05:19:38 -07:00
Kingkor Roy Tirtho
aa83708499 feat(graalvm): enable GraalVM native image support and update packaging configurations 2026-10-17 17:01:40 +06:00
Kingkor Roy Tirtho
62c1fe7902 feat: enhance Android build configuration with stable/nightly flavors and update launcher icons 2026-10-16 21:34:44 +06:00
Kingkor Roy Tirtho
9fba2aa33d feat(window): implement window state persistence and restoration 2026-10-16 20:49:16 +06:00
Kingkor Roy Tirtho
9db35e092b feat(server): implement port fallback mechanism for playback proxy server 2026-10-16 20:06:12 +06:00
Kingkor Roy Tirtho
2474c26a83 refactor: move package name from dev.krtirtho to org.spotube 2026-10-16 15:37:15 +06:00
Kingkor Roy Tirtho
7743d6d4c3 feat(lyrics): implement SyncedLyricsList component and enhance LyricsViewModel for better state management 2026-10-16 15:12:10 +06:00
Kingkor Roy Tirtho
77a9cef3a1 fix(stream-proxy): improve logging for cache timeout and handle disconnection scenarios 2026-10-16 14:44:39 +06:00
Kingkor Roy Tirtho
c9d16a25c6 fix(audio-player): android shuffle wasnt working 2026-10-16 14:36:27 +06:00
Kingkor Roy Tirtho
da394d40ea fix(audio-api): enhance fallback mechanism for text search results in YouTube 2026-10-16 14:29:43 +06:00
Kingkor Roy Tirtho
09d77211b9 fix(newpipe): fall back through multiple searches when ISRC search misses
NewPipe durations are seconds, not ms, so duration scoring never matched.
Fix the uploader match direction, normalize titles, and walk a ladder of
searches (ISRC, songs, videos, title only) until a confident match is found.
Return nothing rather than unrelated results. Add detailed ISRC/NewPipe logs.
2026-10-16 14:26:39 +06:00
Kingkor Roy Tirtho
61dd89f3ed fix(stream-proxy): stream upstream audio instead of buffering the whole track
Ktor's request() buffers the full body before returning, so ExoPlayer timed
out waiting for headers. Use prepareRequest().execute{} and tee the body to
the cache file, committing only complete 200 responses. Treat player
disconnects (broken pipe) as debug-level noise rather than upstream errors.
2026-10-16 14:26:39 +06:00
Kingkor Roy Tirtho
ba62ff03c3 feat(user-agents): implement dynamic user agent fetching and caching mechanism 2026-10-16 13:47:08 +06:00
Jonatan Nyberg
b0327fd224 translation: add Swedish resource translations
Signed-off-by: Jonatan Nyberg <84130654+NickWick13@users.noreply.github.com>
2026-09-20 14:17:16 +03:00
568 changed files with 6983 additions and 4270 deletions

View File

@ -76,7 +76,7 @@ body:
description: Where did you install Spotube from? description: Where did you install Spotube from?
multiple: true multiple: true
options: options:
- "Website (spotube.cc)" - "Website (spotube.org)"
- "GitHub Releases (Binary)" - "GitHub Releases (Binary)"
- "GitHub Actions (Nightly Binary)" - "GitHub Actions (Nightly Binary)"
- "Play Store (Android)" - "Play Store (Android)"

View File

@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Ask a question - name: Ask a question
url: https://github.com/KRTirtho/spotube/discussions/new?category=q-a url: https://github.com/team-spotube/spotube/discussions/new?category=q-a
about: The place for questions, support and feature requests about: The place for questions, support and feature requests

View File

@ -27,8 +27,8 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview COMPOSE_WEBVIEW_REPO: KRTirtho/ComposeNativeWebview
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce COMPOSE_WEBVIEW_COMMIT: cbd36f88927298a28a2d64c6acf7ec3b2def6f9d
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin
@ -141,11 +141,16 @@ jobs:
EOF EOF
- name: Assemble Release - name: Assemble Release
run: ./gradlew :composeApp:assembleRelease run: |
if [ "${{ inputs.release_type }}" = "nightly" ]; then
./gradlew :composeApp:assembleNightlyRelease -Pnightly=true -PbuildNumber=${{ github.run_number }}
else
./gradlew :composeApp:assembleStableRelease
fi
- name: Rename APK - name: Rename APK
run: | run: |
APK_FILE=$(find composeApp/build/outputs/apk/release -name "*.apk" -type f | head -1) APK_FILE=$(find composeApp/build/outputs/apk -path "*elease*" -name "*.apk" -type f | head -1)
cp "$APK_FILE" Spotube-android-all-arch.apk cp "$APK_FILE" Spotube-android-all-arch.apk
- name: Upload APK - name: Upload APK
@ -176,7 +181,7 @@ jobs:
- name: Install packaging tools - name: Install packaging tools
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y rpm fuse sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@ -193,13 +198,19 @@ jobs:
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v4
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package Linux - name: Package Linux
run: ./gradlew :composeApp:packageReleaseDeb :composeApp:packageReleaseRpm run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Linux artifacts - name: Rename Linux artifacts
run: | run: |
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1) DEB_FILE=$(find composeApp/build/compose/binaries/main/graalvm-deb -maxdepth 1 -name "*.deb" -type f | head -1)
RPM_FILE=$(find composeApp/build -name "*.rpm" -type f | head -1) RPM_FILE=$(find composeApp/build/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
cp "$DEB_FILE" Spotube-linux-x86_64.deb cp "$DEB_FILE" Spotube-linux-x86_64.deb
cp "$RPM_FILE" Spotube-linux-x86_64.rpm cp "$RPM_FILE" Spotube-linux-x86_64.rpm
@ -237,7 +248,7 @@ jobs:
- name: Install packaging tools - name: Install packaging tools
run: | run: |
sudo apt-get update sudo apt-get update
sudo apt-get install -y rpm fuse sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
- name: Set up JDK 21 - name: Set up JDK 21
uses: actions/setup-java@v4 uses: actions/setup-java@v4
@ -254,13 +265,19 @@ jobs:
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v4
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package Linux - name: Package Linux
run: ./gradlew :composeApp:packageReleaseDeb :composeApp:packageReleaseRpm run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Linux artifacts - name: Rename Linux artifacts
run: | run: |
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1) DEB_FILE=$(find composeApp/build/compose/binaries/main/graalvm-deb -maxdepth 1 -name "*.deb" -type f | head -1)
RPM_FILE=$(find composeApp/build -name "*.rpm" -type f | head -1) RPM_FILE=$(find composeApp/build/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
cp "$DEB_FILE" Spotube-linux-aarch64.deb cp "$DEB_FILE" Spotube-linux-aarch64.deb
cp "$RPM_FILE" Spotube-linux-aarch64.rpm cp "$RPM_FILE" Spotube-linux-aarch64.rpm
@ -307,13 +324,25 @@ jobs:
- name: Set up Gradle - name: Set up Gradle
uses: gradle/actions/setup-gradle@v4 uses: gradle/actions/setup-gradle@v4
# native-image links with MSVC
- name: Set up MSVC
uses: ilammy/msvc-dev-cmd@v1
- name: Cache GraalVM toolchain
uses: actions/cache@v4
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package EXE - name: Package EXE
run: ./gradlew :composeApp:packageReleaseExe run: ./gradlew :composeApp:packageGraalvmExe -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename Windows artifacts - name: Rename Windows artifacts
shell: pwsh shell: pwsh
run: | run: |
$EXE_FILE = Get-ChildItem -Path composeApp/build -Filter "*.exe" -Recurse | Select-Object -First 1 # Top level only: subfolders hold the unpacked app, and __* are electron-builder helpers
$EXE_FILE = Get-ChildItem -Path composeApp/build/compose/binaries/main/graalvm-exe -Filter "*.exe" |
Where-Object { $_.Name -notlike '__*' } | Select-Object -First 1
Copy-Item $EXE_FILE.FullName Spotube-windows-x86_64-setup.exe Copy-Item $EXE_FILE.FullName Spotube-windows-x86_64-setup.exe
- name: Upload EXE - name: Upload EXE
@ -356,12 +385,18 @@ jobs:
- name: Make gradlew executable - name: Make gradlew executable
run: chmod +x gradlew run: chmod +x gradlew
- name: Cache GraalVM toolchain
uses: actions/cache@v4
with:
path: ~/.gradle/nucleus/graalvm
key: graalvm-${{ runner.os }}-${{ runner.arch }}
- name: Package DMG - name: Package DMG
run: ./gradlew :composeApp:packageReleaseDmg run: ./gradlew :composeApp:packageGraalvmDmg -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
- name: Rename macOS artifact - name: Rename macOS artifact
run: | run: |
DMG_FILE=$(find composeApp/build -name "*.dmg" -type f | head -1) DMG_FILE=$(find composeApp/build/compose/binaries/main/graalvm-dmg -maxdepth 1 -name "*.dmg" -type f | head -1)
cp "$DMG_FILE" Spotube-macos-universal.dmg cp "$DMG_FILE" Spotube-macos-universal.dmg
- name: Upload DMG - name: Upload DMG
@ -389,18 +424,35 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# The build resolves its plugins from mavenLocal, so Gradle needs the same setup as the build jobs
- name: Download maven local
uses: actions/download-artifact@v4
with:
name: maven-local
path: ~/.m2/repository/
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4
- name: Set release variables - name: Set release variables
id: vars id: vars
run: | run: |
if [ "${{ inputs.release_type }}" = "stable" ]; then if [ "${{ inputs.release_type }}" = "stable" ]; then
VERSION=$(grep 'versionName' composeApp/build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/') chmod +x gradlew
VERSION=$(./gradlew -q :composeApp:printVersion)
TAG="v${VERSION}" TAG="v${VERSION}"
echo "is_prerelease=false" >> "$GITHUB_OUTPUT" echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
echo "is_draft=true" >> "$GITHUB_OUTPUT" echo "is_draft=true" >> "$GITHUB_OUTPUT"
echo "release_name=Spotube ${TAG}" >> "$GITHUB_OUTPUT" echo "release_name=Spotube ${TAG}" >> "$GITHUB_OUTPUT"
else else
TAG="nightly" TAG="nightly"
VERSION="$(date +%Y-%m-%d)" VERSION="$(date +%Y-%m-%d)-${{ github.run_number }}"
echo "is_prerelease=true" >> "$GITHUB_OUTPUT" echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
echo "is_draft=false" >> "$GITHUB_OUTPUT" echo "is_draft=false" >> "$GITHUB_OUTPUT"
echo "release_name=Nightly ${VERSION}" >> "$GITHUB_OUTPUT" echo "release_name=Nightly ${VERSION}" >> "$GITHUB_OUTPUT"

View File

@ -5,14 +5,14 @@
- `:composeApp` uses custom KMP source sets (`mobileMain`, `androidJvmMain`) with explicit `dependsOn`; Gradle prints "Default Kotlin Hierarchy Template Not Applied Correctly" warning. Treat as known/expected. - `:composeApp` uses custom KMP source sets (`mobileMain`, `androidJvmMain`) with explicit `dependsOn`; Gradle prints "Default Kotlin Hierarchy Template Not Applied Correctly" warning. Treat as known/expected.
## Real app entrypoints ## Real app entrypoints
- Desktop/JVM: `composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt` (`mainClass = dev.krtirtho.spotube.MainKt`). - Desktop/JVM: `composeApp/src/jvmMain/kotlin/org/spotube/spotube/main.kt` (`mainClass = org.spotube.spotube.MainKt`).
- Android: `composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/MainActivity.kt`. - Android: `composeApp/src/androidMain/kotlin/org/spotube/spotube/MainActivity.kt`.
- iOS bridge: `composeApp/src/iosMain/kotlin/dev/krtirtho/spotube/MainViewController.kt` and `iosApp/iosApp/ContentView.swift`. - iOS bridge: `composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt` and `iosApp/iosApp/ContentView.swift`.
## High-value commands ## High-value commands
- Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only. - Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only.
- Run desktop: `:composeApp:run` - Run desktop: `:composeApp:run`
- Build Android debug: `:composeApp:assembleDebug` - Build Android debug: `:composeApp:assembleStableDebug` (or `assembleNightlyDebug`). Android has `stable`/`nightly` product flavors (nightly = beta channel: `.nightly` applicationId suffix, own icons/name in `composeApp/src/nightly/res`). Desktop icons come from `composeApp/icons/{stable,nightly}`, chosen with `-Pnightly=true`.
- Module checks: `:composeApp:check`, `:plugin_interfaces:check`, `:js_plugin_example:check` - Module checks: `:composeApp:check`, `:plugin_interfaces:check`, `:js_plugin_example:check`
- Focused tests: `:composeApp:jvmTest`, `:composeApp:iosSimulatorArm64Test`, `:plugin_interfaces:jvmTest`, `:plugin_interfaces:jsTest`, `:js_plugin_example:jsTest` - Focused tests: `:composeApp:jvmTest`, `:composeApp:iosSimulatorArm64Test`, `:plugin_interfaces:jvmTest`, `:plugin_interfaces:jsTest`, `:js_plugin_example:jsTest`
- No lint/typecheck/formatter tasks are configured; `:composeApp:check` is the only aggregated check. - No lint/typecheck/formatter tasks are configured; `:composeApp:check` is the only aggregated check.
@ -22,9 +22,18 @@
- Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`). - Kotlin: `2.3.0`, JVM target: `11` (compile/target compatibility in both `composeApp/build.gradle.kts` and `plugin_interfaces/build.gradle.kts`).
- **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority). - **compose-webview (desktop) dispatcher hijack:** `dev.nucleusframework:composewebview` transitively pulls `nucleus.decorated-window-tao`, which registers a `TaoMainDispatcherFactory` (`loadPriority = 100`) via `META-INF/services` that hijacks `Dispatchers.Main` away from the Swing EDT. This breaks lifecycle's `enforceMainThreadIfNeeded` (window fails with "Method addObserver must be called on the main thread"). The app overrides it with `SwingMainDispatcherFactory` (`composeApp/src/jvmMain/.../core/coroutines/`, `loadPriority = Int.MAX_VALUE`) which pins `Dispatchers.Main` back to the Swing EDT. Keep that factory + its `META-INF/services/kotlinx.coroutines.internal.MainDispatcherFactory` resource; the long-term fix is in the webview (make the Tao dispatcher opt-in or lower its priority).
## GraalVM native image (desktop)
- Enabled via `graalvm { }` inside `nucleus.application` in `composeApp/build.gradle.kts`. Nucleus downloads GraalVM CE itself (or uses `$GRAALVM_HOME`). Linux needs `gcc` + `patchelf`.
- Build: `:composeApp:createGraalvmNativeDistributable` (output `composeApp/build/compose/binaries/main/graalvm-app/`), installers via `packageGraalvm{Deb,Rpm,AppImage,Dmg,Msi,Exe}`. A build takes ~10 min and uses all cores and ~15 GB RAM.
- Hand-maintained + agent-recorded metadata lives in `composeApp/graalvm/reachability-metadata.json`. After adding features that use reflection/JNI/JNA (vlcj, Zipline, plugins), run `:composeApp:runWithNativeAgent`, exercise the feature, then close the window normally (the merge only happens on a clean exit).
- `uk.co.caprica.vlcj.binding.lib.LibVlc` must stay registered (`allDeclaredMethods` + `jniAccessible`): JNA direct mapping finds its `native` methods by reflection.
- The `*UberJarForCurrentOS` task excludes other OSes'/CPUs' native libs and stale CEF binaries, because native-image embeds every registered resource into the binary. On Linux, `copyGraalvmJvmSo` is disabled and `copyGraalvmAwtSoLibs` prefers native-image's own libs (10 KB `libjvm.so`/`libjava.so` shims instead of the JDK's 28 MB `libjvm.so`).
- `isProductionBuild` (top of `composeApp/build.gradle.kts`) is true for `package*`/`create*`/`notarize*`/`nativeImageCompile` tasks, or with `-Pproduction=true|false`. It gates the Rust release profile (Gobley `jvmVariant`) so `:composeApp:run` keeps fast debug Rust builds. Put new slow, ship-only optimizations behind it.
- Full user-facing build docs are in `README.md` ("Building from source").
## Codegen and plugin packaging ## Codegen and plugin packaging
- JS plugin bundles: `:js_plugin_example:packageDevelopmentPlugin` and `:js_plugin_example:packageProductionPlugin`. Output is `.smplug` files in `js_plugin_example/build/distributions/`. - JS plugin bundles: `:js_plugin_example:packageDevelopmentPlugin` and `:js_plugin_example:packageProductionPlugin`. Output is `.smplug` files in `js_plugin_example/build/distributions/`.
- Zipline plugin entrypoint: `mainFunction = "dev.krtirtho.js_plugin_example.main"` in `js_plugin_example/build.gradle.kts`. Plugin metadata from `js_plugin_example/plugin.json`. - Zipline plugin entrypoint: `mainFunction = "org.spotube.js_plugin_example.main"` in `js_plugin_example/build.gradle.kts`. Plugin metadata from `js_plugin_example/plugin.json`.
## Plugin architecture ## Plugin architecture
- `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system. - `plugin_interfaces` exports `zipline.core` and `semver` as API. `composeApp` depends on it for the plugin system.
@ -53,7 +62,7 @@
- `prepare-deps` job checks out & publishes to `mavenLocal`: - `prepare-deps` job checks out & publishes to `mavenLocal`:
- `kdroidFilter/ComposeNativeWebview` (compose-webview) - `kdroidFilter/ComposeNativeWebview` (compose-webview)
- `team-spotube/gradle-plugin` (spotubeGradle + vlcjBundler plugins) - `team-spotube/gradle-plugin` (spotubeGradle + vlcjBundler plugins)
- Build jobs per platform (Android, Linux, Windows, macOS), all `needs: prepare-deps`. - Build jobs per platform (Android, Linux, Windows, macOS), all `needs: prepare-deps`. Desktop jobs ship GraalVM native installers (`packageGraalvm{Deb,Rpm,Exe,Dmg}`, output in `composeApp/build/compose/binaries/main/graalvm-<format>/`), not jpackage/JVM ones.
- **Stable**: reads `versionName` from `composeApp/build.gradle.kts`, tag = `v{version}`, draft release. - **Stable**: reads `versionName` from `composeApp/build.gradle.kts`, tag = `v{version}`, draft release.
- **Nightly**: builds with `-PversionName=nightly`, tag = `nightly` (updates existing), prerelease. - **Nightly**: builds with `-PversionName=nightly`, tag = `nightly` (updates existing), prerelease.
- **Android signing**: decodes `secrets.KEYSTORE` (base64) → `composeApp/upload-keystore.jks`, writes signing config into `local.properties` from secrets. - **Android signing**: decodes `secrets.KEYSTORE` (base64) → `composeApp/upload-keystore.jks`, writes signing config into `local.properties` from secrets.
@ -70,6 +79,6 @@
- **Adaptive Components:** Use `AdaptiveDropdownBottomSheet` or `AdaptiveDialogBottomSheet` inside `commonMain` for adaptive layouts[cite: 2]. Do NOT create platform-specific `expect/actual` variants unless platform-native APIs (like windowing or native scrollbars) are explicitly required[cite: 2]. - **Adaptive Components:** Use `AdaptiveDropdownBottomSheet` or `AdaptiveDialogBottomSheet` inside `commonMain` for adaptive layouts[cite: 2]. Do NOT create platform-specific `expect/actual` variants unless platform-native APIs (like windowing or native scrollbars) are explicitly required[cite: 2].
### 3. Verification & Diagnostic Loops ### 3. Verification & Diagnostic Loops
- **Post-Edit Verification:** After editing Kotlin code, validate compilation by running `./gradlew :composeApp:check` or `./gradlew :composeApp:assembleDebug` via MCP[cite: 2]. - **Post-Edit Verification:** After editing Kotlin code, validate compilation by running `./gradlew :composeApp:check` or `./gradlew :composeApp:assembleStableDebug` via MCP[cite: 2].
- **Dispatcher Integrity:** Never remove or bypass `SwingMainDispatcherFactory` in `composeApp/src/jvmMain/.../core/coroutines/`[cite: 2]. It is required to prevent `compose-webview` from hijacking `Dispatchers.Main` away from the Swing EDT[cite: 2]. - **Dispatcher Integrity:** Never remove or bypass `SwingMainDispatcherFactory` in `composeApp/src/jvmMain/.../core/coroutines/`[cite: 2]. It is required to prevent `compose-webview` from hijacking `Dispatchers.Main` away from the Swing EDT[cite: 2].
- **Live Preview Iteration:** Use `compose-hot-reload` to evaluate UI layout changes headlessly without forcing full Gradle rebuilds. - **Live Preview Iteration:** Use `compose-hot-reload` to evaluate UI layout changes headlessly without forcing full Gradle rebuilds.

File diff suppressed because it is too large Load Diff

View File

@ -30,19 +30,19 @@ All types of contributions are encouraged and valued. See the [Table of Contents
## Code of Conduct ## Code of Conduct
This project and everyone participating in it is governed by the This project and everyone participating in it is governed by the
[Spotube Code of Conduct](https://github.com/KRTirtho/spotube/blob/master/CODE_OF_CONDUCT.md). [Spotube Code of Conduct](https://github.com/team-spotube/spotube/blob/master/CODE_OF_CONDUCT.md).
By participating, you are expected to uphold this code. Please report unacceptable behavior By participating, you are expected to uphold this code. Please report unacceptable behavior
to krtirtho@gmail.com. to krtirtho@gmail.com.
## I Have a Question ## I Have a Question
> If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/KRTirtho/spotube#readme). > If you want to ask a question, we assume that you have read the available [Documentation](https://github.com/team-spotube/spotube#readme).
Before you ask a question, it is best to search for existing [Issues](https://github.com/KRTirtho/spotube/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first. Before you ask a question, it is best to search for existing [Issues](https://github.com/team-spotube/spotube/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue. It is also advisable to search the internet for answers first.
If you then still feel the need to ask a question and need clarification, we recommend the following: If you then still feel the need to ask a question and need clarification, we recommend the following:
- Open an [Discussion](https://github.com/KRTirtho/spotube/discussions/new) with the question label. - Open an [Discussion](https://github.com/team-spotube/spotube/discussions/new) with the question label.
- Provide as much context as you can about what you're running into. - Provide as much context as you can about what you're running into.
- Provide project and platform versions (flutter, dart, pub, linux etc..), depending on what seems relevant. - Provide project and platform versions (flutter, dart, pub, linux etc..), depending on what seems relevant.
@ -61,8 +61,8 @@ We will then take care of the issue as soon as possible.
A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible. A good bug report shouldn't leave others needing to chase you up for more information. Therefore, we ask you to investigate carefully, collect information and describe the issue in detail in your report. Please complete the following steps in advance to help us fix any potential bug as fast as possible.
- Make sure that you are using the latest version. - Make sure that you are using the latest version.
- Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/KRTirtho/spotube#readme). If you are looking for support, you might want to check [this section](#i-have-a-question)). - Determine if your bug is really a bug and not an error on your side e.g. using incompatible environment components/versions (Make sure that you have read the [documentation](https://github.com/team-spotube/spotube#readme). If you are looking for support, you might want to check [this section](#i-have-a-question)).
- To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/KRTirtho/spotubeissues?q=label%3Abug). - To see if other users have experienced (and potentially already solved) the same issue you are having, check if there is not already a bug report existing for your bug or error in the [bug tracker](https://github.com/team-spotube/spotube/issues?q=label%3Abug).
- Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue. - Also make sure to search the internet (including Stack Overflow) to see if users outside of the GitHub community have discussed the issue.
- Collect information about the bug: - Collect information about the bug:
- Stack trace (Traceback) - Stack trace (Traceback)
@ -77,7 +77,7 @@ A good bug report shouldn't leave others needing to chase you up for more inform
We use GitHub issues to track bugs and errors. If you run into an issue with the project: We use GitHub issues to track bugs and errors. If you run into an issue with the project:
- Open an [Issue](https://github.com/KRTirtho/spotube/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.) - Open an [Issue](https://github.com/team-spotube/spotube/issues/new). (Since we can't be sure at this point whether it is a bug or not, we ask you not to talk about a bug yet and not to label the issue.)
- Explain the behavior you would expect and the actual behavior. - Explain the behavior you would expect and the actual behavior.
- Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case. - Please provide as much context as possible and describe the _reproduction steps_ that someone else can follow to recreate the issue on their own. This usually includes your code. For good bug reports you should isolate the problem and create a reduced test case.
- Provide the information you collected in the previous section. - Provide the information you collected in the previous section.
@ -97,15 +97,15 @@ This section guides you through submitting an enhancement suggestion for Spotube
#### Before Submitting an Enhancement #### Before Submitting an Enhancement
- Make sure that you are using the latest version. - Make sure that you are using the latest version.
- Read the [documentation](https://github.com/KRTirtho/spotube#readme) carefully and find out if the functionality is already covered, maybe by an individual configuration. - Read the [documentation](https://github.com/team-spotube/spotube#readme) carefully and find out if the functionality is already covered, maybe by an individual configuration.
- Perform a [search](https://github.com/KRTirtho/spotube/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one. - Perform a [search](https://github.com/team-spotube/spotube/issues) to see if the enhancement has already been suggested. If it has, add a comment to the existing issue instead of opening a new one.
- Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset - Find out whether your idea fits with the scope and aims of the project. It's up to you to make a strong case to convince the project's developers of the merits of this feature. Keep in mind that we want features that will be useful to the majority of our users and not just a small subset
<!-- omit in toc --> <!-- omit in toc -->
#### How Do I Submit a Good Enhancement Suggestion? #### How Do I Submit a Good Enhancement Suggestion?
Enhancement suggestions are tracked as [GitHub issues](https://github.com/KRTirtho/spotube/issues). Enhancement suggestions are tracked as [GitHub issues](https://github.com/team-spotube/spotube/issues).
- Use a **clear and descriptive title** for the issue to identify the suggestion. - Use a **clear and descriptive title** for the issue to identify the suggestion.
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible. - Provide a **step-by-step description of the suggested enhancement** in as many details as possible.

109
README.md
View File

@ -4,7 +4,7 @@
A cross-platform extensible open-source music streaming platform.<br> A cross-platform extensible open-source music streaming platform.<br>
Bring your own music metadata/playlist/audio-source with plugins created by community or by yourself. A small step towards the decentralized music streaming era! Bring your own music metadata/playlist/audio-source with plugins created by community or by yourself. A small step towards the decentralized music streaming era!
<a href="https://spotube.cc"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a> <a href="https://spotube.org"><img alt="Visit the website" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/documentation/website_vector.svg"></a>
<a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a> <a href="https://discord.gg/uJ94vxB6vg"><img alt="Discord Server" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/social/discord-plural_vector.svg"></a>
<a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a> <a href="https://patreon.com/krtirtho"><img alt="Support me on Patron" height="56" src="https://cdn.jsdelivr.net/npm/@intergrav/devins-badges@3/assets/cozy/donate/patreon-singular_vector.svg"></a>
@ -37,21 +37,118 @@ Bring your own music metadata/playlist/audio-source with plugins created by comm
### ⬇️ Download ### ⬇️ Download
Download Spotube from the Official Website: https://spotube.cc Download Spotube from the Official Website: https://spotube.org
### 🔄 Nightly Builds ### 🔄 Nightly Builds
Grab the latest nightly builds of Spotube [from the Offical Website](https://spotube.cc/downloads/nightly). Grab the latest nightly builds of Spotube [from the Offical Website](https://spotube.org/downloads/nightly).
## 🕳️ Building from source ## 🕳️ Building from source
<a href="https://github.com/KRTirtho/spotube/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?+label=Build%20Status"></a> <a href="https://github.com/team-spotube/spotube/actions"><img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/KRTirtho/spotube/spotube-release-binary.yml?+label=Build%20Status"></a>
You can compile Spotube's source code by [following these instructions](CONTRIBUTION.md#your-first-code-contribution). Spotube is a Kotlin Multiplatform + Compose Multiplatform app. The desktop app is built with the [Nucleus](https://nucleusframework.dev) Gradle plugin and released as a **GraalVM native image**: an ahead-of-time compiled binary that needs no JVM installed.
### Prerequisites
- **JDK 21** to run Gradle. The Gradle daemon JDK and the GraalVM toolchain are provisioned automatically.
- **Rust** (`rustup` + `cargo`). The `composeApp` Rust library is built through [Gobley](https://gobley.dev).
- **Android SDK**, only for Android builds.
- **Unpublished dependencies.** Three dependencies aren't on Maven Central yet. Clone each one and run `./gradlew publishToMavenLocal` in it:
- [kdroidFilter/ComposeNativeWebview](https://github.com/kdroidFilter/ComposeNativeWebview), at the commit pinned in [`spotube-release-binary.yml`](.github/workflows/spotube-release-binary.yml) (`COMPOSE_WEBVIEW_COMMIT`)
- [team-spotube/gradle-plugin](https://github.com/team-spotube/gradle-plugin)
- [team-spotube/vlcj-bundler-gradle-plugin](https://github.com/team-spotube/vlcj-bundler-gradle-plugin)
- **Linux runtime:** `webkit2gtk-4.1` for the embedded webview.
Always use the Gradle wrapper (`./gradlew`, or `gradlew.bat` on Windows).
### Development
```bash
./gradlew :composeApp:run # desktop (JVM), fastest edit/run loop
./gradlew :composeApp:assembleStableDebug # Android debug APK (or assembleNightlyDebug)
```
Development builds stay fast. The release-only optimizations (the Rust release profile with LTO and symbol stripping, the native image, native-library pruning) are only applied when you build something to ship. See [Production vs development builds](#production-vs-development-builds).
### Building the desktop app (GraalVM native image)
#### Native toolchain
[Nucleus downloads and caches GraalVM CE](https://nucleusframework.dev/en/docs/performance/graalvm/) under `~/.gradle/nucleus/graalvm` on the first native build. Set `GRAALVM_HOME` to use your own GraalVM instead. Each OS also needs a native C toolchain:
| OS | Requirements |
|---|---|
| Linux | `gcc`, `zlib` headers, `patchelf`; plus `rpm` for `.rpm` packages. Debian/Ubuntu: `sudo apt-get install build-essential zlib1g-dev patchelf rpm`. Fedora: `sudo dnf install gcc zlib-devel patchelf rpm-build` |
| macOS | Xcode Command Line Tools (`xcode-select --install`) |
| Windows | Visual Studio Build Tools with the "Desktop development with C++" workload. Build from an "x64 Native Tools" prompt if `native-image` can't find MSVC. |
A native build is a whole-program compile. It takes **about 10 minutes**, uses **every CPU core**, and peaks at roughly **15 GB of RAM** (it gets slower, not broken, with less). That only happens for the native tasks below, never for `run`.
#### Build and run
```bash
# App folder: binary + its native libs, in composeApp/build/compose/binaries/main/graalvm-app/
./gradlew :composeApp:createGraalvmNativeDistributable
# Run that folder's binary directly
./composeApp/build/compose/binaries/main/graalvm-app/org.spotube.spotube/spotube
# Or build and launch in one step
./gradlew :composeApp:runGraalvmNativeDistributable
```
Keep the `spotube` binary next to the `.so`/`.dll`/`lib/` files it was built with; it loads them from its own folder.
#### Installers
Each OS can only build its own installers:
| OS | Command | Output |
|---|---|---|
| Linux | `./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm` (also `packageGraalvmAppImage`) | `composeApp/build/compose/binaries/main/graalvm-{deb,rpm}/` |
| Windows | `./gradlew :composeApp:packageGraalvmExe` (or `packageGraalvmMsi`) | `composeApp/build/compose/binaries/main/graalvm-exe/` |
| macOS | `./gradlew :composeApp:packageGraalvmDmg` | `composeApp/build/compose/binaries/main/graalvm-dmg/` |
Add `-Pnightly=true` for the nightly channel (separate icons and name).
#### Reflection metadata (when a feature breaks only in the native build)
A native image is a closed world: anything the app reaches through reflection, JNI or JNA (vlcj, the Zipline/QuickJS plugin engine, NewPipe, ktor, datastore, …) must be declared at build time. Nucleus generates most of this automatically. The rest is in [`composeApp/graalvm/reachability-metadata.json`](composeApp/graalvm/reachability-metadata.json).
If something works in `:composeApp:run` but throws `ClassNotFoundException`, `NoSuchMethodException`, `MissingReflectionRegistrationError` or similar in the native binary, record what's missing:
```bash
./gradlew :composeApp:runWithNativeAgent
```
The app starts on the JVM with GraalVM's tracing agent attached. Use the broken feature (and anything near it), then **close the window normally**: the recorded entries are merged into `composeApp/graalvm/` only on a clean exit, not with Ctrl+C. Commit the updated JSON and rebuild the native image.
`./gradlew :composeApp:runGraalvmNative` gives a faster debugging loop: a quick-build (`-Ob`) native image that reports exactly which registration is missing.
Keep the hand-written `uk.co.caprica.vlcj.binding.lib.LibVlc` entry. JNA's direct mapping looks up its `native` methods through reflection, and without the entry, playback fails to initialize.
### Production vs development builds
Production mode is switched on automatically when you ask Gradle for a shippable artifact: any `package*`, `create*`, `notarize*` or `nativeImageCompile` task. You can force it either way with `-Pproduction=true` or `-Pproduction=false`.
| | Development (`run`, debug builds) | Production (installers, native image) |
|---|---|---|
| Rust library (desktop) | `debug` profile, fast incremental compile | `release` profile with LTO, single codegen unit, stripped (31 MB → 1.8 MB) |
| Desktop app | JVM, hot-reloadable | GraalVM native image, `-Os` (size-optimized) |
| Native libraries | everything on the classpath | only the host OS/CPU's natives; other OSes' and CPUs' libraries are pruned from the uber JAR the image is compiled from |
| Skia (Skiko) | extracted from the JAR | loaded from `lib/` next to the binary, not embedded a second time |
| JDK runtime libs (Linux) | n/a | native-image's ~10 KB `libjvm.so`/`libjava.so` shims instead of the JDK's (28 MB `libjvm.so`) |
Android picks the Rust profile from its build type (`debug` or `release`) on its own.
### Releases
Releases are built by [`spotube-release-binary.yml`](.github/workflows/spotube-release-binary.yml), run manually (`workflow_dispatch`) as `stable` or `nightly`. Each desktop platform (Linux x86_64 and aarch64, Windows, macOS) builds its native installers on its own runner. Android builds a signed APK.
## 👥 The Spotube team ## 👥 The Spotube team
- [Kingkor Roy Tirtho](https://github.com/KRTirtho) - The Founder, Maintainer and Lead Developer - [Kingkor Roy Tirtho](https://github.com/team-spotube) - The Founder, Maintainer and Lead Developer
- [Owen Connor](https://github.com/owencz1998) - The Cool Discord Moderator - [Owen Connor](https://github.com/owencz1998) - The Cool Discord Moderator
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer - [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy - [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy

View File

@ -27,7 +27,7 @@ render_always = true
# An array of regex based postprocessors to modify the changelog. # An array of regex based postprocessors to modify the changelog.
postprocessors = [ postprocessors = [
# Replace the placeholder <REPO> with a URL. # Replace the placeholder <REPO> with a URL.
{ pattern = '<REPO>', replace = "https://github.com/KRTirtho/spotube" }, { pattern = '<REPO>', replace = "https://github.com/team-spotube/spotube" },
] ]
# render body even when there are no releases to process # render body even when there are no releases to process
# render_always = true # render_always = true

View File

@ -14,4 +14,9 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
[lib] [lib]
crate-type = ["cdylib", "staticlib"] crate-type = ["cdylib", "staticlib"]
path = "src/commonMain/rust/lib.rs" path = "src/commonMain/rust/lib.rs"
[profile.release]
strip = true
lto = true
codegen-units = 1

View File

@ -16,7 +16,9 @@
*/ */
import gobley.gradle.GobleyHost import gobley.gradle.GobleyHost
import gobley.gradle.Variant
import gobley.gradle.cargo.dsl.jvm import gobley.gradle.cargo.dsl.jvm
import dev.nucleusframework.desktop.application.dsl.NativeImageOptimization
import dev.nucleusframework.desktop.application.dsl.TargetFormat import dev.nucleusframework.desktop.application.dsl.TargetFormat
import org.jetbrains.compose.reload.gradle.ComposeHotRun import org.jetbrains.compose.reload.gradle.ComposeHotRun
import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.JvmTarget
@ -40,22 +42,45 @@ plugins {
kotlin("plugin.atomicfu") version libs.versions.kotlin kotlin("plugin.atomicfu") version libs.versions.kotlin
} }
val bundleId = "dev.krtirtho.spotube" val bundleId = "org.spotube.spotube"
val version = "6.0.0" val version = "6.0.0"
val buildNumber = 46 val buildNumber = 46
// Desktop icon channel; Android picks its icons via the stable/nightly product flavor. Use -Pnightly=true.
val isNightly = providers.gradleProperty("nightly").map { it.toBoolean() }.getOrElse(false)
val desktopIconDir = if (isNightly) "icons/nightly" else "icons/stable"
// Nightlies are numbered by the CI run (-PbuildNumber=<run>, or GITHUB_RUN_NUMBER) so every build is traceable to its run.
val nightlyBuildNumber = providers.gradleProperty("buildNumber").orNull?.toIntOrNull()
?: System.getenv("GITHUB_RUN_NUMBER")?.toIntOrNull()
?: buildNumber
// Production = shipping artifacts (installers, distributables, native images). Size/speed
// optimizations that slow builds down (Rust release + LTO) only apply then, never to `run`/dev.
// Override with -Pproduction=true|false.
val isProductionBuild = providers.gradleProperty("production").map { it.toBoolean() }.getOrElse(
gradle.startParameter.taskNames.any { task ->
task.substringAfterLast(':').let { name ->
name.startsWith("package") || name.startsWith("create") ||
name.startsWith("notarize") || name == "nativeImageCompile"
}
}
)
val targetName = "Spotube" val targetName = "Spotube"
val homePageUrl = "https://spotube.cc" val homePageUrl = "https://spotube.org"
val repositoryUrl = "https://github.com/team-spotube/spotube" val repositoryUrl = "https://github.com/team-spotube/spotube"
val appVersionName = version val appVersionName = version
val appBuildNumber = buildNumber val appBuildNumber = if (isNightly) nightlyBuildNumber else buildNumber
val appRepositoryUrl = repositoryUrl val appRepositoryUrl = repositoryUrl
// CI reads the release version from here instead of scraping this file.
tasks.register("printVersion") {
val appVersion = appVersionName
doLast { println(appVersion) }
}
val githubRepositoryPath = repositoryUrl.removePrefix("https://github.com/") val githubRepositoryPath = repositoryUrl.removePrefix("https://github.com/")
val latestReleaseApiUrl = "https://api.github.com/repos/$githubRepositoryPath/releases/latest" val latestReleaseApiUrl = "https://api.github.com/repos/$githubRepositoryPath/releases/latest"
val generatedAppInfoDir = layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin") val generatedAppInfoDir = layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin")
val generateAppBuildInfo by tasks.registering(Copy::class) { val generateAppBuildInfo by tasks.registering(Copy::class) {
description = "Generate AppBuildInfo.kt" description = "Generate AppBuildInfo.kt"
from(layout.projectDirectory.file("src/commonMain/templates/AppBuildInfo.kt.template")) from(layout.projectDirectory.file("src/commonMain/templates/AppBuildInfo.kt.template"))
into(layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin/dev/krtirtho/spotube")) into(layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin/org/spotube/spotube"))
rename("AppBuildInfo.kt.template", "AppBuildInfo.kt") rename("AppBuildInfo.kt.template", "AppBuildInfo.kt")
expand( expand(
mapOf( mapOf(
@ -287,7 +312,7 @@ kotlin {
} }
android { android {
namespace = "dev.krtirtho.spotube" namespace = "org.spotube.spotube"
compileSdk = libs.versions.android.compileSdk.get().toInt() compileSdk = libs.versions.android.compileSdk.get().toInt()
defaultConfig { defaultConfig {
@ -297,6 +322,19 @@ android {
versionCode = buildNumber versionCode = buildNumber
versionName = version versionName = version
} }
// stable = production channel, nightly = beta channel (own icon, name and installable side by side)
flavorDimensions += "channel"
productFlavors {
create("stable") {
dimension = "channel"
}
create("nightly") {
dimension = "channel"
applicationIdSuffix = ".nightly"
versionCode = nightlyBuildNumber
versionNameSuffix = "-nightly.$nightlyBuildNumber"
}
}
packaging { packaging {
resources { resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}" excludes += "/META-INF/{AL2.0,LGPL2.1}"
@ -350,7 +388,7 @@ dependencies {
} }
nucleus.application { nucleus.application {
mainClass = "dev.krtirtho.spotube.MainKt" mainClass = "org.spotube.spotube.MainKt"
jvmArgs += listOf( jvmArgs += listOf(
"--enable-native-access=ALL-UNNAMED", // need for JNA for compose-webview (wry) to work "--enable-native-access=ALL-UNNAMED", // need for JNA for compose-webview (wry) to work
@ -382,11 +420,13 @@ nucleus.application {
homepage = homePageUrl homepage = homePageUrl
linux { linux {
iconFile.set(project.file("$desktopIconDir/icon.png"))
modules("jdk.security.auth") modules("jdk.security.auth")
debMaintainer = "Team Spotube <team.spotube@proton.me>" debMaintainer = "Team Spotube <team.spotube@proton.me>"
rpmLicenseType = "AGPL-3.0-or-later" rpmLicenseType = "AGPL-3.0-or-later"
} }
windows { windows {
iconFile.set(project.file("$desktopIconDir/icon.ico"))
shortcut = true shortcut = true
menu = true menu = true
dirChooser = true dirChooser = true
@ -394,6 +434,7 @@ nucleus.application {
} }
macOS { macOS {
iconFile.set(project.file("$desktopIconDir/icon.icns"))
notarization { notarization {
} }
@ -403,6 +444,84 @@ nucleus.application {
buildTypes.release.proguard { buildTypes.release.proguard {
configurationFiles.from(project.file("proguard-rules.pro")) configurationFiles.from(project.file("proguard-rules.pro"))
} }
// AOT-compiled native binary (no bundled JVM). Build with `packageGraalvmNative`, try it with
// `runGraalvmNative`. Nucleus downloads GraalVM CE on first use (or uses $GRAALVM_HOME).
// Reflection/JNI metadata the automatic passes miss (vlcj, Zipline, NewPipe/Rhino, uniffi)
// lives in `graalvm/` and is (re)collected by exercising the app under `runWithNativeAgent`.
graalvm {
isEnabled = true
imageName = "spotube"
nativeImageConfigBaseDir.set(project.layout.projectDirectory.dir("graalvm"))
// ProGuard is not used for native images; -Os is the size equivalent
optimization = NativeImageOptimization.SIZE
// Same ceiling as the JVM distribution's -Xmx
maxHeapSize = "384m"
// Skiko is loaded from lib/ next to the binary (Nucleus copies it there); don't embed
// a second ~30 MB copy as a resource
buildArgs.add("-H:+UnlockExperimentalVMOptions")
buildArgs.add("-H:ExcludeResources=.*skiko-[^/]*\\.(so|dylib|dll)")
}
}
// The uber JAR is what the GraalVM build compiles from, and native-image embeds every registered
// resource in the binary. Libraries ship natives for every OS/CPU, so keep only the host's
// (the uber JAR is per-OS/arch already; it isn't used by `run`).
tasks.withType<org.gradle.jvm.tasks.Jar>().matching { it.name.endsWith("UberJarForCurrentOS") }.configureEach {
// Everything the exclude closure uses is local: the configuration cache can't serialize
// references to the build script object.
val hostOs = System.getProperty("os.name").lowercase()
val foreignExtensions = when {
hostOs.contains("win") -> setOf("so", "dylib", "jnilib", "a")
hostOs.contains("mac") -> setOf("so", "dll", "a")
else -> setOf("dll", "dylib", "jnilib", "a")
}
val nativeExtensions = setOf("so", "dll", "dylib", "jnilib", "a")
// CPU names as libraries spell them in their native folders (linux-x86-64, jni/amd64, darwin-aarch64, …)
val cpuDirToken = Regex(
"(?:^|[-_])(x86[-_]64|amd64|x64|aarch64|arm64|armv6|armv7|armel|arm|x86|i[36]86|ppc64le|ppc64|ppc" +
"|s390x|riscv64|loongarch64|mips64el|sparcv9|sparc)(?=$|[-_])"
)
val hostCpuTokens = when (System.getProperty("os.arch").lowercase()) {
"aarch64", "arm64" -> setOf("aarch64", "arm64")
else -> setOf("x86-64", "x86_64", "amd64", "x64")
}
// OS flavours we never ship desktop builds for (sqlite-jdbc and JNA bundle these)
val foreignOsDirToken = Regex("android|musl|freebsd|openbsd|dragonflybsd|aix|sunos")
exclude { element ->
val segments = element.path.lowercase().split('/')
val extension = segments.last().substringAfterLast('.', "")
!element.isDirectory && extension in nativeExtensions && (
extension in foreignExtensions ||
segments.dropLast(1).any { dir ->
foreignOsDirToken.containsMatchIn(dir) ||
cpuDirToken.findAll(dir).any { it.groupValues[1] !in hostCpuTokens }
}
)
}
// The locally built compose-webview SNAPSHOT carries stale CEF binaries (~1.3 GB unstripped
// libcef.so) that no code loads (Linux uses WebKitGTK).
exclude(
"nucleus/native/*/libcef.*",
"nucleus/native/*/libcompose_cef_*",
"nucleus/native/*/cef_subprocess*",
"nucleus/native/*/*.pak",
"nucleus/native/*/icudtl.dat",
"nucleus/native/*/v8_context_snapshot.bin",
)
}
// Linux: native-image emits 13 KB libjvm.so/libjava.so shims (the image itself provides the JVM
// symbols), but Nucleus copies the JDK's real ones next to the binary — libjvm.so alone is 28 MB.
// Ship native-image's own outputs instead: skip the libjvm copy, and in the AWT copy prefer
// nativeCompile's version of any library native-image produced over the JDK's.
tasks.matching { it.name == "copyGraalvmJvmSo" }.configureEach { enabled = false }
tasks.withType<Copy>().matching { it.name == "copyGraalvmAwtSoLibs" }.configureEach {
val nativeCompileDir = layout.buildDirectory.dir("compose/tmp/main/graalvm/nativeCompile").get().asFile
from(nativeCompileDir) { include("*.so") }
eachFile {
if (file.parentFile != nativeCompileDir && nativeCompileDir.resolve(name).exists()) exclude()
}
} }
// Issue with compose plugin // Issue with compose plugin
@ -418,6 +537,8 @@ tasks.withType<ComposeHotRun>().configureEach {
} }
cargo { cargo {
// Gobley defaults desktop to the debug profile, which ships an unstripped ~31 MB library
if (isProductionBuild) jvmVariant = Variant.Release
builds.jvm { builds.jvm {
embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget) embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget)
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

View File

@ -15,14 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.newpipe package org.spotube.spotube.core.newpipe
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import dev.krtirtho.spotube.core.paths.Paths import org.spotube.spotube.core.paths.Paths
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.newpipe package org.spotube.spotube.core.newpipe
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioStream import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
import io.ktor.http.Url import io.ktor.http.Url
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -26,12 +26,19 @@ import org.schabi.newpipe.extractor.ServiceList.YouTube
import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory import org.schabi.newpipe.extractor.services.youtube.linkHandler.YoutubeSearchQueryHandlerFactory
import org.schabi.newpipe.extractor.stream.StreamInfoItem import org.schabi.newpipe.extractor.stream.StreamInfoItem
/** NewPipe reports durations in seconds (-1 when unknown). */
private fun secondsToMs(seconds: Long): Long = if (seconds > 0) seconds * 1000 else 0
actual class NewPipeService { actual class NewPipeService {
actual suspend fun searchVideos(query: String): List<VideoSearchResult> { actual suspend fun searchVideos(query: String, kind: SearchKind): List<VideoSearchResult> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
val extractor = YouTube.getSearchExtractor( val extractor = YouTube.getSearchExtractor(
query, query,
mutableListOf(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS), mutableListOf(
when (kind) {
SearchKind.Songs -> YoutubeSearchQueryHandlerFactory.MUSIC_SONGS
SearchKind.Videos -> YoutubeSearchQueryHandlerFactory.VIDEOS
}),
"" ""
) )
extractor.fetchPage() extractor.fetchPage()
@ -45,7 +52,7 @@ actual class NewPipeService {
title = item.name, title = item.name,
url = item.url, url = item.url,
uploader = item.uploaderName, uploader = item.uploaderName,
durationMs = item.duration, durationMs = secondsToMs(item.duration),
thumbnailUrl = item.thumbnails.first().url) thumbnailUrl = item.thumbnails.first().url)
} else { } else {
null null
@ -101,7 +108,7 @@ actual class NewPipeService {
title = extractor.name, title = extractor.name,
url = extractor.url, url = extractor.url,
uploader = extractor.uploaderName, uploader = extractor.uploaderName,
durationMs = extractor.length, durationMs = secondsToMs(extractor.length),
thumbnailUrl = extractor.thumbnails.first().url, thumbnailUrl = extractor.thumbnails.first().url,
// fallback to video streams if no audio streams are available // fallback to video streams if no audio streams are available
audioStreams = audioStreams.ifEmpty { audioStreams = audioStreams.ifEmpty {

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube package org.spotube.spotube
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
@ -23,10 +23,10 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import dev.krtirtho.spotube.core.deeplink.ExternalUriHandler import org.spotube.spotube.core.deeplink.ExternalUriHandler
import dev.krtirtho.spotube.core.newpipe.NewPipeDownloader import org.spotube.spotube.core.newpipe.NewPipeDownloader
import dev.krtirtho.spotube.core.paths.Paths import org.spotube.spotube.core.paths.Paths
import dev.krtirtho.spotube.media.PlaybackService import org.spotube.spotube.media.PlaybackService
import io.github.vinceglb.filekit.FileKit import io.github.vinceglb.filekit.FileKit
import io.github.vinceglb.filekit.dialogs.init import io.github.vinceglb.filekit.dialogs.init

View File

@ -15,11 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube package org.spotube.spotube
import android.app.Application import android.app.Application
import dev.krtirtho.spotube.core.di.initKoin import org.spotube.spotube.core.di.initKoin
import dev.krtirtho.spotube.core.paths.Paths import org.spotube.spotube.core.paths.Paths
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube package org.spotube.spotube
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
@ -27,7 +27,7 @@ import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException import androidx.media3.common.PlaybackException
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import dev.krtirtho.spotube.media.PlaybackService import org.spotube.spotube.media.PlaybackService
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -84,6 +84,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
private var disposed = false private var disposed = false
private val currentPlaylist = mutableListOf<MediaItem>() private val currentPlaylist = mutableListOf<MediaItem>()
private val originalPlaylist = mutableListOf<MediaItem>()
private var shuffleEnabled = false
private val urlIndexMap = mutableMapOf<String, Int>() private val urlIndexMap = mutableMapOf<String, Int>()
private val _playerState = MutableStateFlow(PlayerState.IDLE) private val _playerState = MutableStateFlow(PlayerState.IDLE)
@ -143,10 +145,6 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
) )
} }
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
_shuffleMode.tryEmit(shuffleModeEnabled)
}
override fun onVolumeChanged(volume: Float) { override fun onVolumeChanged(volume: Float) {
_volume.tryEmit(volume) _volume.tryEmit(volume)
} }
@ -260,8 +258,37 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
actual override suspend fun shuffle(enabled: Boolean) { actual override suspend fun shuffle(enabled: Boolean) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
exoPlayer.shuffleModeEnabled = enabled if (disposed || currentPlaylist.isEmpty() || shuffleEnabled == enabled) return@withContext
val currentIndex = exoPlayer.currentMediaItemIndex
val current = currentPlaylist.getOrNull(currentIndex) ?: return@withContext
val rest = if (enabled) {
originalPlaylist.filterNot { it.url == current.url }.shuffled()
} else {
originalPlaylist.filterNot { it.url == current.url }
}
// Disabling restores original order around the current item.
val before = if (enabled) emptyList() else originalPlaylist.takeWhile { it.url != current.url }
val after = if (enabled) rest else originalPlaylist.dropWhile { it.url != current.url }.drop(1)
// Reorder in place around the playing item so playback is not interrupted.
if (currentIndex < exoPlayer.mediaItemCount - 1) {
exoPlayer.removeMediaItems(currentIndex + 1, exoPlayer.mediaItemCount)
}
if (currentIndex > 0) exoPlayer.removeMediaItems(0, currentIndex)
exoPlayer.addMediaItems(0, before.map { it.toExoMediaItem() })
exoPlayer.addMediaItems(after.map { it.toExoMediaItem() })
currentPlaylist.clear()
currentPlaylist.addAll(before + current + after)
urlIndexMap.clear()
currentPlaylist.forEachIndexed { i, item -> urlIndexMap[item.url] = i }
shuffleEnabled = enabled
_shuffleMode.tryEmit(enabled) _shuffleMode.tryEmit(enabled)
_playlist.tryEmit(currentPlaylist.toList())
syncCurrentItem()
} }
} }
@ -273,6 +300,10 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
currentPlaylist.clear() currentPlaylist.clear()
currentPlaylist.addAll(playlist) currentPlaylist.addAll(playlist)
originalPlaylist.clear()
originalPlaylist.addAll(playlist)
shuffleEnabled = false
_shuffleMode.tryEmit(false)
urlIndexMap.clear() urlIndexMap.clear()
playlist.forEachIndexed { index, item -> playlist.forEachIndexed { index, item ->
urlIndexMap[item.url] = index urlIndexMap[item.url] = index
@ -305,6 +336,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
actual override suspend fun addMediaItem(mediaItem: MediaItem) { actual override suspend fun addMediaItem(mediaItem: MediaItem) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
currentPlaylist.add(mediaItem) currentPlaylist.add(mediaItem)
originalPlaylist.add(mediaItem)
urlIndexMap[mediaItem.url] = currentPlaylist.lastIndex urlIndexMap[mediaItem.url] = currentPlaylist.lastIndex
_playlist.tryEmit(currentPlaylist.toList()) _playlist.tryEmit(currentPlaylist.toList())
exoPlayer.addMediaItem(mediaItem.toExoMediaItem()) exoPlayer.addMediaItem(mediaItem.toExoMediaItem())
@ -317,6 +349,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
val insertIndex = (currentIndex + 1).coerceAtMost(currentPlaylist.size) val insertIndex = (currentIndex + 1).coerceAtMost(currentPlaylist.size)
currentPlaylist.add(insertIndex, mediaItem) currentPlaylist.add(insertIndex, mediaItem)
val origCurrent = currentPlaylist.getOrNull(currentIndex)?.let { c -> originalPlaylist.indexOfFirst { it.url == c.url } } ?: -1
originalPlaylist.add((origCurrent + 1).coerceIn(0, originalPlaylist.size), mediaItem)
urlIndexMap.clear() urlIndexMap.clear()
currentPlaylist.forEachIndexed { index, item -> currentPlaylist.forEachIndexed { index, item ->
urlIndexMap[item.url] = index urlIndexMap[item.url] = index
@ -331,6 +365,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
val index = urlIndexMap[mediaItem.url] ?: return@withContext val index = urlIndexMap[mediaItem.url] ?: return@withContext
currentPlaylist.removeAt(index) currentPlaylist.removeAt(index)
originalPlaylist.removeAll { it.url == mediaItem.url }
urlIndexMap.clear() urlIndexMap.clear()
currentPlaylist.forEachIndexed { i, item -> currentPlaylist.forEachIndexed { i, item ->
urlIndexMap[item.url] = i urlIndexMap[item.url] = i
@ -346,6 +381,9 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
val item = currentPlaylist.removeAt(fromIndex) val item = currentPlaylist.removeAt(fromIndex)
currentPlaylist.add(toIndex, item) currentPlaylist.add(toIndex, item)
if (!shuffleEnabled && fromIndex in originalPlaylist.indices && toIndex in originalPlaylist.indices) {
originalPlaylist.add(toIndex, originalPlaylist.removeAt(fromIndex))
}
urlIndexMap.clear() urlIndexMap.clear()
currentPlaylist.forEachIndexed { i, it -> currentPlaylist.forEachIndexed { i, it ->
urlIndexMap[it.url] = i urlIndexMap[it.url] = i
@ -398,6 +436,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
disposed = true disposed = true
exoPlayer.release() exoPlayer.release()
currentPlaylist.clear() currentPlaylist.clear()
originalPlaylist.clear()
urlIndexMap.clear() urlIndexMap.clear()
_playerState.tryEmit(PlayerState.IDLE) _playerState.tryEmit(PlayerState.IDLE)

View File

@ -15,17 +15,17 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.di package org.spotube.spotube.core.di
import android.content.Context import android.content.Context
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer import org.spotube.spotube.core.audioplayer.AudioPlayer
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
import dev.krtirtho.spotube.core.paths.Paths import org.spotube.spotube.core.paths.Paths
import dev.krtirtho.spotube.core.share.AndroidShareService import org.spotube.spotube.core.share.AndroidShareService
import dev.krtirtho.spotube.core.share.ShareService import org.spotube.spotube.core.share.ShareService
import dev.krtirtho.spotube.media.MediaBrowseHelper import org.spotube.spotube.media.MediaBrowseHelper
import dev.krtirtho.spotube.modules.library.local_tracks.media.AndroidLocalMediaDiscoveryService import org.spotube.spotube.modules.library.local_tracks.media.AndroidLocalMediaDiscoveryService
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaDiscoveryService import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaDiscoveryService
import org.koin.dsl.module import org.koin.dsl.module
actual val platformModules = module { actual val platformModules = module {

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.discovery package org.spotube.spotube.core.discovery
import android.Manifest import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.i18n package org.spotube.spotube.core.i18n
import android.content.res.Configuration import android.content.res.Configuration
import android.content.res.Resources import android.content.res.Resources

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.paths package org.spotube.spotube.core.paths
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment

View File

@ -15,11 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.share package org.spotube.spotube.core.share
import android.content.Context import android.content.Context
import androidx.core.app.ShareCompat import androidx.core.app.ShareCompat
import dev.krtirtho.spotube.core.share.ShareService import org.spotube.spotube.core.share.ShareService
class AndroidShareService(private val context: Context) : ShareService { class AndroidShareService(private val context: Context) : ShareService {
override fun share(url: String, title: String) { override fun share(url: String, title: String) {

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.webview package org.spotube.spotube.core.webview
import android.webkit.CookieManager import android.webkit.CookieManager
import android.webkit.WebStorage import android.webkit.WebStorage

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.zipline package org.spotube.spotube.core.zipline
import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.Executors import java.util.concurrent.Executors

View File

@ -15,31 +15,31 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.media package org.spotube.spotube.media
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata import androidx.media3.common.MediaMetadata
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum import org.spotube.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist import org.spotube.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseItem import org.spotube.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseItem
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist import org.spotube.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchResult import org.spotube.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchResult
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import org.spotube.spotube.core.audioplayer.QueueEntry
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper import org.spotube.spotube.core.playback.CollectionPlaybackHelper
import dev.krtirtho.spotube.modules.album.AlbumRepository import org.spotube.spotube.modules.album.AlbumRepository
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository import org.spotube.spotube.modules.blacklist.BlacklistRepository
import dev.krtirtho.spotube.modules.home.HomeScreenRepository import org.spotube.spotube.modules.home.HomeScreenRepository
import dev.krtirtho.spotube.modules.library.LibraryRepository import org.spotube.spotube.modules.library.LibraryRepository
import dev.krtirtho.spotube.modules.playlist.PlaylistRepository import org.spotube.spotube.modules.playlist.PlaylistRepository
import dev.krtirtho.spotube.modules.plugin.PluginManager import org.spotube.spotube.modules.plugin.PluginManager
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository import org.spotube.spotube.modules.saved_tracks.SavedTracksRepository
import dev.krtirtho.spotube.modules.search.SearchRepository import org.spotube.spotube.modules.search.SearchRepository
import dev.krtirtho.spotube.modules.settings.SettingsRepository import org.spotube.spotube.modules.settings.SettingsRepository
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import org.jetbrains.compose.resources.getString import org.jetbrains.compose.resources.getString
import spotube.composeapp.generated.resources.Res import spotube.composeapp.generated.resources.Res

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.media package org.spotube.spotube.media
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
@ -29,6 +29,7 @@ import androidx.core.app.NotificationCompat
import androidx.media3.common.MediaItem as Media3MediaItem import androidx.media3.common.MediaItem as Media3MediaItem
import androidx.media3.common.Player import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.session.DefaultMediaNotificationProvider
import androidx.media3.session.LibraryResult import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaLibraryService.LibraryParams import androidx.media3.session.MediaLibraryService.LibraryParams
@ -40,11 +41,12 @@ import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.SettableFuture import com.google.common.util.concurrent.SettableFuture
import dev.krtirtho.spotube.MainActivity import org.spotube.spotube.MainActivity
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer import org.spotube.spotube.R
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface import org.spotube.spotube.core.audioplayer.AudioPlayer
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
import org.spotube.spotube.core.audioplayer.QueueEntry
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
@ -75,6 +77,16 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
createNotificationChannel() createNotificationChannel()
// Media3 posts the real playback notification; point it at our channel,
// ID and monochrome icon so it replaces the placeholder below in place.
setMediaNotificationProvider(
DefaultMediaNotificationProvider.Builder(this)
.setChannelId(CHANNEL_ID)
.setNotificationId(NOTIFICATION_ID)
.build()
.apply { setSmallIcon(R.drawable.ic_stat_spotube) }
)
val sessionActivity = PendingIntent.getActivity( val sessionActivity = PendingIntent.getActivity(
this, this,
0, 0,
@ -87,7 +99,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
val notification = NotificationCompat.Builder(this, CHANNEL_ID) val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Spotube") .setContentTitle("Spotube")
.setContentText(runBlocking { getString(Res.string.notification_playback_service_text) }) .setContentText(runBlocking { getString(Res.string.notification_playback_service_text) })
.setSmallIcon(android.R.drawable.ic_media_play) .setSmallIcon(R.drawable.ic_stat_spotube)
.setContentIntent(sessionActivity) .setContentIntent(sessionActivity)
.setOngoing(true) .setOngoing(true)
.build() .build()
@ -371,20 +383,20 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
when { when {
mediaId.startsWith("${MediaBrowseHelper.MEDIA_ID_PLAYLIST}:") -> { mediaId.startsWith("${MediaBrowseHelper.MEDIA_ID_PLAYLIST}:") -> {
val playlistId = mediaId.removePrefix("${MediaBrowseHelper.MEDIA_ID_PLAYLIST}:") val playlistId = mediaId.removePrefix("${MediaBrowseHelper.MEDIA_ID_PLAYLIST}:")
val helper: dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper by inject() val helper: org.spotube.spotube.core.playback.CollectionPlaybackHelper by inject()
helper.playPlaylist(playlistId) helper.playPlaylist(playlistId)
return return
} }
mediaId.startsWith("${MediaBrowseHelper.MEDIA_ID_ALBUM}:") -> { mediaId.startsWith("${MediaBrowseHelper.MEDIA_ID_ALBUM}:") -> {
val albumId = mediaId.removePrefix("${MediaBrowseHelper.MEDIA_ID_ALBUM}:") val albumId = mediaId.removePrefix("${MediaBrowseHelper.MEDIA_ID_ALBUM}:")
val helper: dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper by inject() val helper: org.spotube.spotube.core.playback.CollectionPlaybackHelper by inject()
helper.playAlbum(albumId) helper.playAlbum(albumId)
return return
} }
mediaId == MediaBrowseHelper.MEDIA_ID_SAVED_TRACKS -> { mediaId == MediaBrowseHelper.MEDIA_ID_SAVED_TRACKS -> {
val helper: dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper by inject() val helper: org.spotube.spotube.core.playback.CollectionPlaybackHelper by inject()
helper.playSavedTracks() helper.playSavedTracks()
return return
} }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.modules.library.local_tracks.media package org.spotube.spotube.modules.library.local_tracks.media
import android.Manifest import android.Manifest
import android.content.Context import android.content.Context
@ -26,7 +26,7 @@ import android.os.Environment
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.provider.MediaStore import android.provider.MediaStore
import dev.krtirtho.spotube.core.di.injectLogger import org.spotube.spotube.core.di.injectLogger
import org.koin.core.component.KoinComponent import org.koin.core.component.KoinComponent
import java.io.File import java.io.File
import java.util.Locale import java.util.Locale

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.modules.library.local_tracks.media package org.spotube.spotube.modules.library.local_tracks.media
import android.Manifest import android.Manifest
import android.content.pm.PackageManager import android.content.pm.PackageManager

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 737 B

View File

@ -1,47 +0,0 @@
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@ -1,187 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@ -17,6 +17,7 @@
--> -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon>

View File

@ -17,6 +17,7 @@
--> -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" /> <background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" /> <foreground android:drawable="@mipmap/ic_launcher_foreground" />
</adaptive-icon> <monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,22 @@
<!--
~ 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/>.
-->
<resources>
<string name="app_name">Spotube</string>
<string name="now_playing">Spelas nu</string>
<string name="car_connect_hint">Spela musik från telefonen för att börja lyssna i bilen</string>
</resources>

View File

@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ 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/>.
-->
<resources>
<color name="ic_launcher_background">#000000</color>
</resources>

View File

@ -0,0 +1,169 @@
<!--
~ 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/>.
-->
<resources>
<string name="settings_screen_title">Inställningar</string>
<string name="settings_section_plugins">Tillägg</string>
<string name="settings_section_language_region">Språk och region</string>
<string name="settings_section_appearance">Utseende</string>
<string name="settings_section_playback">Uppspelning</string>
<string name="settings_section_downloads">Nedladdningar</string>
<string name="settings_section_caching">Cachelagring</string>
<string name="settings_section_desktop">Dator</string>
<string name="settings_section_updates">Uppdateringar</string>
<string name="settings_action_close">Stäng</string>
<string name="settings_action_save">Spara</string>
<string name="settings_action_cancel">Avbryt</string>
<string name="settings_language_title">Språk</string>
<string name="settings_language_subtitle_current">Språk för appens gränssnitt. Nuvarande: %1$s</string>
<string name="settings_country_title">Land</string>
<string name="settings_country_subtitle_current">Land som styr innehåll och tillgänglighet. Nuvarande: %1$s</string>
<string name="settings_option_name_and_code">%1$s (%2$s)</string>
<string name="settings_theme_title">Tema</string>
<string name="settings_theme_subtitle_current">Välj hur Spotube anpassar sig till systemets utseende. Nuvarande: %1$s</string>
<string name="settings_theme_light">Ljust</string>
<string name="settings_theme_dark">Mörkt</string>
<string name="settings_theme_system">System</string>
<string name="settings_accent_title">Accentfärg</string>
<string name="settings_accent_subtitle_current">Nuvarande färgpalett: %1$s</string>
<string name="settings_accent_dialog_title">Accentfärg</string>
<string name="settings_accent_dialog_description">Välj en accentpalett och förhandsgranska den i både ljust och mörkt tema.</string>
<string name="settings_preview_light">Ljust</string>
<string name="settings_preview_dark">Mörkt</string>
<string name="settings_preview_label">Förhandsgranskning</string>
<string name="settings_preview_button">Knapp</string>
<string name="settings_accent_green_goblin">Gröna trollet</string>
<string name="settings_accent_electric_violet">Elektriskt violett</string>
<string name="settings_accent_oceanic_cyan">Havscyan</string>
<string name="settings_accent_sunset_orange">Solnedgångsorange</string>
<string name="settings_accent_rose_garden">Rosenträdgård</string>
<string name="settings_accent_midnight_blue">Midnattsblått</string>
<string name="settings_accent_metallic_slate">Metalliskt skiffergrått</string>
<string name="settings_download_folder_title">Egen nedladdningsmapp</string>
<string name="settings_download_folder_current">Nuvarande mapp: %1$s</string>
<string name="settings_download_folder_default">Nuvarande mapp: standardmappen för musik</string>
<string name="settings_download_folder_description">Lämna fältet tomt för att använda standardmappen för musik.</string>
<string name="settings_download_folder_placeholder">C:\Music\Spotube</string>
<string name="settings_local_media_folders_title">Lokala mediemappar</string>
<string name="settings_local_media_folders_subtitle_current">Egna mappar att skanna på datorn. Tillagda: %1$d</string>
<string name="settings_local_media_folders_subtitle_empty">Ingen egen mapp har lagts till ännu</string>
<string name="settings_local_media_folders_description">Lägg till mappar med mappväljaren. Den egna nedladdningsmappen tas med automatiskt.</string>
<string name="settings_local_media_folders_manage">Hantera</string>
<string name="settings_local_media_folders_add_action">Lägg till mapp</string>
<string name="settings_local_media_folders_remove_action">Ta bort mapp</string>
<string name="settings_local_media_folders_none_added">Inga egna mappar har lagts till.</string>
<string name="settings_download_format_title">Musikformat för nedladdning</string>
<string name="settings_download_format_subtitle_current">Önskad kodek och önskat behållarformat för nedladdningar. Nuvarande: %1$s</string>
<string name="settings_download_quality_title">Musikkvalitet för nedladdning</string>
<string name="settings_streaming_format_title">Musikformat för strömning</string>
<string name="settings_streaming_format_subtitle_current">Önskad kodek och önskat behållarformat för musikströmning. Nuvarande: %1$s</string>
<string name="settings_streaming_quality_title">Musikkvalitet för strömning</string>
<string name="settings_subtitle_current">Nuvarande: %1$s</string>
<string name="settings_enable_music_caching_title">Aktivera cachelagring av musik</string>
<string name="settings_enable_music_caching_subtitle">Lagra strömmat ljud lokalt så att det går snabbare att spela upp igen.</string>
<string name="settings_cache_folder_title">Cachemapp</string>
<string name="settings_cache_folder_current">Nuvarande: %1$s</string>
<string name="settings_cache_folder_default">Nuvarande: appens standardcache</string>
<string name="settings_cache_size_limit_title">Maximal cachestorlek</string>
<string name="settings_cache_size_limit_unlimited">Ingen gräns</string>
<string name="settings_cache_size_limit_current">Nuvarande gräns: %1$d MB</string>
<string name="settings_cache_size_limit_description">Ange den maximala cachestorleken i MB. Ange 0 eller lämna tomt för obegränsad storlek.</string>
<string name="settings_cache_size_limit_placeholder">500</string>
<string name="settings_error_cache_size_range">Storleken måste vara 0 eller ett positivt tal.</string>
<string name="settings_enable_endless_playback_title">Fortsätt spela automatiskt</string>
<string name="settings_enable_endless_playback_subtitle">Fortsätt spela rekommenderade låtar när kön tar slut.</string>
<string name="settings_enable_connect_title">Aktivera Connect</string>
<string name="settings_enable_connect_subtitle">Gör det möjligt att fjärrstyra uppspelningen via Spotube Connect.</string>
<string name="settings_playback_port_title">Port för uppspelningens proxyserver</string>
<string name="settings_playback_port_subtitle_current">Port som uppspelningens proxyserver använder. Nuvarande: %1$d</string>
<string name="settings_playback_port_description">Välj en port mellan 1 och 65535.</string>
<string name="settings_playback_port_placeholder">14769</string>
<string name="settings_error_whole_number">Ange ett giltigt heltal.</string>
<string name="settings_error_port_range">Porten måste vara mellan 1 och 65535.</string>
<string name="settings_blacklist_title">Spärrlista</string>
<string name="settings_blacklist_subtitle">Hantera spärrade låtar och artister</string>
<string name="settings_desktop_minimize_title">Minimera till meddelandefältet</string>
<string name="settings_desktop_minimize_subtitle">Låt Spotube fortsätta köras i meddelandefältet när fönstret minimeras.</string>
<string name="settings_desktop_discord_title">Discord Rich Presence</string>
<string name="settings_desktop_discord_subtitle">Visa din aktuella uppspelningsstatus i Discord på datorn.</string>
<string name="settings_updates_auto_check_title">Sök automatiskt efter uppdateringar</string>
<string name="settings_updates_auto_check_subtitle">Sök efter nya versioner av Spotube i bakgrunden.</string>
<string name="settings_plugins_manage_title">Hantera tillägg</string>
<string name="settings_plugins_manage_subtitle">Hantera dina tillägg</string>
<string name="settings_plugins_default_ability_title">Standardtillägg: %1$s</string>
<string name="settings_plugins_ability_metadata">Metadata</string>
<string name="settings_plugins_ability_audio">Ljud</string>
<string name="settings_plugins_ability_lyrics">Låttexter</string>
<string name="settings_plugins_ability_scrobble">Skrobbling</string>
<string name="settings_plugins_no_selection">Inget tillägg valt</string>
<string name="settings_plugins_action_change">Ändra</string>
<string name="settings_plugins_action_select">Välj</string>
<string name="settings_plugins_selected">Valt</string>
<string name="settings_plugins_clear">Rensa val</string>
<string name="settings_plugins_no_plugins">Inga tillägg är tillgängliga</string>
<string name="settings_plugins_plugin_content_description">Tillägget %1$s</string>
<string name="settings_plugins_clear_content_description">Rensa</string>
<string name="plugin_configure_title">Konfigurera</string>
<string name="plugin_screen_title">Tilläggshanterare</string>
<string name="plugin_error_enter_url">Ange en URL</string>
<string name="plugin_error_url_scheme">URL:en måste börja med http:// eller https://</string>
<string name="plugin_error_download_failed">Det gick inte att ladda ner tillägget</string>
<string name="plugin_empty_title">Inga tillägg har installerats ännu</string>
<string name="plugin_empty_subtitle">Klistra in en nedladdningsadress eller välj en fil ovan.</string>
<string name="plugin_installed_count">Installerade %2$s: %1$d</string>
<string name="plugin_installed_singular">tillägg</string>
<string name="plugin_installed_plural">tillägg</string>
<string name="plugin_install_section_title">Installera ett tillägg</string>
<string name="plugin_url_placeholder">https://example.com/plugin.smplug</string>
<string name="plugin_action_download">Ladda ner</string>
<string name="plugin_action_install_from_file">Installera från fil (.smplug)</string>
<string name="plugin_state_active">Aktivt</string>
<string name="plugin_version_label">v%1$s</string>
<string name="plugin_state_builtin">Inbyggt</string>
<string name="plugin_action_remove">Ta bort tillägg</string>
<string name="plugin_action_login">Logga in</string>
<string name="plugin_action_logout">Logga ut</string>
<string name="plugin_section_url_title">Ladda ner från URL</string>
<string name="plugin_section_file_title">Installera från fil</string>
<string name="plugin_section_install">Installera</string>
<string name="plugin_permissions_author_version">• %1$s</string>
<string name="plugin_permissions_requested_title">Begärda behörigheter</string>
<string name="plugin_permissions_none">Det här tillägget kräver inga särskilda behörigheter.</string>
<string name="plugin_permissions_compare_title">Installerat och tillhandahållet</string>
<string name="plugin_permissions_installed_label">Installerat</string>
<string name="plugin_permissions_supplied_label">Tillhandahållet</string>
<string name="plugin_permissions_api_diff">API %1$s → %2$s</string>
<string name="plugin_permissions_capability_storage_title">Beständig lagring</string>
<string name="plugin_permissions_capability_storage_desc">Läsa och skriva data på disken</string>
<string name="plugin_permissions_capability_network_title">Nätverksförfrågningar</string>
<string name="plugin_permissions_capability_network_desc">Skicka och ta emot data via internet</string>
<string name="plugin_permissions_capability_webview_title">WebView</string>
<string name="plugin_permissions_capability_webview_desc">Visa webbinnehåll i appen</string>
</resources>

View File

@ -907,6 +907,7 @@
<string name="plugin_discover_badge_verified">Verified</string> <string name="plugin_discover_badge_verified">Verified</string>
<string name="plugin_discover_stars_content_description">GitHub stars</string> <string name="plugin_discover_stars_content_description">GitHub stars</string>
<string name="plugin_discover_repository_content_description">GitHub repository URL</string> <string name="plugin_discover_repository_content_description">GitHub repository URL</string>
<string name="plugin_discover_repository_host">github.com</string>
<!-- Plugin release install dialog --> <!-- Plugin release install dialog -->
<string name="plugin_install_no_releases">No releases found</string> <string name="plugin_install_no_releases">No releases found</string>

View File

@ -1,34 +0,0 @@
/*
* 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.tools.user_agents
object UserAgents {
private val agents = listOf(
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/147.0.0.0 Safari/537.36",
"Mozilla/5.0 (Linux; Android 15; ALI-NX1 Build/HONORALI-N21) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7727.92 Mobile Safari/537.36",
"Mozilla/5.0 (Linux; Android 14; SM-A528B Build/UP1A.231005.007) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/147.0.7727.108 Mobile Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36 Edg/146.0.0.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.7; rv:68.0) Gecko/20100101 Firefox/68.0 Waterfox/56.6.2021.2816",
"Mozilla/5.0 (Macintosh; Intel Mac OS X10.14; rv:65.0) Gecko/20100101 Firefox/65.0 Waterfox/56.2.7",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36 Vivaldi/7.6.3797.63",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.91 Safari/537.36 Vivaldi/1.92.917.39",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36 Vivaldi/7.5.3735.47"
)
fun random(): String = agents.random()
}

View File

@ -1,170 +0,0 @@
/*
* 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.zipline.plugin_apis.newpipe_yt
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioAPI
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioFormat
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioQuality
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioSource
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import dev.krtirtho.spotube.core.di.injectLogger
import dev.krtirtho.spotube.core.newpipe.NewPipeService
import dev.krtirtho.spotube.core.newpipe.VideoSearchResult
import org.koin.core.component.KoinComponent
class RealNewPipeAudioAPI : AudioAPI, KoinComponent {
private val logger by injectLogger<RealNewPipeAudioAPI>()
private val newPipeService = NewPipeService()
companion object {
private val youtubeIDRegex = Regex("^[a-zA-Z0-9_-]{11}$")
}
private val isrcProvider = ISRCProviders()
private fun transformVideo(track: MetadataTrack, video: VideoSearchResult): AudioSource.Basic {
var confidence = 0f
val isTitleMatch = track.title.lowercase() in video.title.lowercase()
val isArtistInTitle = track.artists.any { it.name.lowercase() in video.title.lowercase() }
val isAlbumInTitle =
track.album != null && track.album!!.title.lowercase() in video.title.lowercase()
val isUploaderMatch =
video.uploader.lowercase() in (track.artists.firstOrNull()?.name?.lowercase() ?: "")
val isDurationMatch =
video.durationMs in (track.durationMs - 30_000)..(track.durationMs + 30_000) // Duration within ±30 seconds
if (isTitleMatch) confidence += 0.3f
if (isUploaderMatch) confidence += 0.3f
if (isDurationMatch) confidence += 0.2f // Duration within ±30 seconds
if (isArtistInTitle) confidence += 0.1f
if (isAlbumInTitle) confidence += 0.1f
return AudioSource.Basic(
id = video.id,
title = video.title,
artist = video.uploader,
album = null,
thumbnails = listOf(
Thumbnail(
url = video.thumbnailUrl,
width = 300,
height = 300)
),
externalUri = "https://www.youtube.com/watch?v=${video.id}",
confidence = confidence)
}
override val supportedQualities: List<AudioFormat> = listOf(
AudioFormat(
codec = "opus", container = "webm", qualities = listOf(
AudioQuality.Lossy(bitrate = 44_000),
AudioQuality.Lossy(bitrate = 96_000),
AudioQuality.Lossy(bitrate = 128_000),
AudioQuality.Lossy(bitrate = 256_000))
), AudioFormat(
codec = "aac", container = "mp4", qualities = listOf(
AudioQuality.Lossy(bitrate = 44_000),
AudioQuality.Lossy(bitrate = 96_000),
AudioQuality.Lossy(bitrate = 128_000),
AudioQuality.Lossy(bitrate = 256_000))
)
)
override suspend fun getStreamsByTrack(track: MetadataTrack): List<AudioSource> {
val isYouTubeUrl =
track.externalUri != null && (track.externalUri!!.contains("youtube.com") || track.externalUri!!.contains(
"youtu.be"
))
val isYouTubeID = track.id.matches(youtubeIDRegex)
logger.i {
"Searching streams for track(${track.id}): ${track.title} by ${
track.artists.joinToString(
", "
) { it.name }
}"
}
if (isYouTubeID || isYouTubeUrl) {
logger.i { "Track has YouTube ID or URL, fetching video info directly" }
return newPipeService.getVideoInfo(track.id).let { videoInfo ->
listOf(
AudioSource.Streamed(
id = videoInfo.id,
title = videoInfo.title,
artist = videoInfo.uploader,
album = null,
thumbnails = listOf(
Thumbnail(
url = videoInfo.thumbnailUrl,
width = 300,
height = 300)
),
externalUri = "https://www.youtube.com/watch?v=${videoInfo.id}",
confidence = 1.0f,
streams = videoInfo.audioStreams)
)
}
}
logger.i { "No YouTube ID or URL found, performing search with track metadata" }
val isrcCode = track.isrcCode ?: isrcProvider.auto(track)
val searchQuery = "${track.title} - ${track.artists.joinToString(", ") { it.name }}".trim()
logger.i { "Searching for video with query: $searchQuery" }
val videos = newPipeService.searchVideos(isrcCode ?: searchQuery)
logger.i { "Found ${videos.size} videos with query: $searchQuery" }
var sources = videos.map { transformVideo(track, it) }
if (isrcCode != null && ((sources.size < 2 && sources.any { it.confidence < 0.3f }) || sources.isEmpty())) {
logger.i { "ISRC search failed, fallback to title search" }
val videos = newPipeService.searchVideos(searchQuery)
sources = videos.map { transformVideo(track, it) }
}
logger.i { "Found ${sources.size} sources with confidence >= 0.3f" }
// Return 5 highest to lowest confidence
return sources.sortedByDescending { it.confidence }.take(5)
}
override suspend fun getStreamsOfAudioSource(source: AudioSource.Basic): List<AudioSource.Streamed> {
return newPipeService.getVideoInfo(source.id).let { videoInfo ->
listOf(
AudioSource.Streamed(
id = videoInfo.id,
title = videoInfo.title,
artist = videoInfo.uploader,
album = null,
thumbnails = listOf(
Thumbnail(
url = videoInfo.thumbnailUrl,
width = 300,
height = 300)
),
externalUri = "https://www.youtube.com/watch?v=${videoInfo.id}",
confidence = source.confidence,
streams = videoInfo.audioStreams
)
)
}
}
}

View File

@ -1,2 +0,0 @@
package dev.krtirtho.spotube.modules.blacklist

View File

@ -1,329 +0,0 @@
/*
* 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.modules.lyrics
import org.spotube.millennium.composables.MillenniumBarsLoader
import org.spotube.millennium.composables.MillenniumIcon
import org.spotube.millennium.composables.MillenniumIconButton
import org.spotube.millennium.composables.MillenniumIconButtonStyle
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import dev.krtirtho.spotube.core.ui.theming.AppTheme
import com.composeunstyled.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import compose.icons.FeatherIcons
import compose.icons.feathericons.X
import dev.krtirtho.plugin_interfaces.plugin_apis.lyrics.LyricsLine
import dev.krtirtho.spotube.core.ui.component.ApplicationBackButton
import dev.krtirtho.spotube.modules.shell.LocalAppShellBottomInset
import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxArrowDown4
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import org.koin.compose.viewmodel.koinViewModel
import spotube.composeapp.generated.resources.Res
import spotube.composeapp.generated.resources.common_unknown_error
import spotube.composeapp.generated.resources.lyrics_mode_plain
import spotube.composeapp.generated.resources.lyrics_mode_synced
import spotube.composeapp.generated.resources.lyrics_no_plain
import spotube.composeapp.generated.resources.lyrics_no_synced
import spotube.composeapp.generated.resources.common_close
@Composable
fun LyricsScreen(
viewModel: LyricsViewModel = koinViewModel(),
modifier: Modifier = Modifier,
onClose: (() -> Unit)? = null) {
val uiState by viewModel.uiState.collectAsState()
val positionMillis by viewModel.positionMillisFlow.collectAsState()
val scope = rememberCoroutineScope()
Column(modifier = modifier.fillMaxSize().background(AppTheme.colors.background)) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically) {
if (onClose != null) {
MillenniumIconButton(
onClick = onClose,
style = MillenniumIconButtonStyle.Quiet
) {
MillenniumIcon(
imageVector = Iconsax.IconsaxArrowDown4,
contentDescription = stringResource(Res.string.common_close)
)
}
} else {
ApplicationBackButton()
}
Spacer(Modifier.weight(1f))
Row(
modifier = Modifier
.clip(RoundedCornerShape(50.dp))
.background(AppTheme.colors.surfaceVariant)
.padding(3.dp),
horizontalArrangement = Arrangement.spacedBy(3.dp)
) {
LyricsModeTab(
label = stringResource(Res.string.lyrics_mode_synced),
isSelected = uiState.mode == LyricType.SYNCED,
onClick = { viewModel.setMode(LyricType.SYNCED) }
)
LyricsModeTab(
label = stringResource(Res.string.lyrics_mode_plain),
isSelected = uiState.mode == LyricType.STATIC,
onClick = { viewModel.setMode(LyricType.STATIC) }
)
}
Spacer(Modifier.weight(1f))
Spacer(Modifier.width(44.dp))
}
Column(
modifier = Modifier
.weight(1f)
.fillMaxWidth()
.padding(horizontal = 16.dp)
.padding(bottom = LocalAppShellBottomInset.current + 24.dp)
) {
Spacer(modifier = Modifier.height(16.dp))
when {
uiState.isLoading -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
MillenniumBarsLoader(size = 48.dp)
}
}
uiState.error != null -> {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = uiState.error ?: stringResource(Res.string.common_unknown_error),
style = AppTheme.typography.bodyLarge,
color = AppTheme.colors.error,
textAlign = TextAlign.Center
)
}
}
uiState.mode == LyricType.SYNCED -> {
SyncedLyricsContent(
syncedLyrics = uiState.syncedLyrics ?: emptyList(),
currentIndex = viewModel.currentLyricIndex(positionMillis),
onScrollToIndex = { index ->
scope.launch {
// scroll will be handled by LazyColumn
}
}
)
}
uiState.mode == LyricType.STATIC -> {
PlainLyricsContent(plainLyrics = uiState.plainLyrics ?: "")
}
}
}
}
}
@Composable
private fun LyricsModeTab(
label: String,
isSelected: Boolean,
onClick: () -> Unit) {
Box(
modifier = Modifier
.clip(RoundedCornerShape(50.dp))
.background(
if (isSelected) {
AppTheme.colors.primary
} else {
Color.Transparent
}
)
.clickable(onClick = onClick)
.padding(horizontal = 18.dp, vertical = 7.dp)
) {
Text(
text = label,
style = AppTheme.typography.labelLarge,
fontWeight = FontWeight.SemiBold,
color = if (isSelected) {
AppTheme.colors.onPrimary
} else {
AppTheme.colors.onSurfaceVariant
}
)
}
}
@Composable
private fun SyncedLyricsContent(
syncedLyrics: List<LyricsLine>,
currentIndex: Int,
onScrollToIndex: (Int) -> Unit) {
if (syncedLyrics.isEmpty()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(Res.string.lyrics_no_synced),
style = AppTheme.typography.bodyLarge,
color = AppTheme.colors.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
return
}
val listState = rememberLazyListState()
LaunchedEffect(currentIndex) {
if (currentIndex >= 0) {
listState.animateScrollToItem(
index = currentIndex.coerceIn(0, syncedLyrics.lastIndex),
scrollOffset = -100
)
}
}
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
itemsIndexed(syncedLyrics) { index, line ->
val isCurrentLine = index == currentIndex
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(10.dp))
.then(
if (isCurrentLine) {
Modifier.background(AppTheme.colors.primaryContainer)
} else {
Modifier
}
)
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.Top,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
text = formatLyricTime(line.time),
style = AppTheme.typography.labelSmall,
color = if (isCurrentLine) {
AppTheme.colors.primary
} else {
AppTheme.colors.onSurfaceVariant
},
modifier = Modifier.width(44.dp)
)
Text(
text = line.text.ifBlank { "..." },
modifier = Modifier.weight(1f),
style = AppTheme.typography.bodyLarge.copy(
fontWeight = if (isCurrentLine) FontWeight.Bold else FontWeight.Normal,
color = if (isCurrentLine) {
AppTheme.colors.primary
} else {
AppTheme.colors.onSurface
}
)
)
}
}
}
}
private fun formatLyricTime(millis: Long): String {
val totalSeconds = (millis / 1000).coerceAtLeast(0)
val minutes = totalSeconds / 60
val seconds = totalSeconds % 60
return "$minutes:${seconds.toString().padStart(2, '0')}"
}
@Composable
private fun PlainLyricsContent(
plainLyrics: String) {
if (plainLyrics.isBlank()) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = stringResource(Res.string.lyrics_no_plain),
style = AppTheme.typography.bodyLarge,
color = AppTheme.colors.onSurfaceVariant,
textAlign = TextAlign.Center
)
}
return
}
LazyColumn {
item {
Text(
text = plainLyrics,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
style = AppTheme.typography.bodyLarge.copy(lineHeight = 30.sp),
color = AppTheme.colors.onSurface,
textAlign = TextAlign.Center
)
}
}
}

View File

@ -1,3 +0,0 @@
package dev.krtirtho.spotube.resources.iconsax
object Iconsax

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube package org.spotube.spotube
import androidx.compose.animation.ExperimentalSharedTransitionApi import androidx.compose.animation.ExperimentalSharedTransitionApi
import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.SharedTransitionLayout
@ -33,42 +33,42 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import dev.krtirtho.spotube.core.DebugConfig import org.spotube.spotube.core.DebugConfig
import dev.krtirtho.spotube.core.i18n.AppLocaleProvider import org.spotube.spotube.core.i18n.AppLocaleProvider
import dev.krtirtho.spotube.core.navigation.Navigator import org.spotube.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes import org.spotube.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES import org.spotube.spotube.core.navigation.TOP_LEVEL_ROUTES
import dev.krtirtho.spotube.core.navigation.navPopTransitionSpec import org.spotube.spotube.core.navigation.navPopTransitionSpec
import dev.krtirtho.spotube.core.navigation.navPredictivePopTransitionSpec import org.spotube.spotube.core.navigation.navPredictivePopTransitionSpec
import dev.krtirtho.spotube.core.navigation.navPushTransitionSpec import org.spotube.spotube.core.navigation.navPushTransitionSpec
import dev.krtirtho.spotube.core.navigation.rememberNavigationState import org.spotube.spotube.core.navigation.rememberNavigationState
import dev.krtirtho.spotube.core.navigation.toEntries import org.spotube.spotube.core.navigation.toEntries
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope import org.spotube.spotube.core.ui.component.LocalSharedTransitionScope
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme import org.spotube.spotube.core.ui.theming.SpotubeTheme
import dev.krtirtho.spotube.modules.library.LibraryTab import org.spotube.spotube.modules.library.LibraryTab
import dev.krtirtho.spotube.modules.settings.SettingsRepository import org.spotube.spotube.modules.settings.SettingsRepository
import dev.krtirtho.spotube.modules.settings.UserSettings import org.spotube.spotube.modules.settings.UserSettings
import dev.krtirtho.spotube.modules.shell.AppShell import org.spotube.spotube.modules.shell.AppShell
import dev.krtirtho.spotube.modules.support.LocalSupportPrompt import org.spotube.spotube.modules.support.LocalSupportPrompt
import dev.krtirtho.spotube.modules.support.SupportDialog import org.spotube.spotube.modules.support.SupportDialog
import dev.krtirtho.spotube.modules.support.SupportReminderIntervalMillis import org.spotube.spotube.modules.support.SupportReminderIntervalMillis
import dev.krtirtho.spotube.modules.update.AppUpdateDialog import org.spotube.spotube.modules.update.AppUpdateDialog
import dev.krtirtho.spotube.modules.update.AppUpdateService import org.spotube.spotube.modules.update.AppUpdateService
import dev.krtirtho.spotube.modules.update.PluginUpdateDialog import org.spotube.spotube.modules.update.PluginUpdateDialog
import dev.krtirtho.spotube.modules.update.PluginUpdateService import org.spotube.spotube.modules.update.PluginUpdateService
import dev.krtirtho.spotube.modules.webview.WebViewScreen import org.spotube.spotube.modules.webview.WebViewScreen
import dev.krtirtho.spotube.modules.welcome.WelcomeRepository import org.spotube.spotube.modules.welcome.WelcomeRepository
import dev.krtirtho.spotube.resources.iconsax.Iconsax import org.spotube.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd import org.spotube.spotube.resources.iconsax.IconsaxCd
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive import org.spotube.spotube.resources.iconsax.IconsaxDirectboxReceive
import dev.krtirtho.spotube.resources.iconsax.IconsaxHome import org.spotube.spotube.resources.iconsax.IconsaxHome
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirrorScreenRegular import org.spotube.spotube.resources.iconsax.IconsaxMirrorScreenRegular
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicDashboard import org.spotube.spotube.resources.iconsax.IconsaxMusicDashboard
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary import org.spotube.spotube.resources.iconsax.IconsaxMusicLibrary
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch import org.spotube.spotube.resources.iconsax.IconsaxSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2 import org.spotube.spotube.resources.iconsax.IconsaxSetting2
import dev.krtirtho.spotube.resources.iconsax.IconsaxSoundTwotone import org.spotube.spotube.resources.iconsax.IconsaxSoundTwotone
import dev.krtirtho.spotube.resources.iconsax.User import org.spotube.spotube.resources.iconsax.User
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlin.time.Clock import kotlin.time.Clock
import org.koin.compose.koinInject import org.koin.compose.koinInject
@ -253,7 +253,7 @@ fun App(
onDismiss = appUpdateService::dismissUpdate, onDismiss = appUpdateService::dismissUpdate,
onIgnore = appUpdateService::ignoreUpdate, onIgnore = appUpdateService::ignoreUpdate,
onUpdate = { onUpdate = {
runCatching { openUrlInBrowser("https://spotube.cc/downloads/") } runCatching { openUrlInBrowser("https://spotube.org/downloads/") }
appUpdateService.dismissUpdate() appUpdateService.dismissUpdate()
}) })
} }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube package org.spotube.spotube
enum class PlatformType { enum class PlatformType {
Android, IOS, Windows, Linux, MacOS, Unknown Android, IOS, Windows, Linux, MacOS, Unknown

View File

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core package org.spotube.spotube.core
/** /**
* Development-only switches. These are intentionally kept out of [dev.krtirtho.spotube.modules.settings.UserSettings] * Development-only switches. These are intentionally kept out of [org.spotube.spotube.modules.settings.UserSettings]
* so they can never leak into user-facing configuration. * so they can never leak into user-facing configuration.
*/ */
object DebugConfig { object DebugConfig {

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlin.time.Duration import kotlin.time.Duration

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow

View File

@ -15,11 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import dev.krtirtho.spotube.core.db.Database import org.spotube.spotube.core.db.Database
import dev.krtirtho.spotube.core.db.DatabaseKeys import org.spotube.spotube.core.db.DatabaseKeys
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json

View File

@ -15,13 +15,13 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.audioplayer package org.spotube.spotube.core.audioplayer
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
import dev.krtirtho.spotube.core.di.injectLogger import org.spotube.spotube.core.di.injectLogger
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository import org.spotube.spotube.modules.blacklist.BlacklistRepository
import dev.krtirtho.spotube.modules.plugin.PluginProvider import org.spotube.spotube.modules.plugin.PluginProvider
import dev.krtirtho.spotube.modules.settings.SettingsProvider import org.spotube.spotube.modules.settings.SettingsProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO

View File

@ -15,14 +15,14 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.db package org.spotube.spotube.core.db
import androidx.datastore.core.DataStore import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.PreferenceDataStoreFactory import androidx.datastore.preferences.core.PreferenceDataStoreFactory
import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import dev.krtirtho.spotube.core.paths.Paths import org.spotube.spotube.core.paths.Paths
import okio.FileSystem import okio.FileSystem
import okio.Path.Companion.toPath import okio.Path.Companion.toPath
import okio.SYSTEM import okio.SYSTEM
@ -77,6 +77,12 @@ class Database(val paths: Paths) {
) )
} }
val windowStateDataStore: DataStore<Preferences> by lazy {
PreferenceDataStoreFactory.createWithPath(
produceFile = { "${paths.getApplicationDataDirPath()}/spotube_window.preferences_pb".toPath() }
)
}
private companion object { private companion object {
/** Preference files written by previous launches; their presence means an upgrade. */ /** Preference files written by previous launches; their presence means an upgrade. */
val EXISTING_INSTALL_PROBE_FILES = listOf( val EXISTING_INSTALL_PROBE_FILES = listOf(
@ -95,4 +101,5 @@ object DatabaseKeys {
val AUDIO_PLAYER_QUEUE_STATE_KEY = stringPreferencesKey("audio_player_queue_state") val AUDIO_PLAYER_QUEUE_STATE_KEY = stringPreferencesKey("audio_player_queue_state")
val LOCAL_MEDIA_CACHE_STATE_KEY = stringPreferencesKey("local_media_cache_state") val LOCAL_MEDIA_CACHE_STATE_KEY = stringPreferencesKey("local_media_cache_state")
val LOCAL_MEDIA_LAST_SCAN_AT_KEY = longPreferencesKey("local_media_last_scan_at") val LOCAL_MEDIA_LAST_SCAN_AT_KEY = longPreferencesKey("local_media_last_scan_at")
val WINDOW_STATE_KEY = stringPreferencesKey("window_state")
} }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.deeplink package org.spotube.spotube.core.deeplink
/** /**
* Cross-platform receiver for URIs handed to the app by the operating system * Cross-platform receiver for URIs handed to the app by the operating system

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.di package org.spotube.spotube.core.di
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.di package org.spotube.spotube.core.di
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import co.touchlab.kermit.koin.KermitKoinLogger import co.touchlab.kermit.koin.KermitKoinLogger

View File

@ -15,82 +15,82 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.di package org.spotube.spotube.core.di
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueueRepository import org.spotube.spotube.core.audioplayer.AudioPlayerQueueRepository
import dev.krtirtho.spotube.core.audioplayer.DeviceAudioPlayerQueue import org.spotube.spotube.core.audioplayer.DeviceAudioPlayerQueue
import dev.krtirtho.spotube.core.audioplayer.QueueStateRepository import org.spotube.spotube.core.audioplayer.QueueStateRepository
import dev.krtirtho.spotube.core.db.Database import org.spotube.spotube.core.db.Database
import dev.krtirtho.spotube.core.discovery.DeviceDiscoveryService import org.spotube.spotube.core.discovery.DeviceDiscoveryService
import dev.krtirtho.spotube.core.discord.DiscordRpcService import org.spotube.spotube.core.discord.DiscordRpcService
import dev.krtirtho.spotube.core.jam.JamRoomClient import org.spotube.spotube.core.jam.JamRoomClient
import dev.krtirtho.spotube.core.jam.JamRoomService import org.spotube.spotube.core.jam.JamRoomService
import dev.krtirtho.spotube.core.navigation.navigationModule import org.spotube.spotube.core.navigation.navigationModule
import dev.krtirtho.spotube.core.remote.RemoteControlClient import org.spotube.spotube.core.remote.RemoteControlClient
import dev.krtirtho.spotube.core.remote.RemoteControlHandler import org.spotube.spotube.core.remote.RemoteControlHandler
import dev.krtirtho.spotube.core.remote.RemoteControlService import org.spotube.spotube.core.remote.RemoteControlService
import dev.krtirtho.spotube.core.remote.RemotePlaybackController import org.spotube.spotube.core.remote.RemotePlaybackController
import dev.krtirtho.spotube.core.scrobble.ScrobbleManager import org.spotube.spotube.core.scrobble.ScrobbleManager
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper import org.spotube.spotube.core.playback.CollectionPlaybackHelper
import dev.krtirtho.spotube.core.server.AlternativeTracksRepository import org.spotube.spotube.core.server.AlternativeTracksRepository
import dev.krtirtho.spotube.core.server.CacheManager import org.spotube.spotube.core.server.CacheManager
import dev.krtirtho.spotube.core.server.LocalServer import org.spotube.spotube.core.server.LocalServer
import dev.krtirtho.spotube.core.server.MatchedTracksRepository import org.spotube.spotube.core.server.MatchedTracksRepository
import dev.krtirtho.spotube.core.server.TrackSourceRepository import org.spotube.spotube.core.server.TrackSourceRepository
import dev.krtirtho.spotube.core.server.StreamingUrlRepository import org.spotube.spotube.core.server.StreamingUrlRepository
import dev.krtirtho.spotube.core.webview.WebViewController import org.spotube.spotube.core.webview.WebViewController
import dev.krtirtho.spotube.modules.album.AlbumRepository import org.spotube.spotube.modules.album.AlbumRepository
import dev.krtirtho.spotube.modules.album.AlbumViewModel import org.spotube.spotube.modules.album.AlbumViewModel
import dev.krtirtho.spotube.modules.artist.ArtistRepository import org.spotube.spotube.modules.artist.ArtistRepository
import dev.krtirtho.spotube.modules.artist.ArtistViewModel import org.spotube.spotube.modules.artist.ArtistViewModel
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository import org.spotube.spotube.modules.blacklist.BlacklistRepository
import dev.krtirtho.spotube.modules.blacklist.BlacklistViewModel import org.spotube.spotube.modules.blacklist.BlacklistViewModel
import dev.krtirtho.spotube.modules.devices.DevicesViewModel import org.spotube.spotube.modules.devices.DevicesViewModel
import dev.krtirtho.spotube.modules.devices.RemoteControlViewModel import org.spotube.spotube.modules.devices.RemoteControlViewModel
import dev.krtirtho.spotube.modules.jam.JamViewModel import org.spotube.spotube.modules.jam.JamViewModel
import dev.krtirtho.spotube.modules.downloads.DownloadManager import org.spotube.spotube.modules.downloads.DownloadManager
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import org.spotube.spotube.modules.downloads.DownloadsViewModel
import dev.krtirtho.spotube.modules.home.HomeScreenRepository import org.spotube.spotube.modules.home.HomeScreenRepository
import dev.krtirtho.spotube.modules.home.HomeScreenViewModel import org.spotube.spotube.modules.home.HomeScreenViewModel
import dev.krtirtho.spotube.modules.plugin.MetadataAuthViewModel import org.spotube.spotube.modules.plugin.MetadataAuthViewModel
import dev.krtirtho.spotube.modules.library.LibraryRepository import org.spotube.spotube.modules.library.LibraryRepository
import dev.krtirtho.spotube.modules.library.LibraryState import org.spotube.spotube.modules.library.LibraryState
import dev.krtirtho.spotube.modules.library.album.LibraryAlbumsViewModel import org.spotube.spotube.modules.library.album.LibraryAlbumsViewModel
import dev.krtirtho.spotube.modules.library.artist.LibraryArtistsViewModel import org.spotube.spotube.modules.library.artist.LibraryArtistsViewModel
import dev.krtirtho.spotube.modules.library.local_tracks.LibraryLocalTracksViewModel import org.spotube.spotube.modules.library.local_tracks.LibraryLocalTracksViewModel
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaCacheRepository import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaCacheRepository
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaFoldersConfig import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaFoldersConfig
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaLibraryCoordinator import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaLibraryCoordinator
import dev.krtirtho.spotube.modules.library.playlist.LibraryPlaylistsViewModel import org.spotube.spotube.modules.library.playlist.LibraryPlaylistsViewModel
import dev.krtirtho.spotube.modules.lyrics.LyricsViewModel import org.spotube.spotube.modules.lyrics.LyricsViewModel
import dev.krtirtho.spotube.modules.playlist.PlaylistRepository import org.spotube.spotube.modules.playlist.PlaylistRepository
import dev.krtirtho.spotube.modules.playlist.PlaylistViewModel import org.spotube.spotube.modules.playlist.PlaylistViewModel
import dev.krtirtho.spotube.modules.plugin.AudioPluginSource import org.spotube.spotube.modules.plugin.AudioPluginSource
import dev.krtirtho.spotube.modules.plugin.PluginManager import org.spotube.spotube.modules.plugin.PluginManager
import dev.krtirtho.spotube.modules.plugin.PluginProvider import org.spotube.spotube.modules.plugin.PluginProvider
import dev.krtirtho.spotube.modules.plugin.PluginViewModel import org.spotube.spotube.modules.plugin.PluginViewModel
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuardViewModel import org.spotube.spotube.modules.plugin.guard.MetadataPluginGuardViewModel
import dev.krtirtho.spotube.modules.plugin.ScrobblePluginSource import org.spotube.spotube.modules.plugin.ScrobblePluginSource
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository import org.spotube.spotube.modules.saved_tracks.SavedTracksRepository
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel import org.spotube.spotube.modules.saved_tracks.SavedTracksViewModel
import dev.krtirtho.spotube.modules.search.SearchRepository import org.spotube.spotube.modules.search.SearchRepository
import dev.krtirtho.spotube.modules.search.SearchScreenViewModel import org.spotube.spotube.modules.search.SearchScreenViewModel
import dev.krtirtho.spotube.modules.update.AppUpdateService import org.spotube.spotube.modules.update.AppUpdateService
import dev.krtirtho.spotube.modules.update.PluginUpdateService import org.spotube.spotube.modules.update.PluginUpdateService
import dev.krtirtho.spotube.modules.settings.SettingsProvider import org.spotube.spotube.modules.settings.SettingsProvider
import dev.krtirtho.spotube.modules.settings.SettingsRepository import org.spotube.spotube.modules.settings.SettingsRepository
import dev.krtirtho.spotube.modules.settings.UserSettingsSource import org.spotube.spotube.modules.settings.UserSettingsSource
import dev.krtirtho.spotube.modules.settings.SettingsViewModel import org.spotube.spotube.modules.settings.SettingsViewModel
import dev.krtirtho.spotube.modules.settings.JamSettingsViewModel import org.spotube.spotube.modules.settings.JamSettingsViewModel
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersViewModel import org.spotube.spotube.modules.settings.LocalMediaFoldersViewModel
import dev.krtirtho.spotube.modules.shell.AppShellViewModel import org.spotube.spotube.modules.shell.AppShellViewModel
import dev.krtirtho.spotube.modules.shell.PlayerOptionsViewModel import org.spotube.spotube.modules.shell.PlayerOptionsViewModel
import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel import org.spotube.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel
import dev.krtirtho.spotube.modules.shell.player_queue.PlayerQueueContentViewModel import org.spotube.spotube.modules.shell.player_queue.PlayerQueueContentViewModel
import dev.krtirtho.spotube.modules.welcome.WelcomeRepository import org.spotube.spotube.modules.welcome.WelcomeRepository
import dev.krtirtho.spotube.modules.welcome.WelcomeViewModel import org.spotube.spotube.modules.welcome.WelcomeViewModel
import org.koin.core.module.Module import org.koin.core.module.Module
import org.koin.core.module.dsl.bind import org.koin.core.module.dsl.bind
import org.koin.core.module.dsl.createdAtStart import org.koin.core.module.dsl.createdAtStart

View File

@ -15,12 +15,12 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.discord package org.spotube.spotube.core.discord
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
import dev.krtirtho.spotube.core.audioplayer.PlayerState import org.spotube.spotube.core.audioplayer.PlayerState
import dev.krtirtho.spotube.core.di.injectLogger import org.spotube.spotube.core.di.injectLogger
import dev.krtirtho.spotube.modules.settings.SettingsProvider import org.spotube.spotube.modules.settings.SettingsProvider
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO import kotlinx.coroutines.IO
@ -202,7 +202,7 @@ class DiscordRpcService(
private data class DiscordRpcState( private data class DiscordRpcState(
val enabled: Boolean, val enabled: Boolean,
val playerState: PlayerState, val playerState: PlayerState,
val mediaItem: dev.krtirtho.spotube.core.audioplayer.MediaItem?, val mediaItem: org.spotube.spotube.core.audioplayer.MediaItem?,
val position: kotlin.time.Duration, val position: kotlin.time.Duration,
val duration: kotlin.time.Duration) val duration: kotlin.time.Duration)
} }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.discovery package org.spotube.spotube.core.discovery
import com.appstractive.dnssd.DiscoveryEvent import com.appstractive.dnssd.DiscoveryEvent
import com.appstractive.dnssd.NetService import com.appstractive.dnssd.NetService

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.discovery package org.spotube.spotube.core.discovery
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.extras package org.spotube.spotube.core.extras
fun String.kebabToTitleCase(): String { fun String.kebabToTitleCase(): String {
return this.split("-").joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } } return this.split("-").joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.i18n package org.spotube.spotube.core.i18n
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
@ -23,7 +23,7 @@ import androidx.compose.runtime.ProvidedValue
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import dev.krtirtho.spotube.modules.settings.SupportedLanguages import org.spotube.spotube.modules.settings.SupportedLanguages
/** /**
* Points the platform locale (which Compose resources resolve strings from) at [languageTag], * Points the platform locale (which Compose resources resolve strings from) at [languageTag],

View File

@ -15,11 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.jam package org.spotube.spotube.core.jam
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import dev.krtirtho.spotube.core.audioplayer.MediaItem import org.spotube.spotube.core.audioplayer.MediaItem
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import org.spotube.spotube.core.audioplayer.QueueEntry
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -168,7 +168,7 @@ data class JamMediaItem(
duration = kotlin.time.Duration.parse("${item.durationMs}ms"), duration = kotlin.time.Duration.parse("${item.durationMs}ms"),
coverURL = item.coverUrl, coverURL = item.coverUrl,
url = item.url, url = item.url,
protocol = dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol protocol = org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
.valueOf(item.protocol.ifBlank { "PROGRESSIVE" })) .valueOf(item.protocol.ifBlank { "PROGRESSIVE" }))
} }
} }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.jam package org.spotube.spotube.core.jam
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import com.ditchoom.buffer.Charset import com.ditchoom.buffer.Charset
@ -29,7 +29,7 @@ import com.ditchoom.mqtt.controlpacket.QualityOfService
import com.ditchoom.mqtt.controlpacket.TopicName import com.ditchoom.mqtt.controlpacket.TopicName
import com.ditchoom.mqtt.controlpacket.WillConfig import com.ditchoom.mqtt.controlpacket.WillConfig
import com.ditchoom.mqtt5.controlpacket.ConnectionRequest import com.ditchoom.mqtt5.controlpacket.ConnectionRequest
import dev.krtirtho.spotube.modules.settings.JamBroker import org.spotube.spotube.modules.settings.JamBroker
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.jam package org.spotube.spotube.core.jam
import kotlin.random.Random import kotlin.random.Random

View File

@ -15,18 +15,18 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.jam package org.spotube.spotube.core.jam
import co.touchlab.kermit.Logger import co.touchlab.kermit.Logger
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist import org.spotube.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.Thumbnail import org.spotube.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
import dev.krtirtho.spotube.core.audioplayer.PlayerState import org.spotube.spotube.core.audioplayer.PlayerState
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import org.spotube.spotube.core.audioplayer.QueueEntry
import dev.krtirtho.spotube.modules.settings.SettingsRepository import org.spotube.spotube.modules.settings.SettingsRepository
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -481,7 +481,7 @@ class JamRoomService(
return configured.ifBlank { "$fallbackPrefix-${Random.nextInt(1000, 9999)}" } return configured.ifBlank { "$fallbackPrefix-${Random.nextInt(1000, 9999)}" }
} }
private fun newClientId(broker: dev.krtirtho.spotube.modules.settings.JamBroker): String { private fun newClientId(broker: org.spotube.spotube.modules.settings.JamBroker): String {
val suffix = buildString(6) { val suffix = buildString(6) {
val chars = "0123456789abcdef" val chars = "0123456789abcdef"
repeat(6) { append(chars[Random.nextInt(chars.length)]) } repeat(6) { append(chars[Random.nextInt(chars.length)]) }

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.navigation package org.spotube.spotube.core.navigation
import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.AnimatedContentTransitionScope
import androidx.compose.animation.ContentTransform import androidx.compose.animation.ContentTransform

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.navigation package org.spotube.spotube.core.navigation
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.receiveAsFlow

View File

@ -15,27 +15,27 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.navigation package org.spotube.spotube.core.navigation
import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.NavKey
import dev.krtirtho.spotube.modules.album.AlbumScreen import org.spotube.spotube.modules.album.AlbumScreen
import dev.krtirtho.spotube.modules.artist.ArtistScreen import org.spotube.spotube.modules.artist.ArtistScreen
import dev.krtirtho.spotube.modules.blacklist.BlacklistScreen import org.spotube.spotube.modules.blacklist.BlacklistScreen
import dev.krtirtho.spotube.modules.devices.DevicesScreen import org.spotube.spotube.modules.devices.DevicesScreen
import dev.krtirtho.spotube.modules.devices.RemoteControlScreen import org.spotube.spotube.modules.devices.RemoteControlScreen
import dev.krtirtho.spotube.modules.jam.JamScreen import org.spotube.spotube.modules.jam.JamScreen
import dev.krtirtho.spotube.modules.home.HomeScreen import org.spotube.spotube.modules.home.HomeScreen
import dev.krtirtho.spotube.modules.library.LibraryScreen import org.spotube.spotube.modules.library.LibraryScreen
import dev.krtirtho.spotube.modules.lyrics.LyricsScreen import org.spotube.spotube.modules.lyrics.LyricsScreen
import dev.krtirtho.spotube.modules.playlist.PlaylistScreen import org.spotube.spotube.modules.playlist.PlaylistScreen
import dev.krtirtho.spotube.modules.plugin.PluginScreen import org.spotube.spotube.modules.plugin.PluginScreen
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuard import org.spotube.spotube.modules.plugin.guard.MetadataPluginGuard
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID import org.spotube.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen import org.spotube.spotube.modules.saved_tracks.SavedTracksScreen
import dev.krtirtho.spotube.modules.search.SearchScreen import org.spotube.spotube.modules.search.SearchScreen
import dev.krtirtho.spotube.modules.settings.SettingsScreen import org.spotube.spotube.modules.settings.SettingsScreen
import dev.krtirtho.spotube.modules.settings.JamSettingsScreen import org.spotube.spotube.modules.settings.JamSettingsScreen
import dev.krtirtho.spotube.modules.welcome.WelcomeScreen import org.spotube.spotube.modules.welcome.WelcomeScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.koin.compose.viewmodel.koinViewModel import org.koin.compose.viewmodel.koinViewModel
import org.koin.core.annotation.KoinExperimentalAPI import org.koin.core.annotation.KoinExperimentalAPI

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.navigation package org.spotube.spotube.core.navigation
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
@ -34,7 +34,7 @@ import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
import androidx.savedstate.serialization.SavedStateConfiguration import androidx.savedstate.serialization.SavedStateConfiguration
import dev.krtirtho.spotube.core.di.injectLogger import org.spotube.spotube.core.di.injectLogger
import kotlinx.serialization.PolymorphicSerializer import kotlinx.serialization.PolymorphicSerializer
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.polymorphic

View File

@ -15,9 +15,9 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package dev.krtirtho.spotube.core.newpipe package org.spotube.spotube.core.newpipe
import dev.krtirtho.plugin_interfaces.plugin_apis.audio.AudioStream import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable

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