mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-11-04 10:13:59 +00:00
Compare commits
9 Commits
def946c549
...
795b043508
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
795b043508 | ||
|
|
9d5854023b | ||
|
|
38e9bbc1d8 | ||
|
|
64fb23e62b | ||
|
|
64a78727ce | ||
|
|
046f724c9b | ||
|
|
e92e62613e | ||
|
|
7ee3c70c96 | ||
|
|
b0327fd224 |
36
.github/workflows/spotube-release-binary.yml
vendored
36
.github/workflows/spotube-release-binary.yml
vendored
@ -166,7 +166,7 @@ jobs:
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
@ -196,12 +196,20 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
# Only the AppImage bundles VLC, GTK and WebKitGTK; deb/rpm depend on the distro packages
|
||||
- name: Package Linux AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :composeApp:packageGraalvmAppImage -PappImage=true -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
- name: Rename Linux artifacts
|
||||
run: |
|
||||
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
|
||||
APPIMAGE_FILE=$(find composeApp/build/compose/binaries/main -name "*.AppImage" -type f | head -1)
|
||||
cp "$APPIMAGE_FILE" Spotube-linux-x86_64.AppImage
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v7
|
||||
@ -215,6 +223,12 @@ jobs:
|
||||
name: linux-rpm
|
||||
path: Spotube-linux-x86_64.rpm
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux-appimage
|
||||
path: Spotube-linux-x86_64.AppImage
|
||||
|
||||
- name: Setup tmate session on failure
|
||||
if: ${{ inputs.debug_ssh && failure() }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
@ -245,7 +259,7 @@ jobs:
|
||||
- name: Install packaging tools
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf
|
||||
sudo apt-get install -y rpm fuse build-essential zlib1g-dev patchelf zstd
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v6
|
||||
@ -275,12 +289,20 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
# Only the AppImage bundles VLC, GTK and WebKitGTK; deb/rpm depend on the distro packages
|
||||
- name: Package Linux AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: ./gradlew :composeApp:packageGraalvmAppImage -PappImage=true -Pnightly=${{ inputs.release_type == 'nightly' }} -PbuildNumber=${{ github.run_number }}
|
||||
|
||||
- name: Rename Linux artifacts
|
||||
run: |
|
||||
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
|
||||
APPIMAGE_FILE=$(find composeApp/build/compose/binaries/main -name "*.AppImage" -type f | head -1)
|
||||
cp "$APPIMAGE_FILE" Spotube-linux-aarch64.AppImage
|
||||
|
||||
- name: Upload DEB
|
||||
uses: actions/upload-artifact@v7
|
||||
@ -294,6 +316,12 @@ jobs:
|
||||
name: linux-rpm-arm64
|
||||
path: Spotube-linux-aarch64.rpm
|
||||
|
||||
- name: Upload AppImage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: linux-appimage-arm64
|
||||
path: Spotube-linux-aarch64.AppImage
|
||||
|
||||
- name: Setup tmate session on failure
|
||||
if: ${{ inputs.debug_ssh && failure() }}
|
||||
uses: mxschmitt/action-tmate@v3
|
||||
@ -479,7 +507,7 @@ jobs:
|
||||
run: |
|
||||
echo "## Downloads" > release-notes.md
|
||||
echo "" >> release-notes.md
|
||||
for dir in android-apk linux-deb linux-rpm linux-deb-arm64 linux-rpm-arm64 windows-exe macos-dmg; do
|
||||
for dir in android-apk linux-deb linux-rpm linux-appimage linux-deb-arm64 linux-rpm-arm64 linux-appimage-arm64 windows-exe macos-dmg; do
|
||||
if [ -d "$dir" ]; then
|
||||
echo "### $dir" >> release-notes.md
|
||||
for f in "$dir"/*; do
|
||||
@ -511,7 +539,9 @@ jobs:
|
||||
android-apk/*.apk
|
||||
linux-deb/*.deb
|
||||
linux-rpm/*.rpm
|
||||
linux-appimage/*.AppImage
|
||||
linux-deb-arm64/*.deb
|
||||
linux-rpm-arm64/*.rpm
|
||||
linux-appimage-arm64/*.AppImage
|
||||
windows-exe/*.exe
|
||||
macos-dmg/*.dmg
|
||||
|
||||
30
README.md
30
README.md
@ -56,7 +56,18 @@ Spotube is a Kotlin Multiplatform + Compose Multiplatform app. The desktop app i
|
||||
- **Android SDK**, only for Android builds.
|
||||
- **Unpublished dependency.** One dependency isn't on Maven Central yet. Clone it 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`)
|
||||
- **Linux runtime:** `webkit2gtk-4.1` for the embedded webview.
|
||||
- **Linux runtime libraries** (needed to run the app, including `:composeApp:run`). The `.deb`/`.rpm` install these as dependencies from your distro. For a source build, install them yourself, **including VLC**. Only the `.AppImage` is self-contained (it bundles VLC, GTK 3 and WebKitGTK, and uses just the host's glibc, graphics, X11 and sound libraries):
|
||||
|
||||
| Distro | Command |
|
||||
|---|---|
|
||||
| Debian/Ubuntu | `sudo apt-get install libvlc5 vlc-plugin-base libwebkit2gtk-4.1-0 libgtk-3-0 libasound2 libgl1 libxtst6 libxi6 libxrender1` |
|
||||
| Fedora | `sudo dnf install vlc-libs webkit2gtk4.1 gtk3 alsa-lib mesa-libGL libXtst libXi libXrender`. `vlc-libs` is only in [RPM Fusion](https://rpmfusion.org/Configuration), so enable it first. |
|
||||
| Arch / CachyOS / Manjaro | `sudo pacman -S vlc webkit2gtk-4.1 gtk3 alsa-lib libglvnd libxtst libxi libxrender` |
|
||||
| openSUSE | `sudo zypper install libvlc5 libwebkit2gtk-4_1-0 libgtk-3-0 libasound2 Mesa-libGL1 libXtst6 libXi6 libXrender1` (for wide codec support, switch to the [Packman](https://en.opensuse.org/Additional_package_repositories#Packman) repo's VLC) |
|
||||
|
||||
Arch-based distros and openSUSE have no native package here. Use the `.AppImage`. The `.rpm` is built with Fedora package names, so it won't resolve on openSUSE.
|
||||
|
||||
On Ubuntu 24.04+/Debian 13 `libgtk-3-0` and `libasound2` are named `libgtk-3-0t64` and `libasound2t64`; apt resolves this automatically.
|
||||
|
||||
Always use the Gradle wrapper (`./gradlew`, or `gradlew.bat` on Windows).
|
||||
|
||||
@ -77,7 +88,7 @@ Development builds stay fast. The release-only optimizations (the Rust release p
|
||||
|
||||
| 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` |
|
||||
| Linux | `gcc`, `zlib` headers, `patchelf`; plus `rpm` for `.rpm` packages, `fuse` for `.AppImage`, and, for the AppImage's bundled libraries, `zstd` (plus `ar`/`tar` and `readelf`, from binutils). Debian/Ubuntu: `sudo apt-get install build-essential zlib1g-dev patchelf rpm fuse zstd`. Fedora: `sudo dnf install gcc zlib-devel patchelf rpm-build fuse zstd` (also install the [Linux runtime libraries](#prerequisites) to run what you 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. |
|
||||
|
||||
@ -104,12 +115,25 @@ 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}/` |
|
||||
| Linux | `./gradlew :composeApp:packageGraalvmDeb :composeApp:packageGraalvmRpm`, then `./gradlew :composeApp:packageGraalvmAppImage -PappImage=true` | `composeApp/build/compose/binaries/main/graalvm-{deb,rpm,appimage}/` |
|
||||
| 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).
|
||||
|
||||
The `.deb` and `.rpm` use your distro's shared libraries and declare them as dependencies, so the package manager installs them (on Fedora, `vlc-libs` needs RPM Fusion enabled). The `.AppImage` is the self-contained one: it bundles VLC, GTK 3 and WebKitGTK (see [What the AppImage bundles](#what-the-appimage-bundles)).
|
||||
|
||||
#### What the AppImage bundles
|
||||
|
||||
Build it with `./gradlew :composeApp:packageGraalvmAppImage -PappImage=true`. Without `-PappImage=true` nothing is bundled and the app uses the system libraries, which is what the `.deb`/`.rpm` and `:composeApp:run` do.
|
||||
|
||||
The bundle follows the [AppImage exclude list](https://github.com/AppImageCommunity/pkg2appimage/blob/master/excludelist) and what Tauri apps ship. It carries VLC (audio-only, from the [vlcj-bundler](https://github.com/team-spotube/vlcj-bundler-gradle-plugin) plugin's `bundleVlc` option), GTK 3, WebKitGTK 4.1, GLib/GIO, pango/cairo and their dependencies, plus glib-networking for HTTPS in the webview. It leaves the host's glibc, `libstdc++`, `libz`, GL/EGL/DRM, X11/xcb, ALSA, fontconfig/freetype/harfbuzz and the Wayland libraries alone, so the host's graphics, display and sound stack is used. Everything is taken from the Ubuntu 24.04 archive, so the AppImage needs glibc 2.38 or newer.
|
||||
|
||||
How it works, in short:
|
||||
- [`DownloadLinuxGtkBundleTask`](buildSrc/src/main/kotlin/org/spotube/gradle/DownloadLinuxGtkBundleTask.kt) downloads the packages, keeps the libraries reachable from GTK/WebKit and relocates them with `patchelf`. Needs `ar`, `tar`, `zstd`, `readelf` and `patchelf`.
|
||||
- WebKitGTK has its helper-process path compiled in, so the library is patched to look relative to the working directory, and at startup the app moves there ([`AppImageEnvironment`](composeApp/src/jvmMain/kotlin/org/spotube/spotube/core/appimage/AppImageEnvironment.kt)). It also points `GIO_EXTRA_MODULES` at the bundled TLS module and sets `WEBKIT_DISABLE_DMABUF_RENDERER=1`, because the bundled WebKitGTK 2.44's dmabuf renderer never paints (or aborts) against a newer host Mesa/compositor. The display backend is left alone: forcing X11 doesn't help, since the webview doesn't paint on XWayland either.
|
||||
- Startup order matters: `main()` calls `GraalVmInitializer.initialize()` first, since the VLC loader and the environment setup read `compose.application.resources.dir`.
|
||||
|
||||
#### 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).
|
||||
|
||||
9
buildSrc/build.gradle.kts
Normal file
9
buildSrc/build.gradle.kts
Normal file
@ -0,0 +1,9 @@
|
||||
// Build logic for the Linux AppImage bundle (see DownloadLinuxGtkBundleTask). Lives in buildSrc so the IDE
|
||||
// resolves it; an `apply(from = ...)` script plugin gets no Kotlin classpath in Android Studio.
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
@ -0,0 +1,260 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
// Bundles GTK 3 and WebKitGTK 4.1 into the Linux AppImage, the way Tauri apps do, so it runs on a
|
||||
// system that has neither. Used only with -PappImage=true; deb/rpm depend on the distro's packages.
|
||||
//
|
||||
// The libraries come from the Ubuntu archive. Like the AppImage exclude list prescribes, nothing that
|
||||
// has to match the host is bundled: glibc, libstdc++, libz, GL/EGL/DRM, X11/xcb, ALSA, fontconfig,
|
||||
// freetype, harfbuzz, expat, gmp and the wayland libraries. What is left is pruned to the libraries
|
||||
// reachable from GTK/WebKit through their ELF NEEDED entries.
|
||||
//
|
||||
// Layout (copied into the AppDir root by Nucleus; AppRun puts $APPDIR/usr/lib on LD_LIBRARY_PATH):
|
||||
// usr/lib/*.so.N shared libraries, RUNPATH=$ORIGIN
|
||||
// usr/lib/<triplet>/webkit2gtk-4.1/WebKit*Process helper processes
|
||||
// usr/lib/<triplet>/gio/modules/libgiognutls.so TLS backend for WebKit/libsoup
|
||||
// libwebkit2gtk has the helper path compiled in as /usr/lib/<triplet>/webkit2gtk-4.1; "/usr" is
|
||||
// patched to "././" (same length), so the helpers resolve relative to the working directory, which the
|
||||
// app sets to $APPDIR/usr at startup (AppImageEnvironment). Needs `ar`, `tar` (+ `zstd`), `readelf`
|
||||
// and `patchelf` on the build machine.
|
||||
|
||||
package org.spotube.gradle
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.file.DirectoryProperty
|
||||
import org.gradle.api.provider.Property
|
||||
import org.gradle.api.tasks.Input
|
||||
import org.gradle.api.tasks.OutputDirectory
|
||||
import org.gradle.api.tasks.TaskAction
|
||||
import org.gradle.work.DisableCachingByDefault
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.zip.GZIPInputStream
|
||||
|
||||
@DisableCachingByDefault(because = "Downloads from the network")
|
||||
abstract class DownloadLinuxGtkBundleTask : DefaultTask() {
|
||||
/** Root passed on to Nucleus' `appResourcesRootDir`; the files land in `<root>/linux-<arch>/usr`. */
|
||||
@get:OutputDirectory
|
||||
abstract val outputDir: DirectoryProperty
|
||||
|
||||
/** Ubuntu release the binaries come from. Its glibc is the minimum the bundle runs on. */
|
||||
@get:Input
|
||||
abstract val ubuntuRelease: Property<String>
|
||||
|
||||
init {
|
||||
group = "dependencies"
|
||||
description = "Downloads and relocates GTK 3 and WebKitGTK 4.1 for the Linux AppImage."
|
||||
}
|
||||
|
||||
private val packages = listOf(
|
||||
"libgtk-3-0t64", "libwebkit2gtk-4.1-0", "libjavascriptcoregtk-4.1-0", "glib-networking",
|
||||
"libatk-bridge2.0-0t64", "libatk1.0-0t64", "libatomic1", "libatspi2.0-0t64", "libbrotli1",
|
||||
"libbz2-1.0", "libcairo-gobject2", "libcairo2", "libcap2", "libdatrie1", "libdbus-1-3",
|
||||
"libdw1t64", "libelf1t64", "libenchant-2-2", "libepoxy0", "libevdev2", "libffi8", "libgcrypt20",
|
||||
"libgdk-pixbuf-2.0-0", "libglib2.0-0t64", "libgnutls30t64", "libgssapi-krb5-2",
|
||||
"libgstreamer-gl1.0-0", "libgstreamer-plugins-base1.0-0", "libgstreamer1.0-0", "libgudev-1.0-0",
|
||||
"libharfbuzz-icu0", "libhogweed6t64", "libhyphen0", "libicu74", "libidn2-0", "libjpeg-turbo8",
|
||||
"libjpeg8", "libk5crypto3", "libkeyutils1", "libkrb5-3", "libkrb5support0", "liblcms2-2",
|
||||
"liblz4-1", "liblzma5", "libmanette-0.2-0", "libnettle8t64", "libnghttp2-14", "liborc-0.4-0t64",
|
||||
"libp11-kit0", "libpango-1.0-0", "libpangocairo-1.0-0", "libpangoft2-1.0-0", "libpixman-1-0",
|
||||
"libpng16-16t64", "libpsl5t64", "libseccomp2", "libsecret-1-0", "libsharpyuv0", "libsoup-3.0-0",
|
||||
"libsqlite3-0", "libsystemd0", "libtasn1-6", "libthai0", "libudev1", "libunistring5", "libunwind8",
|
||||
"libwebp7", "libwebpdemux2", "libwebpmux3", "libxcb-render0", "libxcb-shm0", "libxcomposite1",
|
||||
"libxcursor1", "libxdamage1", "libxext6", "libxfixes3", "libxi6", "libxinerama1", "libxkbcommon0",
|
||||
"libxml2", "libxrandr2", "libxrender1", "libxslt1.1", "libzstd1",
|
||||
)
|
||||
|
||||
// Provided by the host, per the AppImage exclude list (which includes the wayland libraries). Never
|
||||
// copied even if a bundled package carries them.
|
||||
private val hostLibraries = setOf(
|
||||
"libc.so.6", "libm.so.6", "libdl.so.2", "libpthread.so.0", "librt.so.1", "libresolv.so.2",
|
||||
"libutil.so.1", "libnsl.so.1", "libstdc++.so.6", "libgcc_s.so.1", "libz.so.1", "libGL.so.1",
|
||||
"libEGL.so.1", "libGLX.so.0", "libGLdispatch.so.0", "libOpenGL.so.0", "libdrm.so.2",
|
||||
"libgbm.so.1", "libX11.so.6", "libX11-xcb.so.1", "libxcb.so.1", "libasound.so.2",
|
||||
"libfontconfig.so.1", "libfreetype.so.6", "libharfbuzz.so.0", "libexpat.so.1", "libgmp.so.10",
|
||||
"libwayland-client.so.0", "libwayland-server.so.0", "libwayland-cursor.so.0",
|
||||
"libwayland-egl.so.1", "libuuid.so.1", "libgpg-error.so.0", "libfribidi.so.0", "libXau.so.6",
|
||||
"libXdmcp.so.6", "libpcre2-8.so.0", "libselinux.so.1", "libmount.so.1", "libblkid.so.1",
|
||||
)
|
||||
|
||||
private val rootLibraries = listOf(
|
||||
"libgtk-3.so.0", "libgdk-3.so.0", "libwebkit2gtk-4.1.so.0", "libjavascriptcoregtk-4.1.so.0",
|
||||
)
|
||||
|
||||
@TaskAction
|
||||
fun run() {
|
||||
val arch = System.getProperty("os.arch").lowercase()
|
||||
val isArm = arch.contains("aarch64") || arch.contains("arm64")
|
||||
val resourceDir = if (isArm) "linux-arm64" else "linux-x64"
|
||||
val debArch = if (isArm) "arm64" else "amd64"
|
||||
val triplet = if (isArm) "aarch64-linux-gnu" else "x86_64-linux-gnu"
|
||||
val mirror = if (isArm) "http://ports.ubuntu.com/ubuntu-ports" else "http://archive.ubuntu.com/ubuntu"
|
||||
|
||||
val target = outputDir.dir(resourceDir).get().asFile
|
||||
val marker = File(target, ".complete")
|
||||
if (marker.exists()) {
|
||||
println("Linux GTK/WebKit bundle already present. Skipping download.")
|
||||
return
|
||||
}
|
||||
for (tool in listOf("ar", "tar", "readelf", "patchelf")) {
|
||||
val found = try {
|
||||
ProcessBuilder(tool, "--version").redirectErrorStream(true).start()
|
||||
.also { it.inputStream.readBytes() }.waitFor() == 0
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
if (!found) throw GradleException("`$tool` is required to bundle GTK/WebKit on Linux but was not found on PATH.")
|
||||
}
|
||||
|
||||
target.deleteRecursively()
|
||||
val work = File(temporaryDir, "work").apply { deleteRecursively(); mkdirs() }
|
||||
val root = File(work, "root").apply { mkdirs() }
|
||||
|
||||
val index = fetchPackageIndex(mirror, debArch)
|
||||
for (pkg in packages) {
|
||||
val filename = index[pkg] ?: throw GradleException("Package $pkg not found in the ${ubuntuRelease.get()} index")
|
||||
val deb = File(work, "$pkg.deb")
|
||||
println("Downloading $pkg")
|
||||
URI("$mirror/$filename").toURL().openStream().use { input -> deb.outputStream().use { input.copyTo(it) } }
|
||||
extractDeb(deb, root, File(work, "ar-$pkg"))
|
||||
}
|
||||
|
||||
val srcLib = File(root, "usr/lib/$triplet")
|
||||
val libOut = File(target, "usr/lib").apply { mkdirs() }
|
||||
|
||||
// Libraries by SONAME (libbz2.so.1.0 and friends don't end in a single number)
|
||||
val available = HashMap<String, File>()
|
||||
srcLib.listFiles { f -> f.isFile && ".so" in f.name }.orEmpty().forEach { file ->
|
||||
soname(file)?.let { available.putIfAbsent(it, file) }
|
||||
}
|
||||
|
||||
val helperSrc = File(srcLib, "webkit2gtk-4.1")
|
||||
val helperOut = File(libOut, "$triplet/webkit2gtk-4.1")
|
||||
val helpers = listOf("WebKitWebProcess", "WebKitNetworkProcess", "WebKitGPUProcess")
|
||||
for (name in helpers) {
|
||||
// copyTo drops the mode bits; WebKit spawns these, so they must stay executable
|
||||
File(helperSrc, name).copyTo(File(helperOut, name), overwrite = true).setExecutable(true, false)
|
||||
}
|
||||
val injected = File(helperSrc, "injected-bundle/libwebkit2gtkinjectedbundle.so")
|
||||
injected.copyTo(File(helperOut, "injected-bundle/${injected.name}"), overwrite = true)
|
||||
val gioTls = File(srcLib, "gio/modules/libgiognutls.so")
|
||||
gioTls.copyTo(File(libOut, "$triplet/gio/modules/${gioTls.name}"), overwrite = true)
|
||||
|
||||
// Keep only what GTK, WebKit, their helpers and the TLS module pull in via NEEDED
|
||||
val pending = ArrayDeque(rootLibraries)
|
||||
listOf(*helpers.map { File(helperOut, it) }.toTypedArray(), File(helperOut, "injected-bundle/${injected.name}"),
|
||||
File(libOut, "$triplet/gio/modules/${gioTls.name}")).forEach { pending += needed(it) }
|
||||
val keep = LinkedHashSet<String>()
|
||||
while (pending.isNotEmpty()) {
|
||||
val name = pending.removeFirst()
|
||||
if (name in hostLibraries || name in keep) continue
|
||||
val file = available[name] ?: continue
|
||||
keep += name
|
||||
pending += needed(file)
|
||||
}
|
||||
for (name in keep) available.getValue(name).copyTo(File(libOut, name), overwrite = true)
|
||||
|
||||
patchWebKitPaths(File(libOut, "libwebkit2gtk-4.1.so.0"), triplet)
|
||||
|
||||
// Libraries find each other next to themselves; the helpers and modules sit two/three levels down
|
||||
libOut.listFiles { f -> f.isFile }.orEmpty().forEach { patchRpath(it, "\$ORIGIN") }
|
||||
helpers.forEach { patchRpath(File(helperOut, it), "\$ORIGIN/../..") }
|
||||
patchRpath(File(helperOut, "injected-bundle/${injected.name}"), "\$ORIGIN/../../..")
|
||||
patchRpath(File(libOut, "$triplet/gio/modules/${gioTls.name}"), "\$ORIGIN/../../..")
|
||||
|
||||
marker.writeText("")
|
||||
work.deleteRecursively()
|
||||
println("GTK/WebKit bundled into $target (${keep.size} libraries)")
|
||||
}
|
||||
|
||||
/** "/usr/lib/<triplet>/webkit2gtk-4.1[/injected-bundle]" -> "././lib/<triplet>/webkit2gtk-4.1[/...]". */
|
||||
private fun patchWebKitPaths(lib: File, triplet: String) {
|
||||
val bytes = lib.readBytes()
|
||||
val needle = "/usr/lib/$triplet/webkit2gtk-4.1".toByteArray()
|
||||
val replacement = "././".toByteArray()
|
||||
var count = 0
|
||||
var i = 0
|
||||
while (i <= bytes.size - needle.size) {
|
||||
var match = true
|
||||
for (j in needle.indices) {
|
||||
if (bytes[i + j] != needle[j]) { match = false; break }
|
||||
}
|
||||
if (match) {
|
||||
replacement.copyInto(bytes, i)
|
||||
count++
|
||||
i += needle.size
|
||||
} else {
|
||||
i++
|
||||
}
|
||||
}
|
||||
if (count == 0) throw GradleException("WebKit helper path not found in ${lib.name}; the bundling layout needs updating")
|
||||
lib.writeBytes(bytes)
|
||||
}
|
||||
|
||||
private fun soname(file: File): String? =
|
||||
exec(file.parentFile, "readelf", "-d", file.absolutePath).lineSequence()
|
||||
.firstOrNull { "(SONAME)" in it }?.substringAfter('[')?.substringBefore(']')
|
||||
|
||||
private fun needed(file: File): List<String> =
|
||||
exec(file.parentFile, "readelf", "-d", file.absolutePath).lineSequence()
|
||||
.filter { "(NEEDED)" in it }.map { it.substringAfter('[').substringBefore(']') }.toList()
|
||||
|
||||
/** name -> pool filename; `-updates` is read first so security/bugfix builds win. */
|
||||
private fun fetchPackageIndex(mirror: String, debArch: String): Map<String, String> {
|
||||
val release = ubuntuRelease.get()
|
||||
val index = HashMap<String, String>()
|
||||
for (suite in listOf("$release-updates", release)) {
|
||||
for (component in listOf("main", "universe")) {
|
||||
val url = URI("$mirror/dists/$suite/$component/binary-$debArch/Packages.gz").toURL()
|
||||
GZIPInputStream(url.openStream()).bufferedReader().useLines { lines ->
|
||||
var name: String? = null
|
||||
for (line in lines) {
|
||||
when {
|
||||
line.startsWith("Package: ") -> name = line.removePrefix("Package: ")
|
||||
line.startsWith("Filename: ") -> name?.let { index.putIfAbsent(it, line.removePrefix("Filename: ")) }
|
||||
line.isEmpty() -> name = null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
private fun extractDeb(deb: File, into: File, scratch: File) {
|
||||
scratch.mkdirs()
|
||||
exec(scratch, "ar", "x", deb.absolutePath)
|
||||
val data = scratch.listFiles { f -> f.name.startsWith("data.tar") }?.singleOrNull()
|
||||
?: throw GradleException("No data archive in ${deb.name}")
|
||||
// GNU tar picks the decompressor from the content (zstd needs the zstd binary installed)
|
||||
exec(scratch, "tar", "-xf", data.absolutePath, "-C", into.absolutePath)
|
||||
}
|
||||
|
||||
private fun patchRpath(file: File, rpath: String) {
|
||||
exec(file.parentFile, "patchelf", "--set-rpath", rpath, file.absolutePath)
|
||||
}
|
||||
|
||||
private fun exec(dir: File, vararg command: String): String {
|
||||
val process = ProcessBuilder(*command).directory(dir).redirectErrorStream(true).start()
|
||||
val output = process.inputStream.bufferedReader().readText()
|
||||
if (process.waitFor() != 0) {
|
||||
throw GradleException("`${command.joinToString(" ")}` failed:\n$output")
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
@ -18,11 +18,13 @@
|
||||
import gobley.gradle.GobleyHost
|
||||
import gobley.gradle.Variant
|
||||
import gobley.gradle.cargo.dsl.jvm
|
||||
import gobley.gradle.rust.targets.RustAndroidTarget
|
||||
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
|
||||
import org.gradle.api.tasks.Copy
|
||||
import org.spotube.gradle.DownloadLinuxGtkBundleTask
|
||||
import java.io.FileInputStream
|
||||
import java.util.Properties
|
||||
|
||||
@ -106,9 +108,30 @@ tasks.configureEach {
|
||||
}
|
||||
}
|
||||
|
||||
// Windows/macOS always bundle VLC. On Linux only the self-contained AppImage (-PappImage=true) bundles
|
||||
// VLC, GTK and WebKitGTK; deb/rpm use the distro's packages.
|
||||
val isLinuxHost = System.getProperty("os.name").lowercase().contains("linux")
|
||||
val bundleForAppImage = isLinuxHost && providers.gradleProperty("appImage").map { it.toBoolean() }.getOrElse(false)
|
||||
|
||||
vlcjBundler {
|
||||
packageName = "$bundleId.core.generated" // choose a different package
|
||||
objectName = "VLCBundleLoaderGenerated" // or rename the object
|
||||
bundleVlc = !isLinuxHost || bundleForAppImage
|
||||
}
|
||||
|
||||
// GTK 3 + WebKitGTK for the AppImage (see DownloadLinuxGtkBundleTask in buildSrc)
|
||||
val downloadLinuxGtkBundle = tasks.register<DownloadLinuxGtkBundleTask>("downloadLinuxGtkBundle") {
|
||||
outputDir.set(layout.buildDirectory.dir("linux-gtk-bundle"))
|
||||
ubuntuRelease.set("noble")
|
||||
}
|
||||
val linuxGtkBundleDir: Provider<Directory> = downloadLinuxGtkBundle.flatMap { it.outputDir }
|
||||
|
||||
// One resources root holding VLC and GTK/WebKit, laid out for the AppImage
|
||||
val assembleAppImageResources = tasks.register<Sync>("assembleAppImageResources") {
|
||||
from(vlcjBundler.vlcNativesDirectory)
|
||||
from(linuxGtkBundleDir)
|
||||
exclude("**/.complete")
|
||||
into(layout.buildDirectory.dir("appimage-resources"))
|
||||
}
|
||||
|
||||
|
||||
@ -302,6 +325,7 @@ kotlin {
|
||||
implementation(libs.jna)
|
||||
|
||||
implementation(libs.nucleus.core.runtime)
|
||||
implementation(libs.nucleus.graalvm.runtime)
|
||||
implementation(libs.nucleus.nucleus.application)
|
||||
implementation(libs.nucleus.decorated.window.tao)
|
||||
implementation(libs.compose.native.tray)
|
||||
@ -401,10 +425,15 @@ nucleus.application {
|
||||
|
||||
nativeDistributions {
|
||||
packageName = bundleId
|
||||
// User-facing name (.app bundle, DMG productName, Launchpad/Dock); packageName above is the reverse-DNS id.
|
||||
// The GraalVM DMG task reads only appName ?: packageName, not macOS.packageName
|
||||
appName = if (isNightly) "Spotube Nightly" else "Spotube"
|
||||
packageVersion = version
|
||||
licenseFile = project.file("../LICENSE")
|
||||
|
||||
appResourcesRootDir.set(vlcjBundler.vlcNativesDirectory)
|
||||
appResourcesRootDir.set(
|
||||
if (bundleForAppImage) assembleAppImageResources.map { layout.buildDirectory.dir("appimage-resources").get() } else vlcjBundler.vlcNativesDirectory
|
||||
)
|
||||
|
||||
targetFormats(
|
||||
TargetFormat.Dmg,
|
||||
@ -424,6 +453,44 @@ nucleus.application {
|
||||
modules("jdk.security.auth")
|
||||
debMaintainer = "Team Spotube <team.spotube@proton.me>"
|
||||
rpmLicenseType = "AGPL-3.0-or-later"
|
||||
// deb/rpm use the distro's shared libraries (only the AppImage bundles VLC). VLC is not
|
||||
// bundled on Linux otherwise, the webview needs WebKitGTK 4.1, and the window/tray/MPRIS
|
||||
// natives link GTK 3, GLib/GIO and D-Bus. Setting these replaces electron-builder's
|
||||
// Electron-specific defaults (libnss3, libnotify, ...).
|
||||
debDepends = listOf(
|
||||
"libvlc5",
|
||||
"vlc-plugin-base",
|
||||
"libwebkit2gtk-4.1-0",
|
||||
"libgtk-3-0 | libgtk-3-0t64",
|
||||
"libglib2.0-0 | libglib2.0-0t64",
|
||||
"libdbus-1-3",
|
||||
"libasound2 | libasound2t64",
|
||||
"libgl1",
|
||||
"libfontconfig1",
|
||||
"libfreetype6",
|
||||
"libx11-6",
|
||||
"libxext6",
|
||||
"libxi6",
|
||||
"libxrender1",
|
||||
"libxtst6",
|
||||
)
|
||||
// vlc-libs comes from RPM Fusion (Fedora ships no VLC); the README tells users to enable it
|
||||
rpmRequires = listOf(
|
||||
"vlc-libs",
|
||||
"webkit2gtk4.1",
|
||||
"gtk3",
|
||||
"glib2",
|
||||
"dbus-libs",
|
||||
"alsa-lib",
|
||||
"mesa-libGL",
|
||||
"fontconfig",
|
||||
"freetype",
|
||||
"libX11",
|
||||
"libXext",
|
||||
"libXi",
|
||||
"libXrender",
|
||||
"libXtst",
|
||||
)
|
||||
}
|
||||
windows {
|
||||
iconFile.set(project.file("$desktopIconDir/icon.ico"))
|
||||
@ -435,6 +502,8 @@ nucleus.application {
|
||||
|
||||
macOS {
|
||||
iconFile.set(project.file("$desktopIconDir/icon.icns"))
|
||||
bundleID = if (isNightly) "$bundleId.nightly" else bundleId
|
||||
dockName = appName
|
||||
notarization {
|
||||
|
||||
}
|
||||
@ -527,6 +596,8 @@ tasks.withType<Copy>().matching { it.name == "copyGraalvmAwtSoLibs" }.configureE
|
||||
// Issue with compose plugin
|
||||
// https://github.com/JetBrains/compose-hot-reload/blob/master/docs/Known_limitations.md#property-composeapplicationresourcesdir-is-null-when-running-hot-reload-tasks
|
||||
tasks.withType<ComposeHotRun>().configureEach {
|
||||
// Populates the resources dir below with the VLC natives (appResourcesRootDir); hot run doesn't depend on it by default
|
||||
dependsOn("prepareAppResources")
|
||||
jvmArgs(
|
||||
"--enable-native-access=ALL-UNNAMED"
|
||||
)
|
||||
@ -544,6 +615,15 @@ cargo {
|
||||
}
|
||||
}
|
||||
|
||||
// Gobley adds build/intermediates/rust/<triple>/<variant> as a jniLibs source dir, but AGP leaves source dirs
|
||||
// that don't exist at configuration time out of the merge inputs. On a clean build (CI) libcompose_app.so would
|
||||
// then silently miss the APK and crash at runtime with UnsatisfiedLinkError, so create the dirs up front.
|
||||
RustAndroidTarget.entries.forEach { target ->
|
||||
Variant.entries.forEach { variant ->
|
||||
layout.buildDirectory.dir("intermediates/rust/${target.rustTriple}/$variant").get().asFile.mkdirs()
|
||||
}
|
||||
}
|
||||
|
||||
// Google only ships x86_64 Linux NDK toolchains, so on a Linux arm64 host the Android Rust targets can never
|
||||
// link (clang: "Exec format error"). Desktop packaging doesn't need them; Android is built on x86_64 runners.
|
||||
run {
|
||||
@ -560,3 +640,12 @@ run {
|
||||
.configureEach { enabled = false }
|
||||
}
|
||||
}
|
||||
|
||||
// Nucleus' resource copies (prepareAppResources, copyGraalvmAppResources) drop the execute bit, but WebKitGTK
|
||||
// spawns its helper processes from the AppImage bundle (DownloadLinuxGtkBundleTask in buildSrc) and fails with
|
||||
// "Permission denied" otherwise.
|
||||
tasks.matching { it.name == "prepareAppResources" || it.name == "copyGraalvmAppResources" }.configureEach {
|
||||
(this as? org.gradle.api.tasks.AbstractCopyTask)?.filesMatching("**/webkit2gtk-4.1/WebKit*Process") {
|
||||
permissions { unix("rwxr-xr-x") }
|
||||
}
|
||||
}
|
||||
|
||||
@ -18,15 +18,19 @@
|
||||
package org.spotube.spotube.core.share
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.core.app.ShareCompat
|
||||
import org.spotube.spotube.core.share.ShareService
|
||||
|
||||
class AndroidShareService(private val context: Context) : ShareService {
|
||||
override fun share(url: String, title: String) {
|
||||
ShareCompat.IntentBuilder(context)
|
||||
// context is the application context (Koin), which can only start activities in a new task
|
||||
val chooser = ShareCompat.IntentBuilder(context)
|
||||
.setType("text/plain")
|
||||
.setSubject(title)
|
||||
.setText(url)
|
||||
.startChooser()
|
||||
.createChooserIntent()
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
context.startActivity(chooser)
|
||||
}
|
||||
}
|
||||
|
||||
22
composeApp/src/androidMain/res/values-sv/strings.xml
Normal file
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>
|
||||
169
composeApp/src/commonMain/composeResources/values-sv/strings.xml
Normal file
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>
|
||||
@ -0,0 +1,68 @@
|
||||
/*
|
||||
* 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 org.spotube.spotube.core.appimage
|
||||
|
||||
import com.sun.jna.Function
|
||||
import com.sun.jna.NativeLibrary
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Makes the GTK 3 and WebKitGTK bundled into the AppImage usable. The AppImage's AppRun already puts
|
||||
* `$APPDIR/usr/lib` on `LD_LIBRARY_PATH`; the rest has to be done in-process, before any GTK/WebKit code
|
||||
* loads, because the AppRun is generated and can't be customised:
|
||||
*
|
||||
* - WebKitGTK has its helper-process path compiled in. The build patches it to a path relative to the
|
||||
* working directory (see `DownloadLinuxGtkBundleTask` in buildSrc), so the working directory is moved to
|
||||
* `$APPDIR/usr`.
|
||||
* - TLS in WebKit/libsoup needs glib-networking's GIO module, which lives in the bundle.
|
||||
* - The bundled WebKitGTK 2.44 uses its dmabuf renderer by default. Against a newer host Mesa/compositor
|
||||
* that path never paints the first frame (the webview stays see-through or black until the window is
|
||||
* resized), and the bundled GTK/epoxy can even abort while creating the GL context. Disabling it makes
|
||||
* the webview paint on native Wayland. Left alone if the user already set it.
|
||||
*
|
||||
* Does nothing outside the AppImage (deb/rpm/dev runs have no bundled `usr/lib/libgtk-3.so.0`).
|
||||
*/
|
||||
object AppImageEnvironment {
|
||||
fun apply() {
|
||||
if (!System.getProperty("os.name").lowercase().contains("linux")) return
|
||||
val resourcesDir = System.getProperty("compose.application.resources.dir") ?: return
|
||||
val usrDir = File(resourcesDir, "usr")
|
||||
if (!File(usrDir, "lib/libgtk-3.so.0").isFile) return
|
||||
|
||||
try {
|
||||
val libc = NativeLibrary.getInstance("c")
|
||||
val setenv = libc.getFunction("setenv")
|
||||
val triplet = if (System.getProperty("os.arch").lowercase().let { "aarch64" in it || "arm64" in it }) {
|
||||
"aarch64-linux-gnu"
|
||||
} else {
|
||||
"x86_64-linux-gnu"
|
||||
}
|
||||
setenv.set("GIO_EXTRA_MODULES", File(usrDir, "lib/$triplet/gio/modules").absolutePath)
|
||||
if (System.getenv("WEBKIT_DISABLE_DMABUF_RENDERER").isNullOrBlank()) {
|
||||
setenv.set("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
|
||||
}
|
||||
libc.getFunction("chdir").invokeInt(arrayOf<Any>(usrDir.absolutePath))
|
||||
} catch (_: Throwable) {
|
||||
// Falls back to the host's libraries, which only works where they are installed
|
||||
}
|
||||
}
|
||||
|
||||
private fun Function.set(name: String, value: String) {
|
||||
invokeInt(arrayOf<Any>(name, value, 1))
|
||||
}
|
||||
}
|
||||
@ -38,6 +38,7 @@ import spotube.composeapp.generated.resources.spotube_logo
|
||||
import dev.nucleusframework.application.DecoratedWindow
|
||||
import dev.nucleusframework.application.NucleusBackend
|
||||
import dev.nucleusframework.application.nucleusApplication
|
||||
import dev.nucleusframework.graalvm.GraalVmInitializer
|
||||
import dev.nucleusframework.window.tao.TaoScreenGeometry
|
||||
import io.github.vinceglb.filekit.FileKit
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@ -50,6 +51,7 @@ import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
import org.spotube.spotube.core.appimage.AppImageEnvironment
|
||||
import org.spotube.spotube.core.di.initKoin
|
||||
import org.spotube.spotube.core.i18n.AppLocaleProvider
|
||||
import org.spotube.spotube.core.newpipe.NewPipeDownloader
|
||||
@ -132,6 +134,11 @@ private fun restoreWindowState(saved: PersistedWindowState?): PersistedWindowSta
|
||||
|
||||
@OptIn(ExperimentalComposeUiApi::class, FlowPreview::class)
|
||||
fun main() {
|
||||
// Sets compose.application.resources.dir in the native image. initKoin() creates the audio
|
||||
// player, whose VLC loader reads it, so it can't wait for nucleusApplication() (which calls
|
||||
// this again, harmlessly). Without it the bundled VLC is skipped for the system one.
|
||||
GraalVmInitializer.initialize()
|
||||
AppImageEnvironment.apply()
|
||||
FileKit.init(appId = "org.spotube.spotube")
|
||||
initKoin()
|
||||
NewPipeDownloader.init(KoinPathsProvider.paths)
|
||||
|
||||
@ -72,7 +72,7 @@ ksp = "2.3.5"
|
||||
spotube-gradle = "0.1.2"
|
||||
cryptography = "0.6.0"
|
||||
reorderable = "3.1.0"
|
||||
vlcjBundler = "0.1.0"
|
||||
vlcjBundler = "0.2.0"
|
||||
vanniktechMavenPublish = "0.37.0"
|
||||
uniffi = "0.3.7"
|
||||
kotlinx-atomicfu = "0.33.0"
|
||||
@ -131,6 +131,7 @@ newpipeextractor = { module = "com.github.teamnewpipe:NewPipeExtractor", version
|
||||
nucleus-core-runtime = { module = "dev.nucleusframework:nucleus.core-runtime", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-decorated-window-tao = { module = "dev.nucleusframework:nucleus.decorated-window-tao", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-media-control = { module = "dev.nucleusframework:nucleus.media-control", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-graalvm-runtime = { module = "dev.nucleusframework:nucleus.graalvm-runtime", version.ref = "nucleusNucleusApplication" }
|
||||
nucleus-nucleus-application = { module = "dev.nucleusframework:nucleus.nucleus-application", version.ref = "nucleusNucleusApplication" }
|
||||
semver = { module = "net.swiftzer.semver:semver", version.ref = "semver" }
|
||||
vlcj = { module = "uk.co.caprica:vlcj", version.ref = "vlcj" }
|
||||
|
||||
Loading…
Reference in New Issue
Block a user