Compare commits
14 Commits
50905080b8
...
cf6253a1d8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf6253a1d8 | ||
|
|
aa83708499 | ||
|
|
62c1fe7902 | ||
|
|
9fba2aa33d | ||
|
|
9db35e092b | ||
|
|
2474c26a83 | ||
|
|
7743d6d4c3 | ||
|
|
77a9cef3a1 | ||
|
|
c9d16a25c6 | ||
|
|
da394d40ea | ||
|
|
09d77211b9 | ||
|
|
61dd89f3ed | ||
|
|
ba62ff03c3 | ||
|
|
b0327fd224 |
2
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -76,7 +76,7 @@ body:
|
||||
description: Where did you install Spotube from?
|
||||
multiple: true
|
||||
options:
|
||||
- "Website (spotube.cc)"
|
||||
- "Website (spotube.org)"
|
||||
- "GitHub Releases (Binary)"
|
||||
- "GitHub Actions (Nightly Binary)"
|
||||
- "Play Store (Android)"
|
||||
|
||||
2
.github/ISSUE_TEMPLATE/config.yml
vendored
@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- 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
|
||||
|
||||
88
.github/workflows/spotube-release-binary.yml
vendored
@ -27,8 +27,8 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
COMPOSE_WEBVIEW_REPO: kdroidFilter/ComposeNativeWebview
|
||||
COMPOSE_WEBVIEW_COMMIT: 5aeb268a2c37ff4dbfb76924573344a7c4f763ce
|
||||
COMPOSE_WEBVIEW_REPO: KRTirtho/ComposeNativeWebview
|
||||
COMPOSE_WEBVIEW_COMMIT: cbd36f88927298a28a2d64c6acf7ec3b2def6f9d
|
||||
GRADLE_PLUGIN_REPO: team-spotube/gradle-plugin
|
||||
VLCJ_BUNDLER_REPO: team-spotube/vlcj-bundler-gradle-plugin
|
||||
|
||||
@ -141,11 +141,16 @@ jobs:
|
||||
EOF
|
||||
|
||||
- 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
|
||||
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
|
||||
|
||||
- name: Upload APK
|
||||
@ -176,7 +181,7 @@ jobs:
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
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
|
||||
uses: actions/setup-java@v4
|
||||
@ -193,13 +198,19 @@ jobs:
|
||||
- name: Make gradlew executable
|
||||
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
|
||||
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
|
||||
run: |
|
||||
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1)
|
||||
RPM_FILE=$(find composeApp/build -name "*.rpm" -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/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
|
||||
cp "$DEB_FILE" Spotube-linux-x86_64.deb
|
||||
cp "$RPM_FILE" Spotube-linux-x86_64.rpm
|
||||
|
||||
@ -237,7 +248,7 @@ jobs:
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
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
|
||||
uses: actions/setup-java@v4
|
||||
@ -254,13 +265,19 @@ jobs:
|
||||
- name: Make gradlew executable
|
||||
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
|
||||
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
|
||||
run: |
|
||||
DEB_FILE=$(find composeApp/build -name "*.deb" -type f | head -1)
|
||||
RPM_FILE=$(find composeApp/build -name "*.rpm" -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/compose/binaries/main/graalvm-rpm -maxdepth 1 -name "*.rpm" -type f | head -1)
|
||||
cp "$DEB_FILE" Spotube-linux-aarch64.deb
|
||||
cp "$RPM_FILE" Spotube-linux-aarch64.rpm
|
||||
|
||||
@ -307,13 +324,25 @@ jobs:
|
||||
- name: Set up Gradle
|
||||
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
|
||||
run: ./gradlew :composeApp:packageReleaseExe
|
||||
run: ./gradlew :composeApp:packageGraalvmExe -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
- name: Rename Windows artifacts
|
||||
shell: pwsh
|
||||
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
|
||||
|
||||
- name: Upload EXE
|
||||
@ -356,12 +385,18 @@ jobs:
|
||||
- name: Make gradlew executable
|
||||
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
|
||||
run: ./gradlew :composeApp:packageReleaseDmg
|
||||
run: ./gradlew :composeApp:packageGraalvmDmg -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
- name: Rename macOS artifact
|
||||
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
|
||||
|
||||
- name: Upload DMG
|
||||
@ -389,18 +424,35 @@ jobs:
|
||||
steps:
|
||||
- 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
|
||||
id: vars
|
||||
run: |
|
||||
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}"
|
||||
echo "is_prerelease=false" >> "$GITHUB_OUTPUT"
|
||||
echo "is_draft=true" >> "$GITHUB_OUTPUT"
|
||||
echo "release_name=Spotube ${TAG}" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
TAG="nightly"
|
||||
VERSION="$(date +%Y-%m-%d)"
|
||||
VERSION="$(date +%Y-%m-%d)-${{ github.run_number }}"
|
||||
echo "is_prerelease=true" >> "$GITHUB_OUTPUT"
|
||||
echo "is_draft=false" >> "$GITHUB_OUTPUT"
|
||||
echo "release_name=Nightly ${VERSION}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
23
AGENTS.md
@ -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.
|
||||
|
||||
## Real app entrypoints
|
||||
- Desktop/JVM: `composeApp/src/jvmMain/kotlin/dev/krtirtho/spotube/main.kt` (`mainClass = dev.krtirtho.spotube.MainKt`).
|
||||
- Android: `composeApp/src/androidMain/kotlin/dev/krtirtho/spotube/MainActivity.kt`.
|
||||
- iOS bridge: `composeApp/src/iosMain/kotlin/dev/krtirtho/spotube/MainViewController.kt` and `iosApp/iosApp/ContentView.swift`.
|
||||
- Desktop/JVM: `composeApp/src/jvmMain/kotlin/org/spotube/spotube/main.kt` (`mainClass = org.spotube.spotube.MainKt`).
|
||||
- Android: `composeApp/src/androidMain/kotlin/org/spotube/spotube/MainActivity.kt`.
|
||||
- iOS bridge: `composeApp/src/iosMain/kotlin/org/spotube/spotube/MainViewController.kt` and `iosApp/iosApp/ContentView.swift`.
|
||||
|
||||
## High-value commands
|
||||
- Use Gradle wrapper (`./gradlew` or `./gradlew.bat`) only.
|
||||
- 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`
|
||||
- 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.
|
||||
@ -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`).
|
||||
- **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
|
||||
- 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_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`:
|
||||
- `kdroidFilter/ComposeNativeWebview` (compose-webview)
|
||||
- `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.
|
||||
- **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.
|
||||
@ -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].
|
||||
|
||||
### 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].
|
||||
- **Live Preview Iteration:** Use `compose-hot-reload` to evaluate UI layout changes headlessly without forcing full Gradle rebuilds.
|
||||
|
||||
1478
CHANGELOG.md
@ -30,19 +30,19 @@ All types of contributions are encouraged and valued. See the [Table of Contents
|
||||
## Code of Conduct
|
||||
|
||||
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
|
||||
to krtirtho@gmail.com.
|
||||
|
||||
## 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:
|
||||
|
||||
- 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 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.
|
||||
|
||||
- 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)).
|
||||
- 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).
|
||||
- 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/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.
|
||||
- Collect information about the bug:
|
||||
- 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:
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
@ -97,15 +97,15 @@ This section guides you through submitting an enhancement suggestion for Spotube
|
||||
#### Before Submitting an Enhancement
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
- 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/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
|
||||
|
||||
<!-- omit in toc -->
|
||||
|
||||
#### 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.
|
||||
- Provide a **step-by-step description of the suggested enhancement** in as many details as possible.
|
||||
|
||||
109
README.md
@ -4,7 +4,7 @@
|
||||
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!
|
||||
|
||||
<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://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 Spotube from the Official Website: https://spotube.cc
|
||||
Download Spotube from the Official Website: https://spotube.org
|
||||
|
||||
### 🔄 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
|
||||
|
||||
<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
|
||||
|
||||
- [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
|
||||
- [Piotr Rogowski](https://github.com/karniv00l) - The MacOS Developer
|
||||
- [Rusty Apple](https://github.com/RustyApple) - The Mysterious Unknown Guy
|
||||
|
||||
@ -27,7 +27,7 @@ render_always = true
|
||||
# An array of regex based postprocessors to modify the changelog.
|
||||
postprocessors = [
|
||||
# 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_always = true
|
||||
|
||||
@ -14,4 +14,9 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[lib]
|
||||
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
|
||||
|
||||
@ -16,7 +16,9 @@
|
||||
*/
|
||||
|
||||
import gobley.gradle.GobleyHost
|
||||
import gobley.gradle.Variant
|
||||
import gobley.gradle.cargo.dsl.jvm
|
||||
import dev.nucleusframework.desktop.application.dsl.NativeImageOptimization
|
||||
import dev.nucleusframework.desktop.application.dsl.TargetFormat
|
||||
import org.jetbrains.compose.reload.gradle.ComposeHotRun
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
@ -40,22 +42,45 @@ plugins {
|
||||
kotlin("plugin.atomicfu") version libs.versions.kotlin
|
||||
}
|
||||
|
||||
val bundleId = "dev.krtirtho.spotube"
|
||||
val bundleId = "org.spotube.spotube"
|
||||
val version = "6.0.0"
|
||||
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 homePageUrl = "https://spotube.cc"
|
||||
val homePageUrl = "https://spotube.org"
|
||||
val repositoryUrl = "https://github.com/team-spotube/spotube"
|
||||
val appVersionName = version
|
||||
val appBuildNumber = buildNumber
|
||||
val appBuildNumber = if (isNightly) nightlyBuildNumber else buildNumber
|
||||
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 latestReleaseApiUrl = "https://api.github.com/repos/$githubRepositoryPath/releases/latest"
|
||||
val generatedAppInfoDir = layout.buildDirectory.dir("generated/appBuildInfo/commonMain/kotlin")
|
||||
val generateAppBuildInfo by tasks.registering(Copy::class) {
|
||||
description = "Generate AppBuildInfo.kt"
|
||||
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")
|
||||
expand(
|
||||
mapOf(
|
||||
@ -287,7 +312,7 @@ kotlin {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.krtirtho.spotube"
|
||||
namespace = "org.spotube.spotube"
|
||||
compileSdk = libs.versions.android.compileSdk.get().toInt()
|
||||
|
||||
defaultConfig {
|
||||
@ -297,6 +322,19 @@ android {
|
||||
versionCode = buildNumber
|
||||
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 {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
@ -350,7 +388,7 @@ dependencies {
|
||||
}
|
||||
|
||||
nucleus.application {
|
||||
mainClass = "dev.krtirtho.spotube.MainKt"
|
||||
mainClass = "org.spotube.spotube.MainKt"
|
||||
|
||||
jvmArgs += listOf(
|
||||
"--enable-native-access=ALL-UNNAMED", // need for JNA for compose-webview (wry) to work
|
||||
@ -382,11 +420,13 @@ nucleus.application {
|
||||
homepage = homePageUrl
|
||||
|
||||
linux {
|
||||
iconFile.set(project.file("$desktopIconDir/icon.png"))
|
||||
modules("jdk.security.auth")
|
||||
debMaintainer = "Team Spotube <team.spotube@proton.me>"
|
||||
rpmLicenseType = "AGPL-3.0-or-later"
|
||||
}
|
||||
windows {
|
||||
iconFile.set(project.file("$desktopIconDir/icon.ico"))
|
||||
shortcut = true
|
||||
menu = true
|
||||
dirChooser = true
|
||||
@ -394,6 +434,7 @@ nucleus.application {
|
||||
}
|
||||
|
||||
macOS {
|
||||
iconFile.set(project.file("$desktopIconDir/icon.icns"))
|
||||
notarization {
|
||||
|
||||
}
|
||||
@ -403,6 +444,84 @@ nucleus.application {
|
||||
buildTypes.release.proguard {
|
||||
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
|
||||
@ -418,6 +537,8 @@ tasks.withType<ComposeHotRun>().configureEach {
|
||||
}
|
||||
|
||||
cargo {
|
||||
// Gobley defaults desktop to the debug profile, which ships an unstripped ~31 MB library
|
||||
if (isProductionBuild) jvmVariant = Variant.Release
|
||||
builds.jvm {
|
||||
embedRustLibrary = (rustTarget == GobleyHost.current.rustTarget)
|
||||
}
|
||||
|
||||
1332
composeApp/graalvm/reachability-metadata.json
Normal file
BIN
composeApp/icons/nightly/icon.icns
Normal file
BIN
composeApp/icons/nightly/icon.ico
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
composeApp/icons/nightly/icon.png
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
composeApp/icons/stable/icon.icns
Normal file
BIN
composeApp/icons/stable/icon.ico
Normal file
|
After Width: | Height: | Size: 364 KiB |
BIN
composeApp/icons/stable/icon.png
Normal file
|
After Width: | Height: | Size: 114 KiB |
@ -15,14 +15,14 @@
|
||||
* 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.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
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.runBlocking
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -15,10 +15,10 @@
|
||||
* 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 dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.AudioStream
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import io.ktor.http.Url
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
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.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 suspend fun searchVideos(query: String): List<VideoSearchResult> {
|
||||
actual suspend fun searchVideos(query: String, kind: SearchKind): List<VideoSearchResult> {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val extractor = YouTube.getSearchExtractor(
|
||||
query,
|
||||
mutableListOf(YoutubeSearchQueryHandlerFactory.MUSIC_SONGS),
|
||||
mutableListOf(
|
||||
when (kind) {
|
||||
SearchKind.Songs -> YoutubeSearchQueryHandlerFactory.MUSIC_SONGS
|
||||
SearchKind.Videos -> YoutubeSearchQueryHandlerFactory.VIDEOS
|
||||
}),
|
||||
""
|
||||
)
|
||||
extractor.fetchPage()
|
||||
@ -45,7 +52,7 @@ actual class NewPipeService {
|
||||
title = item.name,
|
||||
url = item.url,
|
||||
uploader = item.uploaderName,
|
||||
durationMs = item.duration,
|
||||
durationMs = secondsToMs(item.duration),
|
||||
thumbnailUrl = item.thumbnails.first().url)
|
||||
} else {
|
||||
null
|
||||
@ -101,7 +108,7 @@ actual class NewPipeService {
|
||||
title = extractor.name,
|
||||
url = extractor.url,
|
||||
uploader = extractor.uploaderName,
|
||||
durationMs = extractor.length,
|
||||
durationMs = secondsToMs(extractor.length),
|
||||
thumbnailUrl = extractor.thumbnails.first().url,
|
||||
// fallback to video streams if no audio streams are available
|
||||
audioStreams = audioStreams.ifEmpty {
|
||||
BIN
composeApp/src/androidMain/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 64 KiB |
@ -15,7 +15,7 @@
|
||||
* 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.os.Build
|
||||
@ -23,10 +23,10 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import dev.krtirtho.spotube.core.deeplink.ExternalUriHandler
|
||||
import dev.krtirtho.spotube.core.newpipe.NewPipeDownloader
|
||||
import dev.krtirtho.spotube.core.paths.Paths
|
||||
import dev.krtirtho.spotube.media.PlaybackService
|
||||
import org.spotube.spotube.core.deeplink.ExternalUriHandler
|
||||
import org.spotube.spotube.core.newpipe.NewPipeDownloader
|
||||
import org.spotube.spotube.core.paths.Paths
|
||||
import org.spotube.spotube.media.PlaybackService
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import io.github.vinceglb.filekit.dialogs.init
|
||||
|
||||
@ -15,11 +15,11 @@
|
||||
* 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 dev.krtirtho.spotube.core.di.initKoin
|
||||
import dev.krtirtho.spotube.core.paths.Paths
|
||||
import org.spotube.spotube.core.di.initKoin
|
||||
import org.spotube.spotube.core.paths.Paths
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.Intent
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.Intent
|
||||
@ -27,7 +27,7 @@ import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.PlaybackException
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import dev.krtirtho.spotube.media.PlaybackService
|
||||
import org.spotube.spotube.media.PlaybackService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -84,6 +84,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
private var disposed = false
|
||||
|
||||
private val currentPlaylist = mutableListOf<MediaItem>()
|
||||
private val originalPlaylist = mutableListOf<MediaItem>()
|
||||
private var shuffleEnabled = false
|
||||
private val urlIndexMap = mutableMapOf<String, Int>()
|
||||
|
||||
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) {
|
||||
_volume.tryEmit(volume)
|
||||
}
|
||||
@ -260,8 +258,37 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
|
||||
actual override suspend fun shuffle(enabled: Boolean) {
|
||||
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)
|
||||
_playlist.tryEmit(currentPlaylist.toList())
|
||||
syncCurrentItem()
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,6 +300,10 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
withContext(Dispatchers.Main) {
|
||||
currentPlaylist.clear()
|
||||
currentPlaylist.addAll(playlist)
|
||||
originalPlaylist.clear()
|
||||
originalPlaylist.addAll(playlist)
|
||||
shuffleEnabled = false
|
||||
_shuffleMode.tryEmit(false)
|
||||
urlIndexMap.clear()
|
||||
playlist.forEachIndexed { index, item ->
|
||||
urlIndexMap[item.url] = index
|
||||
@ -305,6 +336,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
actual override suspend fun addMediaItem(mediaItem: MediaItem) {
|
||||
withContext(Dispatchers.Main) {
|
||||
currentPlaylist.add(mediaItem)
|
||||
originalPlaylist.add(mediaItem)
|
||||
urlIndexMap[mediaItem.url] = currentPlaylist.lastIndex
|
||||
_playlist.tryEmit(currentPlaylist.toList())
|
||||
exoPlayer.addMediaItem(mediaItem.toExoMediaItem())
|
||||
@ -317,6 +349,8 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
val insertIndex = (currentIndex + 1).coerceAtMost(currentPlaylist.size)
|
||||
|
||||
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()
|
||||
currentPlaylist.forEachIndexed { index, item ->
|
||||
urlIndexMap[item.url] = index
|
||||
@ -331,6 +365,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
withContext(Dispatchers.Main) {
|
||||
val index = urlIndexMap[mediaItem.url] ?: return@withContext
|
||||
currentPlaylist.removeAt(index)
|
||||
originalPlaylist.removeAll { it.url == mediaItem.url }
|
||||
urlIndexMap.clear()
|
||||
currentPlaylist.forEachIndexed { i, item ->
|
||||
urlIndexMap[item.url] = i
|
||||
@ -346,6 +381,9 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
|
||||
val item = currentPlaylist.removeAt(fromIndex)
|
||||
currentPlaylist.add(toIndex, item)
|
||||
if (!shuffleEnabled && fromIndex in originalPlaylist.indices && toIndex in originalPlaylist.indices) {
|
||||
originalPlaylist.add(toIndex, originalPlaylist.removeAt(fromIndex))
|
||||
}
|
||||
urlIndexMap.clear()
|
||||
currentPlaylist.forEachIndexed { i, it ->
|
||||
urlIndexMap[it.url] = i
|
||||
@ -398,6 +436,7 @@ actual class AudioPlayer actual constructor(context: Any) : AudioPlayerInterface
|
||||
disposed = true
|
||||
exoPlayer.release()
|
||||
currentPlaylist.clear()
|
||||
originalPlaylist.clear()
|
||||
urlIndexMap.clear()
|
||||
|
||||
_playerState.tryEmit(PlayerState.IDLE)
|
||||
@ -15,17 +15,17 @@
|
||||
* 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 dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import dev.krtirtho.spotube.core.paths.Paths
|
||||
import dev.krtirtho.spotube.core.share.AndroidShareService
|
||||
import dev.krtirtho.spotube.core.share.ShareService
|
||||
import dev.krtirtho.spotube.media.MediaBrowseHelper
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.AndroidLocalMediaDiscoveryService
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaDiscoveryService
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayer
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import org.spotube.spotube.core.paths.Paths
|
||||
import org.spotube.spotube.core.share.AndroidShareService
|
||||
import org.spotube.spotube.core.share.ShareService
|
||||
import org.spotube.spotube.media.MediaBrowseHelper
|
||||
import org.spotube.spotube.modules.library.local_tracks.media.AndroidLocalMediaDiscoveryService
|
||||
import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaDiscoveryService
|
||||
import org.koin.dsl.module
|
||||
|
||||
actual val platformModules = module {
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.content.pm.PackageManager
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.Resources
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.os.Environment
|
||||
@ -15,11 +15,11 @@
|
||||
* 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 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 {
|
||||
override fun share(url: String, title: String) {
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.WebStorage
|
||||
@ -15,7 +15,7 @@
|
||||
* 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 java.util.concurrent.Executors
|
||||
@ -15,31 +15,31 @@
|
||||
* 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.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseItem
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchResult
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||
import dev.krtirtho.spotube.modules.album.AlbumRepository
|
||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreenRepository
|
||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||
import dev.krtirtho.spotube.modules.playlist.PlaylistRepository
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||
import dev.krtirtho.spotube.modules.search.SearchRepository
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.album.MetadataAlbum
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.browse.MetadataBrowseItem
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.playlist.MetadataPlaylist
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.search.MetadataSearchResult
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||
import org.spotube.spotube.core.playback.CollectionPlaybackHelper
|
||||
import org.spotube.spotube.modules.album.AlbumRepository
|
||||
import org.spotube.spotube.modules.blacklist.BlacklistRepository
|
||||
import org.spotube.spotube.modules.home.HomeScreenRepository
|
||||
import org.spotube.spotube.modules.library.LibraryRepository
|
||||
import org.spotube.spotube.modules.playlist.PlaylistRepository
|
||||
import org.spotube.spotube.modules.plugin.PluginManager
|
||||
import org.spotube.spotube.modules.saved_tracks.SavedTracksRepository
|
||||
import org.spotube.spotube.modules.search.SearchRepository
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.jetbrains.compose.resources.getString
|
||||
import spotube.composeapp.generated.resources.Res
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.app.NotificationChannel
|
||||
@ -29,6 +29,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.MediaItem as Media3MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.DefaultMediaNotificationProvider
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
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.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
import dev.krtirtho.spotube.MainActivity
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayer
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import org.spotube.spotube.MainActivity
|
||||
import org.spotube.spotube.R
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayer
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -75,6 +77,16 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
|
||||
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(
|
||||
this,
|
||||
0,
|
||||
@ -87,7 +99,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle("Spotube")
|
||||
.setContentText(runBlocking { getString(Res.string.notification_playback_service_text) })
|
||||
.setSmallIcon(android.R.drawable.ic_media_play)
|
||||
.setSmallIcon(R.drawable.ic_stat_spotube)
|
||||
.setContentIntent(sessionActivity)
|
||||
.setOngoing(true)
|
||||
.build()
|
||||
@ -371,20 +383,20 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
||||
when {
|
||||
mediaId.startsWith("${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)
|
||||
return
|
||||
}
|
||||
|
||||
mediaId.startsWith("${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)
|
||||
return
|
||||
}
|
||||
|
||||
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()
|
||||
return
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.content.Context
|
||||
@ -26,7 +26,7 @@ import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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 java.io.File
|
||||
import java.util.Locale
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.content.pm.PackageManager
|
||||
BIN
composeApp/src/androidMain/res/drawable-hdpi/ic_stat_spotube.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
composeApp/src/androidMain/res/drawable-mdpi/ic_stat_spotube.png
Normal file
|
After Width: | Height: | Size: 737 B |
@ -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>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.8 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
@ -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>
|
||||
@ -17,6 +17,7 @@
|
||||
-->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
-->
|
||||
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
</adaptive-icon>
|
||||
<background android:drawable="@color/ic_launcher_background" />
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_monochrome" />
|
||||
</adaptive-icon>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.3 KiB |
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 7.3 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 36 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 23 KiB |
22
composeApp/src/androidMain/res/values-sv/strings.xml
Normal 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>
|
||||
21
composeApp/src/androidMain/res/values/colors.xml
Normal 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>
|
||||
169
composeApp/src/commonMain/composeResources/values-sv/strings.xml
Normal 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>
|
||||
@ -907,6 +907,7 @@
|
||||
<string name="plugin_discover_badge_verified">Verified</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_host">github.com</string>
|
||||
|
||||
<!-- Plugin release install dialog -->
|
||||
<string name="plugin_install_no_releases">No releases found</string>
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,2 +0,0 @@
|
||||
package dev.krtirtho.spotube.modules.blacklist
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
package dev.krtirtho.spotube.resources.iconsax
|
||||
|
||||
object Iconsax
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.SharedTransitionLayout
|
||||
@ -33,42 +33,42 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import dev.krtirtho.spotube.core.DebugConfig
|
||||
import dev.krtirtho.spotube.core.i18n.AppLocaleProvider
|
||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.navigation.TOP_LEVEL_ROUTES
|
||||
import dev.krtirtho.spotube.core.navigation.navPopTransitionSpec
|
||||
import dev.krtirtho.spotube.core.navigation.navPredictivePopTransitionSpec
|
||||
import dev.krtirtho.spotube.core.navigation.navPushTransitionSpec
|
||||
import dev.krtirtho.spotube.core.navigation.rememberNavigationState
|
||||
import dev.krtirtho.spotube.core.navigation.toEntries
|
||||
import dev.krtirtho.spotube.core.ui.component.LocalSharedTransitionScope
|
||||
import dev.krtirtho.spotube.core.ui.theming.SpotubeTheme
|
||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettings
|
||||
import dev.krtirtho.spotube.modules.shell.AppShell
|
||||
import dev.krtirtho.spotube.modules.support.LocalSupportPrompt
|
||||
import dev.krtirtho.spotube.modules.support.SupportDialog
|
||||
import dev.krtirtho.spotube.modules.support.SupportReminderIntervalMillis
|
||||
import dev.krtirtho.spotube.modules.update.AppUpdateDialog
|
||||
import dev.krtirtho.spotube.modules.update.AppUpdateService
|
||||
import dev.krtirtho.spotube.modules.update.PluginUpdateDialog
|
||||
import dev.krtirtho.spotube.modules.update.PluginUpdateService
|
||||
import dev.krtirtho.spotube.modules.webview.WebViewScreen
|
||||
import dev.krtirtho.spotube.modules.welcome.WelcomeRepository
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxCd
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxHome
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMirrorScreenRegular
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicDashboard
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicLibrary
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearch
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSetting2
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSoundTwotone
|
||||
import dev.krtirtho.spotube.resources.iconsax.User
|
||||
import org.spotube.spotube.core.DebugConfig
|
||||
import org.spotube.spotube.core.i18n.AppLocaleProvider
|
||||
import org.spotube.spotube.core.navigation.Navigator
|
||||
import org.spotube.spotube.core.navigation.Routes
|
||||
import org.spotube.spotube.core.navigation.TOP_LEVEL_ROUTES
|
||||
import org.spotube.spotube.core.navigation.navPopTransitionSpec
|
||||
import org.spotube.spotube.core.navigation.navPredictivePopTransitionSpec
|
||||
import org.spotube.spotube.core.navigation.navPushTransitionSpec
|
||||
import org.spotube.spotube.core.navigation.rememberNavigationState
|
||||
import org.spotube.spotube.core.navigation.toEntries
|
||||
import org.spotube.spotube.core.ui.component.LocalSharedTransitionScope
|
||||
import org.spotube.spotube.core.ui.theming.SpotubeTheme
|
||||
import org.spotube.spotube.modules.library.LibraryTab
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import org.spotube.spotube.modules.settings.UserSettings
|
||||
import org.spotube.spotube.modules.shell.AppShell
|
||||
import org.spotube.spotube.modules.support.LocalSupportPrompt
|
||||
import org.spotube.spotube.modules.support.SupportDialog
|
||||
import org.spotube.spotube.modules.support.SupportReminderIntervalMillis
|
||||
import org.spotube.spotube.modules.update.AppUpdateDialog
|
||||
import org.spotube.spotube.modules.update.AppUpdateService
|
||||
import org.spotube.spotube.modules.update.PluginUpdateDialog
|
||||
import org.spotube.spotube.modules.update.PluginUpdateService
|
||||
import org.spotube.spotube.modules.webview.WebViewScreen
|
||||
import org.spotube.spotube.modules.welcome.WelcomeRepository
|
||||
import org.spotube.spotube.resources.iconsax.Iconsax
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxCd
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxDirectboxReceive
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxHome
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxMirrorScreenRegular
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxMusicDashboard
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxMusicLibrary
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxSearch
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxSetting2
|
||||
import org.spotube.spotube.resources.iconsax.IconsaxSoundTwotone
|
||||
import org.spotube.spotube.resources.iconsax.User
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlin.time.Clock
|
||||
import org.koin.compose.koinInject
|
||||
@ -253,7 +253,7 @@ fun App(
|
||||
onDismiss = appUpdateService::dismissUpdate,
|
||||
onIgnore = appUpdateService::ignoreUpdate,
|
||||
onUpdate = {
|
||||
runCatching { openUrlInBrowser("https://spotube.cc/downloads/") }
|
||||
runCatching { openUrlInBrowser("https://spotube.org/downloads/") }
|
||||
appUpdateService.dismissUpdate()
|
||||
})
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package dev.krtirtho.spotube
|
||||
package org.spotube.spotube
|
||||
|
||||
enum class PlatformType {
|
||||
Android, IOS, Windows, Linux, MacOS, Unknown
|
||||
@ -15,10 +15,10 @@
|
||||
* 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.
|
||||
*/
|
||||
object DebugConfig {
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.StateFlow
|
||||
@ -15,10 +15,10 @@
|
||||
* 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 dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlin.time.Duration
|
||||
@ -15,7 +15,7 @@
|
||||
* 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
|
||||
|
||||
@ -15,11 +15,11 @@
|
||||
* 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 dev.krtirtho.spotube.core.db.Database
|
||||
import dev.krtirtho.spotube.core.db.DatabaseKeys
|
||||
import org.spotube.spotube.core.db.Database
|
||||
import org.spotube.spotube.core.db.DatabaseKeys
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -15,13 +15,13 @@
|
||||
* 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 dev.krtirtho.spotube.core.di.injectLogger
|
||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginProvider
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
import org.spotube.spotube.modules.blacklist.BlacklistRepository
|
||||
import org.spotube.spotube.modules.plugin.PluginProvider
|
||||
import org.spotube.spotube.modules.settings.SettingsProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
@ -15,14 +15,14 @@
|
||||
* 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.preferences.core.PreferenceDataStoreFactory
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.longPreferencesKey
|
||||
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.Path.Companion.toPath
|
||||
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 {
|
||||
/** Preference files written by previous launches; their presence means an upgrade. */
|
||||
val EXISTING_INSTALL_PROBE_FILES = listOf(
|
||||
@ -95,4 +101,5 @@ object DatabaseKeys {
|
||||
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_LAST_SCAN_AT_KEY = longPreferencesKey("local_media_last_scan_at")
|
||||
val WINDOW_STATE_KEY = stringPreferencesKey("window_state")
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
* 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
|
||||
@ -15,7 +15,7 @@
|
||||
* 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 co.touchlab.kermit.Logger
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.koin.KermitKoinLogger
|
||||
@ -15,82 +15,82 @@
|
||||
* 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 dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueueRepository
|
||||
import dev.krtirtho.spotube.core.audioplayer.DeviceAudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueStateRepository
|
||||
import dev.krtirtho.spotube.core.db.Database
|
||||
import dev.krtirtho.spotube.core.discovery.DeviceDiscoveryService
|
||||
import dev.krtirtho.spotube.core.discord.DiscordRpcService
|
||||
import dev.krtirtho.spotube.core.jam.JamRoomClient
|
||||
import dev.krtirtho.spotube.core.jam.JamRoomService
|
||||
import dev.krtirtho.spotube.core.navigation.navigationModule
|
||||
import dev.krtirtho.spotube.core.remote.RemoteControlClient
|
||||
import dev.krtirtho.spotube.core.remote.RemoteControlHandler
|
||||
import dev.krtirtho.spotube.core.remote.RemoteControlService
|
||||
import dev.krtirtho.spotube.core.remote.RemotePlaybackController
|
||||
import dev.krtirtho.spotube.core.scrobble.ScrobbleManager
|
||||
import dev.krtirtho.spotube.core.playback.CollectionPlaybackHelper
|
||||
import dev.krtirtho.spotube.core.server.AlternativeTracksRepository
|
||||
import dev.krtirtho.spotube.core.server.CacheManager
|
||||
import dev.krtirtho.spotube.core.server.LocalServer
|
||||
import dev.krtirtho.spotube.core.server.MatchedTracksRepository
|
||||
import dev.krtirtho.spotube.core.server.TrackSourceRepository
|
||||
import dev.krtirtho.spotube.core.server.StreamingUrlRepository
|
||||
import dev.krtirtho.spotube.core.webview.WebViewController
|
||||
import dev.krtirtho.spotube.modules.album.AlbumRepository
|
||||
import dev.krtirtho.spotube.modules.album.AlbumViewModel
|
||||
import dev.krtirtho.spotube.modules.artist.ArtistRepository
|
||||
import dev.krtirtho.spotube.modules.artist.ArtistViewModel
|
||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistRepository
|
||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistViewModel
|
||||
import dev.krtirtho.spotube.modules.devices.DevicesViewModel
|
||||
import dev.krtirtho.spotube.modules.devices.RemoteControlViewModel
|
||||
import dev.krtirtho.spotube.modules.jam.JamViewModel
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadManager
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreenRepository
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreenViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.MetadataAuthViewModel
|
||||
import dev.krtirtho.spotube.modules.library.LibraryRepository
|
||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||
import dev.krtirtho.spotube.modules.library.album.LibraryAlbumsViewModel
|
||||
import dev.krtirtho.spotube.modules.library.artist.LibraryArtistsViewModel
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.LibraryLocalTracksViewModel
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaCacheRepository
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaFoldersConfig
|
||||
import dev.krtirtho.spotube.modules.library.local_tracks.media.LocalMediaLibraryCoordinator
|
||||
import dev.krtirtho.spotube.modules.library.playlist.LibraryPlaylistsViewModel
|
||||
import dev.krtirtho.spotube.modules.lyrics.LyricsViewModel
|
||||
import dev.krtirtho.spotube.modules.playlist.PlaylistRepository
|
||||
import dev.krtirtho.spotube.modules.playlist.PlaylistViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.AudioPluginSource
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginManager
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginProvider
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuardViewModel
|
||||
import dev.krtirtho.spotube.modules.plugin.ScrobblePluginSource
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksRepository
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import dev.krtirtho.spotube.modules.search.SearchRepository
|
||||
import dev.krtirtho.spotube.modules.search.SearchScreenViewModel
|
||||
import dev.krtirtho.spotube.modules.update.AppUpdateService
|
||||
import dev.krtirtho.spotube.modules.update.PluginUpdateService
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||
import dev.krtirtho.spotube.modules.settings.UserSettingsSource
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.JamSettingsViewModel
|
||||
import dev.krtirtho.spotube.modules.settings.LocalMediaFoldersViewModel
|
||||
import dev.krtirtho.spotube.modules.shell.AppShellViewModel
|
||||
import dev.krtirtho.spotube.modules.shell.PlayerOptionsViewModel
|
||||
import dev.krtirtho.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel
|
||||
import dev.krtirtho.spotube.modules.shell.player_queue.PlayerQueueContentViewModel
|
||||
import dev.krtirtho.spotube.modules.welcome.WelcomeRepository
|
||||
import dev.krtirtho.spotube.modules.welcome.WelcomeViewModel
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueueRepository
|
||||
import org.spotube.spotube.core.audioplayer.DeviceAudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.QueueStateRepository
|
||||
import org.spotube.spotube.core.db.Database
|
||||
import org.spotube.spotube.core.discovery.DeviceDiscoveryService
|
||||
import org.spotube.spotube.core.discord.DiscordRpcService
|
||||
import org.spotube.spotube.core.jam.JamRoomClient
|
||||
import org.spotube.spotube.core.jam.JamRoomService
|
||||
import org.spotube.spotube.core.navigation.navigationModule
|
||||
import org.spotube.spotube.core.remote.RemoteControlClient
|
||||
import org.spotube.spotube.core.remote.RemoteControlHandler
|
||||
import org.spotube.spotube.core.remote.RemoteControlService
|
||||
import org.spotube.spotube.core.remote.RemotePlaybackController
|
||||
import org.spotube.spotube.core.scrobble.ScrobbleManager
|
||||
import org.spotube.spotube.core.playback.CollectionPlaybackHelper
|
||||
import org.spotube.spotube.core.server.AlternativeTracksRepository
|
||||
import org.spotube.spotube.core.server.CacheManager
|
||||
import org.spotube.spotube.core.server.LocalServer
|
||||
import org.spotube.spotube.core.server.MatchedTracksRepository
|
||||
import org.spotube.spotube.core.server.TrackSourceRepository
|
||||
import org.spotube.spotube.core.server.StreamingUrlRepository
|
||||
import org.spotube.spotube.core.webview.WebViewController
|
||||
import org.spotube.spotube.modules.album.AlbumRepository
|
||||
import org.spotube.spotube.modules.album.AlbumViewModel
|
||||
import org.spotube.spotube.modules.artist.ArtistRepository
|
||||
import org.spotube.spotube.modules.artist.ArtistViewModel
|
||||
import org.spotube.spotube.modules.blacklist.BlacklistRepository
|
||||
import org.spotube.spotube.modules.blacklist.BlacklistViewModel
|
||||
import org.spotube.spotube.modules.devices.DevicesViewModel
|
||||
import org.spotube.spotube.modules.devices.RemoteControlViewModel
|
||||
import org.spotube.spotube.modules.jam.JamViewModel
|
||||
import org.spotube.spotube.modules.downloads.DownloadManager
|
||||
import org.spotube.spotube.modules.downloads.DownloadsViewModel
|
||||
import org.spotube.spotube.modules.home.HomeScreenRepository
|
||||
import org.spotube.spotube.modules.home.HomeScreenViewModel
|
||||
import org.spotube.spotube.modules.plugin.MetadataAuthViewModel
|
||||
import org.spotube.spotube.modules.library.LibraryRepository
|
||||
import org.spotube.spotube.modules.library.LibraryState
|
||||
import org.spotube.spotube.modules.library.album.LibraryAlbumsViewModel
|
||||
import org.spotube.spotube.modules.library.artist.LibraryArtistsViewModel
|
||||
import org.spotube.spotube.modules.library.local_tracks.LibraryLocalTracksViewModel
|
||||
import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaCacheRepository
|
||||
import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaFoldersConfig
|
||||
import org.spotube.spotube.modules.library.local_tracks.media.LocalMediaLibraryCoordinator
|
||||
import org.spotube.spotube.modules.library.playlist.LibraryPlaylistsViewModel
|
||||
import org.spotube.spotube.modules.lyrics.LyricsViewModel
|
||||
import org.spotube.spotube.modules.playlist.PlaylistRepository
|
||||
import org.spotube.spotube.modules.playlist.PlaylistViewModel
|
||||
import org.spotube.spotube.modules.plugin.AudioPluginSource
|
||||
import org.spotube.spotube.modules.plugin.PluginManager
|
||||
import org.spotube.spotube.modules.plugin.PluginProvider
|
||||
import org.spotube.spotube.modules.plugin.PluginViewModel
|
||||
import org.spotube.spotube.modules.plugin.guard.MetadataPluginGuardViewModel
|
||||
import org.spotube.spotube.modules.plugin.ScrobblePluginSource
|
||||
import org.spotube.spotube.modules.saved_tracks.SavedTracksRepository
|
||||
import org.spotube.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import org.spotube.spotube.modules.search.SearchRepository
|
||||
import org.spotube.spotube.modules.search.SearchScreenViewModel
|
||||
import org.spotube.spotube.modules.update.AppUpdateService
|
||||
import org.spotube.spotube.modules.update.PluginUpdateService
|
||||
import org.spotube.spotube.modules.settings.SettingsProvider
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import org.spotube.spotube.modules.settings.UserSettingsSource
|
||||
import org.spotube.spotube.modules.settings.SettingsViewModel
|
||||
import org.spotube.spotube.modules.settings.JamSettingsViewModel
|
||||
import org.spotube.spotube.modules.settings.LocalMediaFoldersViewModel
|
||||
import org.spotube.spotube.modules.shell.AppShellViewModel
|
||||
import org.spotube.spotube.modules.shell.PlayerOptionsViewModel
|
||||
import org.spotube.spotube.modules.shell.alternative_track.AlternativeTrackContentViewModel
|
||||
import org.spotube.spotube.modules.shell.player_queue.PlayerQueueContentViewModel
|
||||
import org.spotube.spotube.modules.welcome.WelcomeRepository
|
||||
import org.spotube.spotube.modules.welcome.WelcomeViewModel
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.bind
|
||||
import org.koin.core.module.dsl.createdAtStart
|
||||
@ -15,12 +15,12 @@
|
||||
* 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 dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||
import dev.krtirtho.spotube.core.di.injectLogger
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsProvider
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import org.spotube.spotube.core.audioplayer.PlayerState
|
||||
import org.spotube.spotube.core.di.injectLogger
|
||||
import org.spotube.spotube.modules.settings.SettingsProvider
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.IO
|
||||
@ -202,7 +202,7 @@ class DiscordRpcService(
|
||||
private data class DiscordRpcState(
|
||||
val enabled: Boolean,
|
||||
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 duration: kotlin.time.Duration)
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.NetService
|
||||
@ -15,7 +15,7 @@
|
||||
* 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
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
* 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 {
|
||||
return this.split("-").joinToString(" ") { it.replaceFirstChar { char -> char.uppercase() } }
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.CompositionLocalProvider
|
||||
@ -23,7 +23,7 @@ import androidx.compose.runtime.ProvidedValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
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],
|
||||
@ -15,11 +15,11 @@
|
||||
* 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 dev.krtirtho.spotube.core.audioplayer.MediaItem
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.core.audioplayer.MediaItem
|
||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@ -168,7 +168,7 @@ data class JamMediaItem(
|
||||
duration = kotlin.time.Duration.parse("${item.durationMs}ms"),
|
||||
coverURL = item.coverUrl,
|
||||
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" }))
|
||||
}
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
* 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 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.WillConfig
|
||||
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.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
@ -15,7 +15,7 @@
|
||||
* 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
|
||||
|
||||
@ -15,18 +15,18 @@
|
||||
* 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 dev.krtirtho.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||
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.audioplayer.AudioPlayerInterface
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.PlayerState
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsRepository
|
||||
import org.spotube.plugin_interfaces.plugin_apis.audio.StreamProtocol
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.artist.MetadataArtist
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
|
||||
import org.spotube.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerInterface
|
||||
import org.spotube.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import org.spotube.spotube.core.audioplayer.PlayerState
|
||||
import org.spotube.spotube.core.audioplayer.QueueEntry
|
||||
import org.spotube.spotube.modules.settings.SettingsRepository
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@ -481,7 +481,7 @@ class JamRoomService(
|
||||
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 chars = "0123456789abcdef"
|
||||
repeat(6) { append(chars[Random.nextInt(chars.length)]) }
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.ContentTransform
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.flow.receiveAsFlow
|
||||
@ -15,27 +15,27 @@
|
||||
* 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 dev.krtirtho.spotube.modules.album.AlbumScreen
|
||||
import dev.krtirtho.spotube.modules.artist.ArtistScreen
|
||||
import dev.krtirtho.spotube.modules.blacklist.BlacklistScreen
|
||||
import dev.krtirtho.spotube.modules.devices.DevicesScreen
|
||||
import dev.krtirtho.spotube.modules.devices.RemoteControlScreen
|
||||
import dev.krtirtho.spotube.modules.jam.JamScreen
|
||||
import dev.krtirtho.spotube.modules.home.HomeScreen
|
||||
import dev.krtirtho.spotube.modules.library.LibraryScreen
|
||||
import dev.krtirtho.spotube.modules.lyrics.LyricsScreen
|
||||
import dev.krtirtho.spotube.modules.playlist.PlaylistScreen
|
||||
import dev.krtirtho.spotube.modules.plugin.PluginScreen
|
||||
import dev.krtirtho.spotube.modules.plugin.guard.MetadataPluginGuard
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksScreen
|
||||
import dev.krtirtho.spotube.modules.search.SearchScreen
|
||||
import dev.krtirtho.spotube.modules.settings.SettingsScreen
|
||||
import dev.krtirtho.spotube.modules.settings.JamSettingsScreen
|
||||
import dev.krtirtho.spotube.modules.welcome.WelcomeScreen
|
||||
import org.spotube.spotube.modules.album.AlbumScreen
|
||||
import org.spotube.spotube.modules.artist.ArtistScreen
|
||||
import org.spotube.spotube.modules.blacklist.BlacklistScreen
|
||||
import org.spotube.spotube.modules.devices.DevicesScreen
|
||||
import org.spotube.spotube.modules.devices.RemoteControlScreen
|
||||
import org.spotube.spotube.modules.jam.JamScreen
|
||||
import org.spotube.spotube.modules.home.HomeScreen
|
||||
import org.spotube.spotube.modules.library.LibraryScreen
|
||||
import org.spotube.spotube.modules.lyrics.LyricsScreen
|
||||
import org.spotube.spotube.modules.playlist.PlaylistScreen
|
||||
import org.spotube.spotube.modules.plugin.PluginScreen
|
||||
import org.spotube.spotube.modules.plugin.guard.MetadataPluginGuard
|
||||
import org.spotube.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
import org.spotube.spotube.modules.saved_tracks.SavedTracksScreen
|
||||
import org.spotube.spotube.modules.search.SearchScreen
|
||||
import org.spotube.spotube.modules.settings.SettingsScreen
|
||||
import org.spotube.spotube.modules.settings.JamSettingsScreen
|
||||
import org.spotube.spotube.modules.welcome.WelcomeScreen
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.koin.compose.viewmodel.koinViewModel
|
||||
import org.koin.core.annotation.KoinExperimentalAPI
|
||||
@ -15,7 +15,7 @@
|
||||
* 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.MutableState
|
||||
@ -34,7 +34,7 @@ import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
|
||||
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
|
||||
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.modules.SerializersModule
|
||||
import kotlinx.serialization.modules.polymorphic
|
||||
@ -15,9 +15,9 @@
|
||||
* 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
|
||||
|
||||
@Serializable
|
||||