Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
293561d81f | ||
|
|
2de31735f7 | ||
|
|
327aa08763 | ||
|
|
8aee86fc60 | ||
|
|
cc65f238ae | ||
|
|
acd087cfe0 | ||
|
|
05de10c912 |
@@ -28,19 +28,17 @@ subtracks is an open source Android client for [Subsonic-compatible](http://www.
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Home screen to browse your library over many categories, genres, and more
|
- **Large Libraries:** Built for very large libraries, from syncing the whole collection to playing playlists with thousands of tracks.
|
||||||
- Album and artist artwork, shown full-resolution in the detail and now playing views
|
- **Downloads:** Song and bulk downloads, with download quality (encoding, bitrate) and metered-connection settings.
|
||||||
- Downloads with album, artist and playlist bulk downloads, download quality and a metered-connection toggle
|
- **Offline Mode:** Stars, scrobbles and play counts queue up while you're offline and sync back to the server when you're online again.
|
||||||
- Search across albums, artists, playlists and songs, with per-list sort and filter controls
|
- **Artwork-first:** Album and artist art everywhere, shown full-resolution in the detail and now playing views.
|
||||||
- Gapless playback, an editable queue you can reorder and jump through, and loop and shuffle modes
|
- **Adaptive Theming:** Colours drawn from the current song's artwork.
|
||||||
- Up next queue to add items into the currently playing queue without rewriting it
|
- **Search:** Fast search across the whole library, with per-list sort and filter controls.
|
||||||
- Multiple server support with quick switching
|
- **Playback:** Gapless playback, an editable queue you can reorder and jump through, plus loop and shuffle modes.
|
||||||
- Stream quality (bitrate and format) settings for Wi-Fi and mobile and automatic switching
|
- **Up Next:** Add items into the currently playing queue without rewriting it.
|
||||||
- Scrobbling and starred items, cached while offline and sent back to the server when online
|
- **Multiple Servers:** Connect several servers and switch between them quickly.
|
||||||
- Media notification and headset/Bluetooth controls
|
- **Stream Quality:** Bitrate and format settings for Wi-Fi and mobile, with automatic switching.
|
||||||
- Adaptive theming from the current song's artwork
|
- **System Controls:** Media notification and headset/Bluetooth controls.
|
||||||
|
|
||||||
The full library is mirrored at sync, so browsing, searching, sorting and filtering all work without waiting on the server. Only media and artwork are fetched on demand, and not even that when they're already downloaded.
|
|
||||||
|
|
||||||
## Building
|
## Building
|
||||||
|
|
||||||
@@ -55,6 +53,14 @@ gradle :app:assembleDebug
|
|||||||
|
|
||||||
Test, lint and screenshot commands, plus the integration harness, are documented in [AGENTS.md](AGENTS.md). The architecture is described in [docs/architecture.md](docs/architecture.md).
|
Test, lint and screenshot commands, plus the integration harness, are documented in [AGENTS.md](AGENTS.md). The architecture is described in [docs/architecture.md](docs/architecture.md).
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
The GitHub repository [austinried/subtracks](https://github.com/austinried/subtracks) is a mirror of the repo hosted on my personal (private) code forge that syncs whenever I commit to it.
|
||||||
|
|
||||||
|
Bug reports and feature requests are accepted on GitHub, as well as pull requests, but:
|
||||||
|
* A well-crafted issue with clear reproduction and logs for bugs or mock-ups and use case details for features is preferred over a pull request
|
||||||
|
* Pull requests will be pulled in as patches to my code forge and then synced back (you'll still be the author, I'll be shown as the committer)
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
|
||||||
Want to see subtracks in your language? Visit the project on [Weblate](https://hosted.weblate.org/engage/subtracks/) to help!
|
Want to see subtracks in your language? Visit the project on [Weblate](https://hosted.weblate.org/engage/subtracks/) to help!
|
||||||
@@ -65,7 +71,7 @@ Want to see subtracks in your language? Visit the project on [Weblate](https://h
|
|||||||
|
|
||||||
## AI usage disclosure
|
## AI usage disclosure
|
||||||
|
|
||||||
This project contains code and documentation generated by LLM, which is created via human prompting, iteration and review.
|
This project contains code and documentation generated by LLM, which is created via human prompting, iteration and review. The current native rewrite is based on two (and a half) other iterations of this project that I coded by hand, so the architecture and other decisions behind that are all still coming from me.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ android {
|
|||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
|
|
||||||
androidResources {
|
androidResources {
|
||||||
@@ -43,7 +44,28 @@ android {
|
|||||||
|
|
||||||
lint {
|
lint {
|
||||||
checkReleaseBuilds = false
|
checkReleaseBuilds = false
|
||||||
disable += setOf("MissingTranslation", "MissingQuantity")
|
warningsAsErrors = true
|
||||||
|
disable +=
|
||||||
|
setOf(
|
||||||
|
"MissingTranslation",
|
||||||
|
"MissingQuantity",
|
||||||
|
// Version nags are handled by the dependency-update process, not the build.
|
||||||
|
"AndroidGradlePluginVersion",
|
||||||
|
"GradleDependency",
|
||||||
|
"NewerVersionAvailable",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
packaging {
|
||||||
|
jniLibs {
|
||||||
|
// Prebuilt .so files with no symbol table; leaving them unstripped avoids the strip warning.
|
||||||
|
keepDebugSymbols +=
|
||||||
|
setOf(
|
||||||
|
"**/libandroidx.graphics.path.so",
|
||||||
|
"**/libdatastore_shared_counter.so",
|
||||||
|
"**/libsqliteJni.so",
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
testOptions {
|
testOptions {
|
||||||
@@ -71,6 +93,7 @@ android {
|
|||||||
kotlin {
|
kotlin {
|
||||||
compilerOptions {
|
compilerOptions {
|
||||||
jvmTarget.set(JvmTarget.JVM_17)
|
jvmTarget.set(JvmTarget.JVM_17)
|
||||||
|
allWarningsAsErrors.set(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,6 +105,8 @@ ksp {
|
|||||||
arg("room.schemaLocation", "$projectDir/schemas")
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resolving the variant classpaths here makes AGP warn that a configuration was resolved during
|
||||||
|
// configuration time; upstream, tracked in mikepenz/AboutLibraries#1369.
|
||||||
aboutLibraries {
|
aboutLibraries {
|
||||||
collect {
|
collect {
|
||||||
configPath = file("config")
|
configPath = file("config")
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
@@ -18,7 +19,8 @@
|
|||||||
android:networkSecurityConfig="@xml/network_security_config"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Subtracks">
|
android:theme="@style/Theme.Subtracks"
|
||||||
|
tools:ignore="DataExtractionRules,UnusedAttribute">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -35,7 +37,8 @@
|
|||||||
<service
|
<service
|
||||||
android:name=".playback.PlaybackService"
|
android:name=".playback.PlaybackService"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:foregroundServiceType="mediaPlayback">
|
android:foregroundServiceType="mediaPlayback"
|
||||||
|
tools:ignore="ExportedService">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="androidx.media3.session.MediaSessionService" />
|
<action android:name="androidx.media3.session.MediaSessionService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package com.subtracks.data.download
|
|||||||
|
|
||||||
import android.app.DownloadManager
|
import android.app.DownloadManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Environment
|
import android.os.Environment
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.subtracks.data.model.DownloadError
|
import com.subtracks.data.model.DownloadError
|
||||||
|
|
||||||
class SystemDownloadEngine(
|
class SystemDownloadEngine(
|
||||||
@@ -12,7 +12,7 @@ class SystemDownloadEngine(
|
|||||||
override fun enqueue(request: EngineRequest): Long {
|
override fun enqueue(request: EngineRequest): Long {
|
||||||
val download =
|
val download =
|
||||||
DownloadManager
|
DownloadManager
|
||||||
.Request(Uri.parse(request.uri))
|
.Request(request.uri.toUri())
|
||||||
.setTitle(request.title)
|
.setTitle(request.title)
|
||||||
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_MUSIC, request.path)
|
.setDestinationInExternalFilesDir(context, Environment.DIRECTORY_MUSIC, request.path)
|
||||||
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||||
|
|||||||
@@ -248,11 +248,11 @@ fun HeroHeader(
|
|||||||
onDownloadAction: (BulkDownloadAction) -> Unit,
|
onDownloadAction: (BulkDownloadAction) -> Unit,
|
||||||
onMore: () -> Unit,
|
onMore: () -> Unit,
|
||||||
topInset: Dp,
|
topInset: Dp,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
controlsModifier: Modifier = Modifier,
|
controlsModifier: Modifier = Modifier,
|
||||||
thumbnailRef: CoverArtRef? = null,
|
thumbnailRef: CoverArtRef? = null,
|
||||||
comment: String? = null,
|
comment: String? = null,
|
||||||
onSubtitleClick: (() -> Unit)? = null,
|
onSubtitleClick: (() -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier =
|
modifier =
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalResources
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -123,6 +123,7 @@ fun HomeListScreen(
|
|||||||
artists: LazyPagingItems<Artist>,
|
artists: LazyPagingItems<Artist>,
|
||||||
songs: LazyPagingItems<AlbumSongItem>,
|
songs: LazyPagingItems<AlbumSongItem>,
|
||||||
playlists: LazyPagingItems<Playlist>,
|
playlists: LazyPagingItems<Playlist>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
genres: List<String> = emptyList(),
|
genres: List<String> = emptyList(),
|
||||||
decades: List<Long> = emptyList(),
|
decades: List<Long> = emptyList(),
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
||||||
@@ -135,10 +136,9 @@ fun HomeListScreen(
|
|||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
onGenreClick: (String) -> Unit = {},
|
onGenreClick: (String) -> Unit = {},
|
||||||
onDecadeClick: (Long) -> Unit = {},
|
onDecadeClick: (Long) -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val now = remember { System.currentTimeMillis() }
|
val now = remember { System.currentTimeMillis() }
|
||||||
val resources = LocalContext.current.resources
|
val resources = LocalResources.current
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
topBar = {
|
topBar = {
|
||||||
|
|||||||
@@ -172,6 +172,7 @@ fun HomeScreen(
|
|||||||
feed: HomeFeed,
|
feed: HomeFeed,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
playingSongId: String?,
|
playingSongId: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
topInset: Dp = 0.dp,
|
topInset: Dp = 0.dp,
|
||||||
bottomInset: Dp = 0.dp,
|
bottomInset: Dp = 0.dp,
|
||||||
onAlbumClick: (Album) -> Unit = {},
|
onAlbumClick: (Album) -> Unit = {},
|
||||||
@@ -182,7 +183,6 @@ fun HomeScreen(
|
|||||||
onGenreClick: (String) -> Unit = {},
|
onGenreClick: (String) -> Unit = {},
|
||||||
onDecadeClick: (Long) -> Unit = {},
|
onDecadeClick: (Long) -> Unit = {},
|
||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||||
if (feed.loading) {
|
if (feed.loading) {
|
||||||
@@ -295,6 +295,7 @@ fun OfflineHomeScreen(
|
|||||||
songs: List<AlbumSongItem>,
|
songs: List<AlbumSongItem>,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
playingSongId: String?,
|
playingSongId: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
topInset: Dp = 0.dp,
|
topInset: Dp = 0.dp,
|
||||||
bottomInset: Dp = 0.dp,
|
bottomInset: Dp = 0.dp,
|
||||||
onAlbumClick: (Album) -> Unit = {},
|
onAlbumClick: (Album) -> Unit = {},
|
||||||
@@ -303,7 +304,6 @@ fun OfflineHomeScreen(
|
|||||||
onSongPlay: (Song) -> Unit = {},
|
onSongPlay: (Song) -> Unit = {},
|
||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
onMore: (OfflineListKind) -> Unit = {},
|
onMore: (OfflineListKind) -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
|||||||
@@ -146,6 +146,7 @@ fun AlbumDetailRoute(
|
|||||||
fun AlbumDetailScreen(
|
fun AlbumDetailScreen(
|
||||||
album: Album?,
|
album: Album?,
|
||||||
songs: List<Song>,
|
songs: List<Song>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
discs: List<Disc> = emptyList(),
|
discs: List<Disc> = emptyList(),
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
artwork: ArtworkColors?,
|
artwork: ArtworkColors?,
|
||||||
@@ -160,7 +161,6 @@ fun AlbumDetailScreen(
|
|||||||
onMore: () -> Unit = {},
|
onMore: () -> Unit = {},
|
||||||
onArtistClick: (() -> Unit)? = null,
|
onArtistClick: (() -> Unit)? = null,
|
||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val multiDisc = songs.map { it.disc ?: 1L }.distinct().size > 1
|
val multiDisc = songs.map { it.disc ?: 1L }.distinct().size > 1
|
||||||
val resources = LocalResources.current
|
val resources = LocalResources.current
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ fun AlbumsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onAlbumClick: (Album) -> Unit,
|
onAlbumClick: (Album) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
filtered: Boolean = false,
|
filtered: Boolean = false,
|
||||||
onClearFilters: () -> Unit = {},
|
onClearFilters: () -> Unit = {},
|
||||||
@@ -55,7 +56,6 @@ fun AlbumsContent(
|
|||||||
topInset: Dp = 0.dp,
|
topInset: Dp = 0.dp,
|
||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ fun ArtistDetailRoute(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ArtistDetailScreen(
|
fun ArtistDetailScreen(
|
||||||
artist: Artist?,
|
artist: Artist?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
artistName: String = "",
|
artistName: String = "",
|
||||||
isAlbumArtist: Boolean = false,
|
isAlbumArtist: Boolean = false,
|
||||||
albums: List<Album>,
|
albums: List<Album>,
|
||||||
@@ -232,7 +233,6 @@ fun ArtistDetailScreen(
|
|||||||
starred: Boolean = false,
|
starred: Boolean = false,
|
||||||
onToggleStar: (() -> Unit)? = null,
|
onToggleStar: (() -> Unit)? = null,
|
||||||
onShuffle: () -> Unit = {},
|
onShuffle: () -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val listState = rememberLazyGridState()
|
val listState = rememberLazyGridState()
|
||||||
val fill = rememberViewportFill(listState)
|
val fill = rememberViewportFill(listState)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ fun ArtistsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onArtistClick: (Artist) -> Unit,
|
onArtistClick: (Artist) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
filtered: Boolean = false,
|
filtered: Boolean = false,
|
||||||
onClearFilters: () -> Unit = {},
|
onClearFilters: () -> Unit = {},
|
||||||
@@ -52,7 +53,6 @@ fun ArtistsContent(
|
|||||||
topInset: Dp = 0.dp,
|
topInset: Dp = 0.dp,
|
||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
||||||
|
|||||||
@@ -386,6 +386,7 @@ fun LibraryScreen(
|
|||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
onSync: () -> Unit,
|
onSync: () -> Unit,
|
||||||
onOpenSettings: () -> Unit,
|
onOpenSettings: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onOpenSearch: () -> Unit = {},
|
onOpenSearch: () -> Unit = {},
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
onExitOffline: () -> Unit = {},
|
onExitOffline: () -> Unit = {},
|
||||||
@@ -409,7 +410,6 @@ fun LibraryScreen(
|
|||||||
playlistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
playlistDownloads: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
dismissals: Flow<Unit> = emptyFlow(),
|
dismissals: Flow<Unit> = emptyFlow(),
|
||||||
homeContent: @Composable (topInset: Dp, bottomInset: Dp) -> Unit = { _, _ -> },
|
homeContent: @Composable (topInset: Dp, bottomInset: Dp) -> Unit = { _, _ -> },
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val pagerState =
|
val pagerState =
|
||||||
rememberPagerState(
|
rememberPagerState(
|
||||||
@@ -831,6 +831,7 @@ private fun ListOptionsGlyph(modifier: Modifier = Modifier) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("FrequentlyChangingValue")
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
internal fun LibraryTabs(
|
internal fun LibraryTabs(
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ fun PlaylistDetailScreen(
|
|||||||
songs: LazyPagingItems<PlaylistSongItem>,
|
songs: LazyPagingItems<PlaylistSongItem>,
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
artwork: ArtworkColors?,
|
artwork: ArtworkColors?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
downloads: Map<String, SongDownload> = emptyMap(),
|
downloads: Map<String, SongDownload> = emptyMap(),
|
||||||
downloadStatus: ListDownloadStatus = ListDownloadStatus(),
|
downloadStatus: ListDownloadStatus = ListDownloadStatus(),
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
@@ -131,7 +132,6 @@ fun PlaylistDetailScreen(
|
|||||||
onPlay: () -> Unit = {},
|
onPlay: () -> Unit = {},
|
||||||
onMore: () -> Unit = {},
|
onMore: () -> Unit = {},
|
||||||
playingSongId: String? = null,
|
playingSongId: String? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
HeroDetailScaffold(
|
HeroDetailScaffold(
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ fun PlaylistsContent(
|
|||||||
coverArt: (String?, Boolean) -> CoverArtRef?,
|
coverArt: (String?, Boolean) -> CoverArtRef?,
|
||||||
bottomInset: Dp,
|
bottomInset: Dp,
|
||||||
onPlaylistClick: (Playlist) -> Unit,
|
onPlaylistClick: (Playlist) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
filtered: Boolean = false,
|
filtered: Boolean = false,
|
||||||
onClearFilters: () -> Unit = {},
|
onClearFilters: () -> Unit = {},
|
||||||
@@ -51,7 +52,6 @@ fun PlaylistsContent(
|
|||||||
topInset: Dp = 0.dp,
|
topInset: Dp = 0.dp,
|
||||||
onSync: () -> Unit = {},
|
onSync: () -> Unit = {},
|
||||||
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
downloadStatuses: Map<String, ListDownloadStatus> = emptyMap(),
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
when {
|
when {
|
||||||
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
items.itemCount == 0 && items.loadState.refresh is LoadState.Loading -> {
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ private fun buildVectorPath(vector: ImageVector): Path {
|
|||||||
@Composable
|
@Composable
|
||||||
fun SongRow(
|
fun SongRow(
|
||||||
song: Song,
|
song: Song,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
coverArtId: String? = null,
|
coverArtId: String? = null,
|
||||||
coverArt: ((String?, Boolean) -> CoverArtRef?)? = null,
|
coverArt: ((String?, Boolean) -> CoverArtRef?)? = null,
|
||||||
isPlaying: Boolean = false,
|
isPlaying: Boolean = false,
|
||||||
@@ -100,7 +101,6 @@ fun SongRow(
|
|||||||
durationSeconds: Long? = null,
|
durationSeconds: Long? = null,
|
||||||
download: SongDownload? = null,
|
download: SongDownload? = null,
|
||||||
trailingContent: (@Composable () -> Unit)? = null,
|
trailingContent: (@Composable () -> Unit)? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|||||||
@@ -60,10 +60,10 @@ fun MiniPlayer(
|
|||||||
onExpand: () -> Unit,
|
onExpand: () -> Unit,
|
||||||
onPlayPause: () -> Unit,
|
onPlayPause: () -> Unit,
|
||||||
onNext: () -> Unit,
|
onNext: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
progressInset: Boolean = false,
|
progressInset: Boolean = false,
|
||||||
onExpandDrag: (Float) -> Unit = {},
|
onExpandDrag: (Float) -> Unit = {},
|
||||||
onExpandRelease: (Float) -> Unit = {},
|
onExpandRelease: (Float) -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val item = state.item ?: return
|
val item = state.item ?: return
|
||||||
var dragUpPx by remember { mutableFloatStateOf(0f) }
|
var dragUpPx by remember { mutableFloatStateOf(0f) }
|
||||||
|
|||||||
@@ -182,9 +182,9 @@ fun NowPlayingRoute(
|
|||||||
onQueue: () -> Unit,
|
onQueue: () -> Unit,
|
||||||
onViewAlbum: (String) -> Unit,
|
onViewAlbum: (String) -> Unit,
|
||||||
onViewArtist: (String) -> Unit,
|
onViewArtist: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
contextMenuHost: ContextMenuHost? = null,
|
contextMenuHost: ContextMenuHost? = null,
|
||||||
setStar: (StarType, String, Boolean) -> Unit,
|
setStar: (StarType, String, Boolean) -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
controller: PlaybackController = koinInject(),
|
controller: PlaybackController = koinInject(),
|
||||||
libraryRepository: LibraryRepository = koinInject(),
|
libraryRepository: LibraryRepository = koinInject(),
|
||||||
downloadRepository: DownloadRepository = koinInject(),
|
downloadRepository: DownloadRepository = koinInject(),
|
||||||
@@ -286,6 +286,7 @@ fun NowPlayingScreen(
|
|||||||
onPlayPause: () -> Unit,
|
onPlayPause: () -> Unit,
|
||||||
onNext: () -> Unit,
|
onNext: () -> Unit,
|
||||||
onPrevious: () -> Unit,
|
onPrevious: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
onSkipPrevious: () -> Unit = onPrevious,
|
onSkipPrevious: () -> Unit = onPrevious,
|
||||||
onShuffle: () -> Unit = {},
|
onShuffle: () -> Unit = {},
|
||||||
onRepeat: () -> Unit = {},
|
onRepeat: () -> Unit = {},
|
||||||
@@ -298,7 +299,6 @@ fun NowPlayingScreen(
|
|||||||
thumbnailRef: CoverArtRef? = null,
|
thumbnailRef: CoverArtRef? = null,
|
||||||
previousArt: NowPlayingArt? = null,
|
previousArt: NowPlayingArt? = null,
|
||||||
nextArt: NowPlayingArt? = null,
|
nextArt: NowPlayingArt? = null,
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val playButtonSize = 90.dp
|
val playButtonSize = 90.dp
|
||||||
val playCircleDiameter = playButtonSize * 20f / 24f
|
val playCircleDiameter = playButtonSize * 20f / 24f
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import androidx.compose.material3.TopAppBar
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
@@ -106,10 +107,10 @@ class QueueViewModel(
|
|||||||
var ready by mutableStateOf(false)
|
var ready by mutableStateOf(false)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var initialIndex by mutableStateOf(0)
|
var initialIndex by mutableIntStateOf(0)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var generation by mutableStateOf(0)
|
var generation by mutableIntStateOf(0)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
private var nextId = 0L
|
private var nextId = 0L
|
||||||
@@ -562,6 +563,7 @@ internal fun dropTarget(
|
|||||||
return (before - if (before > from) 1 else 0) + 1
|
return (before - if (before > from) 1 else 0) + 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("ModifierParameter")
|
||||||
@Composable
|
@Composable
|
||||||
private fun QueueRowItem(
|
private fun QueueRowItem(
|
||||||
row: QueueRow,
|
row: QueueRow,
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ import org.koin.compose.koinInject
|
|||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
enum class SearchFilter(
|
enum class SearchFilter(
|
||||||
@StringRes val label: Int,
|
@param:StringRes val label: Int,
|
||||||
) {
|
) {
|
||||||
Songs(R.string.search_songs),
|
Songs(R.string.search_songs),
|
||||||
Albums(R.string.resources_album_name),
|
Albums(R.string.resources_album_name),
|
||||||
@@ -125,6 +125,7 @@ fun SearchScreen(
|
|||||||
query: String,
|
query: String,
|
||||||
onQueryChange: (String) -> Unit,
|
onQueryChange: (String) -> Unit,
|
||||||
results: SearchResults,
|
results: SearchResults,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
filters: Set<SearchFilter> = ALL_FILTERS,
|
filters: Set<SearchFilter> = ALL_FILTERS,
|
||||||
onToggleFilter: (SearchFilter) -> Unit = {},
|
onToggleFilter: (SearchFilter) -> Unit = {},
|
||||||
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
coverArt: (String?, Boolean) -> CoverArtRef? = { _, _ -> null },
|
||||||
@@ -135,7 +136,6 @@ fun SearchScreen(
|
|||||||
onPlaylistClick: (Playlist) -> Unit = {},
|
onPlaylistClick: (Playlist) -> Unit = {},
|
||||||
onSongClick: (Song) -> Unit = {},
|
onSongClick: (Song) -> Unit = {},
|
||||||
onLongClick: (MenuTarget) -> Unit = {},
|
onLongClick: (MenuTarget) -> Unit = {},
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
val backLabel = stringResource(R.string.navigation_back)
|
val backLabel = stringResource(R.string.navigation_back)
|
||||||
|
|||||||
@@ -90,8 +90,8 @@ fun AddSourceScreen(
|
|||||||
onSave: () -> Unit,
|
onSave: () -> Unit,
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
onBack: (() -> Unit)?,
|
onBack: (() -> Unit)?,
|
||||||
dismissals: Flow<Unit> = emptyFlow(),
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
dismissals: Flow<Unit> = emptyFlow(),
|
||||||
) {
|
) {
|
||||||
val nameFocus = remember { FocusRequester() }
|
val nameFocus = remember { FocusRequester() }
|
||||||
val addressFocus = remember { FocusRequester() }
|
val addressFocus = remember { FocusRequester() }
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ import androidx.compose.material.icons.rounded.Add
|
|||||||
import androidx.compose.material.icons.rounded.Description
|
import androidx.compose.material.icons.rounded.Description
|
||||||
import androidx.compose.material.icons.rounded.Download
|
import androidx.compose.material.icons.rounded.Download
|
||||||
import androidx.compose.material.icons.rounded.Edit
|
import androidx.compose.material.icons.rounded.Edit
|
||||||
|
import androidx.compose.material.icons.rounded.Favorite
|
||||||
|
import androidx.compose.material.icons.rounded.Info
|
||||||
|
import androidx.compose.material.icons.rounded.Language
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
@@ -41,6 +44,7 @@ import androidx.compose.material3.TextButton
|
|||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -49,10 +53,12 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.platform.LocalResources
|
import androidx.compose.ui.platform.LocalResources
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.subtracks.BuildConfig
|
||||||
import com.subtracks.R
|
import com.subtracks.R
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import com.subtracks.data.prefs.StreamQuality
|
import com.subtracks.data.prefs.StreamQuality
|
||||||
@@ -65,6 +71,9 @@ import org.koin.compose.viewmodel.koinViewModel
|
|||||||
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
|
private val bitrateOptions = listOf(0, 24, 32, 64, 96, 128, 192, 256, 320)
|
||||||
private val streamFormats = listOf(null, "mp3", "opus", "ogg", "webm", "aac", "flac")
|
private val streamFormats = listOf(null, "mp3", "opus", "ogg", "webm", "aac", "flac")
|
||||||
|
|
||||||
|
private const val PROJECT_HOMEPAGE = "https://github.com/austinried/subtracks"
|
||||||
|
private const val SUPPORT_URL = "https://ko-fi.com/austinried"
|
||||||
|
|
||||||
private enum class SettingsDialog { WifiQuality, MobileQuality, DownloadQuality, SyncConcurrency }
|
private enum class SettingsDialog { WifiQuality, MobileQuality, DownloadQuality, SyncConcurrency }
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -124,6 +133,7 @@ fun SettingsScreen(
|
|||||||
downloadQuality: StreamQuality,
|
downloadQuality: StreamQuality,
|
||||||
downloadOverMetered: Boolean,
|
downloadOverMetered: Boolean,
|
||||||
scrobbling: Boolean,
|
scrobbling: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
offline: Boolean = false,
|
offline: Boolean = false,
|
||||||
onSelectSource: (Long) -> Unit,
|
onSelectSource: (Long) -> Unit,
|
||||||
onEditServer: (Long) -> Unit,
|
onEditServer: (Long) -> Unit,
|
||||||
@@ -139,12 +149,12 @@ fun SettingsScreen(
|
|||||||
onOpenLicenses: () -> Unit,
|
onOpenLicenses: () -> Unit,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
dismissals: Flow<Unit> = emptyFlow(),
|
dismissals: Flow<Unit> = emptyFlow(),
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
) {
|
) {
|
||||||
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
|
var dialog by remember { mutableStateOf<SettingsDialog?>(null) }
|
||||||
DismissOnRequest(dismissals) { dialog = null }
|
DismissOnRequest(dismissals) { dialog = null }
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val fill = rememberViewportFill(listState)
|
val fill = rememberViewportFill(listState)
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
@@ -312,6 +322,16 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { SectionHeader(stringResource(R.string.settings_about_name)) }
|
item { SectionHeader(stringResource(R.string.settings_about_name)) }
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.app_name)) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(stringResource(R.string.settings_about_version, BuildConfig.VERSION_NAME))
|
||||||
|
},
|
||||||
|
leadingContent = { Icon(Icons.Rounded.Info, contentDescription = null) },
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
item {
|
item {
|
||||||
ListItem(
|
ListItem(
|
||||||
headlineContent = { Text(stringResource(R.string.settings_about_actions_licenses)) },
|
headlineContent = { Text(stringResource(R.string.settings_about_actions_licenses)) },
|
||||||
@@ -320,6 +340,30 @@ fun SettingsScreen(
|
|||||||
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.settings_about_actions_project_homepage)) },
|
||||||
|
supportingContent = { Text(PROJECT_HOMEPAGE) },
|
||||||
|
leadingContent = { Icon(Icons.Rounded.Language, contentDescription = null) },
|
||||||
|
modifier = Modifier.clickable { runCatching { uriHandler.openUri(PROJECT_HOMEPAGE) } },
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
headlineContent = { Text(stringResource(R.string.settings_about_actions_support)) },
|
||||||
|
supportingContent = { Text(SUPPORT_URL) },
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
Icons.Rounded.Favorite,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier.clickable { runCatching { uriHandler.openUri(SUPPORT_URL) } },
|
||||||
|
colors = ListItemDefaults.colors(containerColor = Color.Transparent),
|
||||||
|
)
|
||||||
|
}
|
||||||
item { Spacer(Modifier.height(fill)) }
|
item { Spacer(Modifier.height(fill)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -397,7 +441,7 @@ private fun ConcurrencyDialog(
|
|||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val resources = LocalResources.current
|
val resources = LocalResources.current
|
||||||
var draft by remember(selected) { mutableStateOf(selected) }
|
var draft by remember(selected) { mutableIntStateOf(selected) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = { Text(stringResource(R.string.sync_concurrency)) },
|
title = { Text(stringResource(R.string.sync_concurrency)) },
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.luminance
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.core.graphics.get
|
||||||
import androidx.palette.graphics.Palette
|
import androidx.palette.graphics.Palette
|
||||||
import coil3.SingletonImageLoader
|
import coil3.SingletonImageLoader
|
||||||
import coil3.compose.LocalPlatformContext
|
import coil3.compose.LocalPlatformContext
|
||||||
@@ -202,7 +203,7 @@ object ArtworkSeedCache {
|
|||||||
x: Int,
|
x: Int,
|
||||||
y: Int,
|
y: Int,
|
||||||
): Float {
|
): Float {
|
||||||
val color = Color(bitmap.getPixel(x, y))
|
val color = Color(bitmap[x, y])
|
||||||
return 0.2126f * color.red + 0.7152f * color.green + 0.0722f * color.blue
|
return 0.2126f * color.red + 0.7152f * color.green + 0.0722f * color.blue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 441 B |
|
Before Width: | Height: | Size: 401 B |
|
Before Width: | Height: | Size: 318 B |
|
Before Width: | Height: | Size: 284 B |
|
Before Width: | Height: | Size: 600 B |
|
Before Width: | Height: | Size: 575 B |
|
Before Width: | Height: | Size: 925 B |
|
Before Width: | Height: | Size: 921 B |
@@ -1,8 +1,10 @@
|
|||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:width="24dp"
|
android:width="24dp"
|
||||||
android:height="24dp"
|
android:height="24dp"
|
||||||
android:viewportWidth="300"
|
android:viewportWidth="300"
|
||||||
android:viewportHeight="300">
|
android:viewportHeight="300"
|
||||||
|
tools:ignore="UnusedResources">
|
||||||
<group android:translateY="40.25">
|
<group android:translateY="40.25">
|
||||||
<path
|
<path
|
||||||
android:pathData="m23.84,-0c-12.76,0 -23.44,13 -23.65,25.12 -0.42,79.03 0,160.73 0,170.4 0,12.59 9.8,23.99 23.41,23.99l253.58,0c11.4,0 22.66,-12.38 22.66,-22.79L299.84,24.12c0,-10.8 -10.24,-23.81 -21.84,-23.81 -11.6,0 -254.16,-0.31 -254.16,-0.31zM39.05,25.28c0,0 217.71,0.71 223.67,0.71 6.07,0 12.17,6.83 12.17,12.06l0,142.25c0,8.97 -6.97,14.63 -12.48,14.64l-33.42,0.07c-5.32,0.01 -9.93,-7.19 -11.35,-10.61 -1.89,-4.55 -11.08,-25.75 -12.1,-28.1 -0.6,-1.38 -2.9,-2.83 -5.89,-2.83L98.33,153.45c-2.68,0 -4.89,2.87 -5.67,4.71 -1.12,2.66 -9.02,21.39 -11.63,27.57 -2.73,6.46 -4.37,9.06 -11.47,9.06l-30.97,0c-7.44,0 -13.69,-7.17 -13.67,-15.18l0.28,-139.51c0,-6.88 5.54,-14.83 13.85,-14.83z"
|
android:pathData="m23.84,-0c-12.76,0 -23.44,13 -23.65,25.12 -0.42,79.03 0,160.73 0,170.4 0,12.59 9.8,23.99 23.41,23.99l253.58,0c11.4,0 22.66,-12.38 22.66,-22.79L299.84,24.12c0,-10.8 -10.24,-23.81 -21.84,-23.81 -11.6,0 -254.16,-0.31 -254.16,-0.31zM39.05,25.28c0,0 217.71,0.71 223.67,0.71 6.07,0 12.17,6.83 12.17,12.06l0,142.25c0,8.97 -6.97,14.63 -12.48,14.64l-33.42,0.07c-5.32,0.01 -9.93,-7.19 -11.35,-10.61 -1.89,-4.55 -11.08,-25.75 -12.1,-28.1 -0.6,-1.38 -2.9,-2.83 -5.89,-2.83L98.33,153.45c-2.68,0 -4.89,2.87 -5.67,4.71 -1.12,2.66 -9.02,21.39 -11.63,27.57 -2.73,6.46 -4.37,9.06 -11.47,9.06l-30.97,0c-7.44,0 -13.69,-7.17 -13.67,-15.18l0.28,-139.51c0,-6.88 5.54,-14.83 13.85,-14.83z"
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
<string name="resources_sort_by_year">حسب السنة</string>
|
<string name="resources_sort_by_year">حسب السنة</string>
|
||||||
<string name="search_input_placeholder">بحث</string>
|
<string name="search_input_placeholder">بحث</string>
|
||||||
<string name="settings_about_actions_licenses">الرخص</string>
|
<string name="settings_about_actions_licenses">الرخص</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">موقع المشروع</string>
|
||||||
<string name="settings_about_name">حول</string>
|
<string name="settings_about_name">حول</string>
|
||||||
|
<string name="settings_about_version">الإصدار %1$s</string>
|
||||||
<string name="settings_network_name">الشبكة</string>
|
<string name="settings_network_name">الشبكة</string>
|
||||||
<string name="settings_network_values_kbps">%1$s كيلو بايت في الثانية</string>
|
<string name="settings_network_values_kbps">%1$s كيلو بايت في الثانية</string>
|
||||||
<string name="settings_network_values_unlimited_kbps">غير محدود</string>
|
<string name="settings_network_values_unlimited_kbps">غير محدود</string>
|
||||||
|
|||||||
@@ -10,8 +10,8 @@
|
|||||||
<string name="navigation_tabs_library">Biblioteca</string>
|
<string name="navigation_tabs_library">Biblioteca</string>
|
||||||
<string name="navigation_tabs_settings">Configurações</string>
|
<string name="navigation_tabs_settings">Configurações</string>
|
||||||
<plurals name="resources_album_count">
|
<plurals name="resources_album_count">
|
||||||
<item quantity="one">%1$d album</item>
|
<item quantity="one">%1$d álbum</item>
|
||||||
<item quantity="other">%1$d albums</item>
|
<item quantity="other">%1$d álbuns</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="resources_filter_album">Álbum</string>
|
<string name="resources_filter_album">Álbum</string>
|
||||||
<string name="resources_filter_artist">Artista</string>
|
<string name="resources_filter_artist">Artista</string>
|
||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Ano</string>
|
<string name="resources_sort_by_year">Ano</string>
|
||||||
<string name="search_input_placeholder">Pesquisar</string>
|
<string name="search_input_placeholder">Pesquisar</string>
|
||||||
<string name="settings_about_actions_licenses">Licenças</string>
|
<string name="settings_about_actions_licenses">Licenças</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Página do projeto</string>
|
||||||
|
<string name="settings_about_actions_support">Apoie o desenvolvedor</string>
|
||||||
<string name="settings_about_name">Sobre</string>
|
<string name="settings_about_name">Sobre</string>
|
||||||
|
<string name="settings_about_version">Versão %1$s</string>
|
||||||
<string name="settings_network_name">Rede</string>
|
<string name="settings_network_name">Rede</string>
|
||||||
<string name="settings_network_options_offline_mode">Modo off-line</string>
|
<string name="settings_network_options_offline_mode">Modo off-line</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Usar a internet para sincronizar as músicas.</string>
|
<string name="settings_network_options_offline_mode_off">Usar a internet para sincronizar as músicas.</string>
|
||||||
@@ -42,7 +45,7 @@
|
|||||||
<string name="settings_servers_fields_password">Senha</string>
|
<string name="settings_servers_fields_password">Senha</string>
|
||||||
<string name="settings_servers_fields_username">Usuário</string>
|
<string name="settings_servers_fields_username">Usuário</string>
|
||||||
<string name="navigation_tabs_home">Home</string>
|
<string name="navigation_tabs_home">Home</string>
|
||||||
<string name="resources_album_name">Albums</string>
|
<string name="resources_album_name">Álbuns</string>
|
||||||
<string name="resources_artist_name">Artists</string>
|
<string name="resources_artist_name">Artists</string>
|
||||||
<string name="resources_playlist_name">Playlists</string>
|
<string name="resources_playlist_name">Playlists</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@
|
|||||||
<string name="resources_sort_by_year">根据年份</string>
|
<string name="resources_sort_by_year">根据年份</string>
|
||||||
<string name="search_input_placeholder">搜索</string>
|
<string name="search_input_placeholder">搜索</string>
|
||||||
<string name="settings_about_actions_licenses">许可</string>
|
<string name="settings_about_actions_licenses">许可</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">项目地址</string>
|
||||||
|
<string name="settings_about_actions_support">支持开发者</string>
|
||||||
<string name="settings_about_name">关于</string>
|
<string name="settings_about_name">关于</string>
|
||||||
|
<string name="settings_about_version">版本 %1$s</string>
|
||||||
<string name="settings_network_name">网络</string>
|
<string name="settings_network_name">网络</string>
|
||||||
<string name="settings_network_options_offline_mode">离线模式</string>
|
<string name="settings_network_options_offline_mode">离线模式</string>
|
||||||
<string name="settings_network_options_offline_mode_off">使用互联网同步音乐。</string>
|
<string name="settings_network_options_offline_mode_off">使用互联网同步音乐。</string>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@
|
|||||||
<string name="resources_sort_by_year">根據年份</string>
|
<string name="resources_sort_by_year">根據年份</string>
|
||||||
<string name="search_input_placeholder">搜尋</string>
|
<string name="search_input_placeholder">搜尋</string>
|
||||||
<string name="settings_about_actions_licenses">許可</string>
|
<string name="settings_about_actions_licenses">許可</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">專案首頁</string>
|
||||||
|
<string name="settings_about_actions_support">支援開發者</string>
|
||||||
<string name="settings_about_name">關於</string>
|
<string name="settings_about_name">關於</string>
|
||||||
|
<string name="settings_about_version">版本 %1$s</string>
|
||||||
<string name="settings_network_name">網路</string>
|
<string name="settings_network_name">網路</string>
|
||||||
<string name="settings_network_options_offline_mode">離線模式</string>
|
<string name="settings_network_options_offline_mode">離線模式</string>
|
||||||
<string name="settings_network_options_offline_mode_off">使用網際網路同步音樂。</string>
|
<string name="settings_network_options_offline_mode_off">使用網際網路同步音樂。</string>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
<string name="resources_sort_by_year">Per any</string>
|
<string name="resources_sort_by_year">Per any</string>
|
||||||
<string name="search_input_placeholder">Cercar</string>
|
<string name="search_input_placeholder">Cercar</string>
|
||||||
<string name="settings_about_actions_licenses">Llicències</string>
|
<string name="settings_about_actions_licenses">Llicències</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Pàgina d\'inici del projecte</string>
|
||||||
<string name="settings_about_name">Quant a</string>
|
<string name="settings_about_name">Quant a</string>
|
||||||
|
<string name="settings_about_version">versió %1$s</string>
|
||||||
<string name="settings_network_name">Xarxa</string>
|
<string name="settings_network_name">Xarxa</string>
|
||||||
<string name="settings_network_values_kbps">%1$s kbps</string>
|
<string name="settings_network_values_kbps">%1$s kbps</string>
|
||||||
<string name="settings_network_values_unlimited_kbps">Il·limitat</string>
|
<string name="settings_network_values_unlimited_kbps">Il·limitat</string>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
<string name="resources_sort_by_year">Roku</string>
|
<string name="resources_sort_by_year">Roku</string>
|
||||||
<string name="search_input_placeholder">Hledat</string>
|
<string name="search_input_placeholder">Hledat</string>
|
||||||
<string name="settings_about_actions_licenses">Licence</string>
|
<string name="settings_about_actions_licenses">Licence</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Stránka projektu</string>
|
||||||
|
<string name="settings_about_actions_support">Podpořit vývojáře</string>
|
||||||
<string name="settings_about_name">O aplikaci</string>
|
<string name="settings_about_name">O aplikaci</string>
|
||||||
|
<string name="settings_about_version">verze %1$s</string>
|
||||||
<string name="settings_network_name">Síť</string>
|
<string name="settings_network_name">Síť</string>
|
||||||
<string name="settings_network_options_stream_format_server_default">Použít nastavení serveru</string>
|
<string name="settings_network_options_stream_format_server_default">Použít nastavení serveru</string>
|
||||||
<string name="settings_network_values_kbps">%1$skbps</string>
|
<string name="settings_network_values_kbps">%1$skbps</string>
|
||||||
|
|||||||
@@ -12,7 +12,9 @@
|
|||||||
<string name="resources_sort_by_year">Efter år</string>
|
<string name="resources_sort_by_year">Efter år</string>
|
||||||
<string name="search_input_placeholder">Søg</string>
|
<string name="search_input_placeholder">Søg</string>
|
||||||
<string name="settings_about_actions_licenses">Licenser</string>
|
<string name="settings_about_actions_licenses">Licenser</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Projekt hjemmeside</string>
|
||||||
<string name="settings_about_name">Omkring</string>
|
<string name="settings_about_name">Omkring</string>
|
||||||
|
<string name="settings_about_version">version %1$s</string>
|
||||||
<string name="settings_network_name">Netværk</string>
|
<string name="settings_network_name">Netværk</string>
|
||||||
<string name="settings_network_values_kbps">%1$skbps</string>
|
<string name="settings_network_values_kbps">%1$skbps</string>
|
||||||
<string name="settings_network_values_unlimited_kbps">Ubegrænset</string>
|
<string name="settings_network_values_unlimited_kbps">Ubegrænset</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Nach Jahr</string>
|
<string name="resources_sort_by_year">Nach Jahr</string>
|
||||||
<string name="search_input_placeholder">Suche</string>
|
<string name="search_input_placeholder">Suche</string>
|
||||||
<string name="settings_about_actions_licenses">Lizenzen</string>
|
<string name="settings_about_actions_licenses">Lizenzen</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Projektseite</string>
|
||||||
|
<string name="settings_about_actions_support">Den Entwickler unterstützen</string>
|
||||||
<string name="settings_about_name">Über</string>
|
<string name="settings_about_name">Über</string>
|
||||||
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
<string name="settings_network_name">Netzwerk</string>
|
<string name="settings_network_name">Netzwerk</string>
|
||||||
<string name="settings_network_options_offline_mode">Offline Modus</string>
|
<string name="settings_network_options_offline_mode">Offline Modus</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Nutze das Internet um Musik zu synchronisieren.</string>
|
<string name="settings_network_options_offline_mode_off">Nutze das Internet um Musik zu synchronisieren.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Por Año</string>
|
<string name="resources_sort_by_year">Por Año</string>
|
||||||
<string name="search_input_placeholder">Buscar</string>
|
<string name="search_input_placeholder">Buscar</string>
|
||||||
<string name="settings_about_actions_licenses">Licencias</string>
|
<string name="settings_about_actions_licenses">Licencias</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Página de inicio del proyecto</string>
|
||||||
|
<string name="settings_about_actions_support">Apoya al desarrollador</string>
|
||||||
<string name="settings_about_name">Información</string>
|
<string name="settings_about_name">Información</string>
|
||||||
|
<string name="settings_about_version">versión %1$s</string>
|
||||||
<string name="settings_network_name">Sitio</string>
|
<string name="settings_network_name">Sitio</string>
|
||||||
<string name="settings_network_options_offline_mode">Modo sin conexión</string>
|
<string name="settings_network_options_offline_mode">Modo sin conexión</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Se usa internet para sincronizar música.</string>
|
<string name="settings_network_options_offline_mode_off">Se usa internet para sincronizar música.</string>
|
||||||
|
|||||||
@@ -28,7 +28,10 @@
|
|||||||
<string name="resources_sort_by_year">Urtearen arabera</string>
|
<string name="resources_sort_by_year">Urtearen arabera</string>
|
||||||
<string name="search_input_placeholder">Bilatu</string>
|
<string name="search_input_placeholder">Bilatu</string>
|
||||||
<string name="settings_about_actions_licenses">Lizentziak</string>
|
<string name="settings_about_actions_licenses">Lizentziak</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Proiektuaren webgunea</string>
|
||||||
|
<string name="settings_about_actions_support">Lagundu garatzaileari</string>
|
||||||
<string name="settings_about_name">Honi buruz</string>
|
<string name="settings_about_name">Honi buruz</string>
|
||||||
|
<string name="settings_about_version">%1$s bertsioa</string>
|
||||||
<string name="settings_network_name">Sarea</string>
|
<string name="settings_network_name">Sarea</string>
|
||||||
<string name="navigation_tabs_home">Hasiera</string>
|
<string name="navigation_tabs_home">Hasiera</string>
|
||||||
<string name="resources_album_name">Albumak</string>
|
<string name="resources_album_name">Albumak</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Année</string>
|
<string name="resources_sort_by_year">Année</string>
|
||||||
<string name="search_input_placeholder">Recherche</string>
|
<string name="search_input_placeholder">Recherche</string>
|
||||||
<string name="settings_about_actions_licenses">Licences</string>
|
<string name="settings_about_actions_licenses">Licences</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Page d\'accueil du projet</string>
|
||||||
|
<string name="settings_about_actions_support">Soutenir le développeur</string>
|
||||||
<string name="settings_about_name">À propos</string>
|
<string name="settings_about_name">À propos</string>
|
||||||
|
<string name="settings_about_version">version %1$s</string>
|
||||||
<string name="settings_network_name">Réseau</string>
|
<string name="settings_network_name">Réseau</string>
|
||||||
<string name="settings_network_options_offline_mode">Mode hors-ligne</string>
|
<string name="settings_network_options_offline_mode">Mode hors-ligne</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Se connecter à internet pour synchroniser la musique.</string>
|
<string name="settings_network_options_offline_mode_off">Se connecter à internet pour synchroniser la musique.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Por ano</string>
|
<string name="resources_sort_by_year">Por ano</string>
|
||||||
<string name="search_input_placeholder">Buscar</string>
|
<string name="search_input_placeholder">Buscar</string>
|
||||||
<string name="settings_about_actions_licenses">Licenzas</string>
|
<string name="settings_about_actions_licenses">Licenzas</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Web do Proxecto</string>
|
||||||
|
<string name="settings_about_actions_support">Axuda ao desenvolvemento</string>
|
||||||
<string name="settings_about_name">Acerca de</string>
|
<string name="settings_about_name">Acerca de</string>
|
||||||
|
<string name="settings_about_version">versión %1$s</string>
|
||||||
<string name="settings_network_name">Rede</string>
|
<string name="settings_network_name">Rede</string>
|
||||||
<string name="settings_network_options_offline_mode">Modo sen conexión</string>
|
<string name="settings_network_options_offline_mode">Modo sen conexión</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Usa internet para sincr. música.</string>
|
<string name="settings_network_options_offline_mode_off">Usa internet para sincr. música.</string>
|
||||||
|
|||||||
@@ -24,7 +24,10 @@
|
|||||||
<string name="resources_sort_by_year">Év</string>
|
<string name="resources_sort_by_year">Év</string>
|
||||||
<string name="search_input_placeholder">Keresés</string>
|
<string name="search_input_placeholder">Keresés</string>
|
||||||
<string name="settings_about_actions_licenses">Licenszek</string>
|
<string name="settings_about_actions_licenses">Licenszek</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Projekt weboldal</string>
|
||||||
|
<string name="settings_about_actions_support">Támogasd a fejlesztőt</string>
|
||||||
<string name="settings_about_name">Az alkalmazásról</string>
|
<string name="settings_about_name">Az alkalmazásról</string>
|
||||||
|
<string name="settings_about_version">%1$s verzió</string>
|
||||||
<string name="settings_network_name">Hálózat</string>
|
<string name="settings_network_name">Hálózat</string>
|
||||||
<string name="settings_network_options_offline_mode">Offline mód</string>
|
<string name="settings_network_options_offline_mode">Offline mód</string>
|
||||||
<string name="settings_network_options_stream_format_server_default">Szerver alapértelmezés használata</string>
|
<string name="settings_network_options_stream_format_server_default">Szerver alapértelmezés használata</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Per anno</string>
|
<string name="resources_sort_by_year">Per anno</string>
|
||||||
<string name="search_input_placeholder">Ricerca</string>
|
<string name="search_input_placeholder">Ricerca</string>
|
||||||
<string name="settings_about_actions_licenses">Licenze</string>
|
<string name="settings_about_actions_licenses">Licenze</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Pagina principale del progetto</string>
|
||||||
|
<string name="settings_about_actions_support">Sostieni lo sviluppatore</string>
|
||||||
<string name="settings_about_name">Informazioni</string>
|
<string name="settings_about_name">Informazioni</string>
|
||||||
|
<string name="settings_about_version">versione %1$s</string>
|
||||||
<string name="settings_network_name">Rete</string>
|
<string name="settings_network_name">Rete</string>
|
||||||
<string name="settings_network_options_offline_mode">Modalità Offline</string>
|
<string name="settings_network_options_offline_mode">Modalità Offline</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Usa internet per sincronizzare la musica.</string>
|
<string name="settings_network_options_offline_mode_off">Usa internet per sincronizzare la musica.</string>
|
||||||
|
|||||||
@@ -19,4 +19,5 @@
|
|||||||
<string name="resources_album_name">アルバム</string>
|
<string name="resources_album_name">アルバム</string>
|
||||||
<string name="resources_artist_name">アーティスト</string>
|
<string name="resources_artist_name">アーティスト</string>
|
||||||
<string name="resources_playlist_name">プレイリスト</string>
|
<string name="resources_playlist_name">プレイリスト</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">ホームページ</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -27,7 +27,10 @@
|
|||||||
<string name="resources_sort_by_year">년</string>
|
<string name="resources_sort_by_year">년</string>
|
||||||
<string name="search_input_placeholder">검색</string>
|
<string name="search_input_placeholder">검색</string>
|
||||||
<string name="settings_about_actions_licenses">저작권</string>
|
<string name="settings_about_actions_licenses">저작권</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">프로젝트 홈페이지</string>
|
||||||
|
<string name="settings_about_actions_support">개발자 지원하기</string>
|
||||||
<string name="settings_about_name">subtracks에 대하여</string>
|
<string name="settings_about_name">subtracks에 대하여</string>
|
||||||
|
<string name="settings_about_version">버전: %1$s</string>
|
||||||
<string name="settings_network_name">네트워크</string>
|
<string name="settings_network_name">네트워크</string>
|
||||||
<string name="settings_network_options_offline_mode">오프라인 모드</string>
|
<string name="settings_network_options_offline_mode">오프라인 모드</string>
|
||||||
<string name="settings_network_options_offline_mode_off">노래를 동기화하기 위해 인터넷을 사용합니다.</string>
|
<string name="settings_network_options_offline_mode_off">노래를 동기화하기 위해 인터넷을 사용합니다.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Etter år</string>
|
<string name="resources_sort_by_year">Etter år</string>
|
||||||
<string name="search_input_placeholder">Søk</string>
|
<string name="search_input_placeholder">Søk</string>
|
||||||
<string name="settings_about_actions_licenses">Lisenser</string>
|
<string name="settings_about_actions_licenses">Lisenser</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Prosjekthjemmeside</string>
|
||||||
|
<string name="settings_about_actions_support">Støtt utvikleren</string>
|
||||||
<string name="settings_about_name">Om</string>
|
<string name="settings_about_name">Om</string>
|
||||||
|
<string name="settings_about_version">versjon %1$s</string>
|
||||||
<string name="settings_network_name">Nettverk</string>
|
<string name="settings_network_name">Nettverk</string>
|
||||||
<string name="settings_network_options_offline_mode">Offline-modus</string>
|
<string name="settings_network_options_offline_mode">Offline-modus</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Bruk Internett til å synkronisere musikk.</string>
|
<string name="settings_network_options_offline_mode_off">Bruk Internett til å synkronisere musikk.</string>
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
<string name="resources_sort_by_year">ਸਾਲ ਦੁਆਰਾ</string>
|
<string name="resources_sort_by_year">ਸਾਲ ਦੁਆਰਾ</string>
|
||||||
<string name="search_input_placeholder">ਖੋਜੋ</string>
|
<string name="search_input_placeholder">ਖੋਜੋ</string>
|
||||||
<string name="settings_about_actions_licenses">ਲਾਇਸੰਸ</string>
|
<string name="settings_about_actions_licenses">ਲਾਇਸੰਸ</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">ਪ੍ਰੋਜੈਕਟ ਹੋਮਪੇਜ</string>
|
||||||
<string name="settings_about_name">ਬਾਰੇ</string>
|
<string name="settings_about_name">ਬਾਰੇ</string>
|
||||||
|
<string name="settings_about_version">ਸੰਸਕਰਣ %1$s</string>
|
||||||
<string name="settings_network_name">ਨੈੱਟਵਰਕ</string>
|
<string name="settings_network_name">ਨੈੱਟਵਰਕ</string>
|
||||||
<string name="settings_network_values_kbps">%1$skbps</string>
|
<string name="settings_network_values_kbps">%1$skbps</string>
|
||||||
<string name="settings_network_values_unlimited_kbps">ਅਸੀਮਿਤ</string>
|
<string name="settings_network_values_unlimited_kbps">ਅਸੀਮਿਤ</string>
|
||||||
|
|||||||
@@ -21,7 +21,9 @@
|
|||||||
<string name="resources_sort_by_year">Wg roku</string>
|
<string name="resources_sort_by_year">Wg roku</string>
|
||||||
<string name="search_input_placeholder">Szukaj</string>
|
<string name="search_input_placeholder">Szukaj</string>
|
||||||
<string name="settings_about_actions_licenses">Licencje</string>
|
<string name="settings_about_actions_licenses">Licencje</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Strona główna projektu</string>
|
||||||
<string name="settings_about_name">Informacje o projekcie</string>
|
<string name="settings_about_name">Informacje o projekcie</string>
|
||||||
|
<string name="settings_about_version">wersja %1$s</string>
|
||||||
<string name="settings_network_name">Sieć</string>
|
<string name="settings_network_name">Sieć</string>
|
||||||
<string name="settings_network_options_offline_mode">Tryb offline</string>
|
<string name="settings_network_options_offline_mode">Tryb offline</string>
|
||||||
<string name="settings_network_values_kbps">%1$skbps</string>
|
<string name="settings_network_values_kbps">%1$skbps</string>
|
||||||
|
|||||||
@@ -21,7 +21,10 @@
|
|||||||
<string name="resources_sort_by_year">Por Ano</string>
|
<string name="resources_sort_by_year">Por Ano</string>
|
||||||
<string name="search_input_placeholder">Procurar</string>
|
<string name="search_input_placeholder">Procurar</string>
|
||||||
<string name="settings_about_actions_licenses">Licenças</string>
|
<string name="settings_about_actions_licenses">Licenças</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Página do Projeto</string>
|
||||||
|
<string name="settings_about_actions_support">Apoie o programador</string>
|
||||||
<string name="settings_about_name">Acerca</string>
|
<string name="settings_about_name">Acerca</string>
|
||||||
|
<string name="settings_about_version">versão %1$s</string>
|
||||||
<string name="settings_network_name">Rede</string>
|
<string name="settings_network_name">Rede</string>
|
||||||
<string name="settings_network_options_offline_mode">Modo offline</string>
|
<string name="settings_network_options_offline_mode">Modo offline</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Usar a internet para sincronizar música.</string>
|
<string name="settings_network_options_offline_mode_off">Usar a internet para sincronizar música.</string>
|
||||||
|
|||||||
@@ -31,7 +31,10 @@
|
|||||||
<string name="resources_sort_by_year">По году</string>
|
<string name="resources_sort_by_year">По году</string>
|
||||||
<string name="search_input_placeholder">Поиск</string>
|
<string name="search_input_placeholder">Поиск</string>
|
||||||
<string name="settings_about_actions_licenses">Лицензии</string>
|
<string name="settings_about_actions_licenses">Лицензии</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Сайт проекта</string>
|
||||||
|
<string name="settings_about_actions_support">Поддержать разработчика</string>
|
||||||
<string name="settings_about_name">О Subtracks</string>
|
<string name="settings_about_name">О Subtracks</string>
|
||||||
|
<string name="settings_about_version">версия %1$s</string>
|
||||||
<string name="settings_network_name">Сеть</string>
|
<string name="settings_network_name">Сеть</string>
|
||||||
<string name="settings_network_options_offline_mode">Автономный режим</string>
|
<string name="settings_network_options_offline_mode">Автономный режим</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Использовать интернет для синхронизации музыки.</string>
|
<string name="settings_network_options_offline_mode_off">Использовать интернет для синхронизации музыки.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Efter år</string>
|
<string name="resources_sort_by_year">Efter år</string>
|
||||||
<string name="search_input_placeholder">Sök</string>
|
<string name="search_input_placeholder">Sök</string>
|
||||||
<string name="settings_about_actions_licenses">Licenser</string>
|
<string name="settings_about_actions_licenses">Licenser</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Projekthemsida</string>
|
||||||
|
<string name="settings_about_actions_support">Stöd utvecklaren</string>
|
||||||
<string name="settings_about_name">Om</string>
|
<string name="settings_about_name">Om</string>
|
||||||
|
<string name="settings_about_version">version %1$s</string>
|
||||||
<string name="settings_network_name">Nätverk</string>
|
<string name="settings_network_name">Nätverk</string>
|
||||||
<string name="settings_network_options_offline_mode">Offline-läge</string>
|
<string name="settings_network_options_offline_mode">Offline-läge</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Använd internet för att uppdatera musik.</string>
|
<string name="settings_network_options_offline_mode_off">Använd internet för att uppdatera musik.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">ஆண்டு</string>
|
<string name="resources_sort_by_year">ஆண்டு</string>
|
||||||
<string name="search_input_placeholder">தேடல்</string>
|
<string name="search_input_placeholder">தேடல்</string>
|
||||||
<string name="settings_about_actions_licenses">உரிமங்கள்</string>
|
<string name="settings_about_actions_licenses">உரிமங்கள்</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">திட்ட முகப்புப்பக்கம்</string>
|
||||||
|
<string name="settings_about_actions_support">டெவலப்பரை ஆதரிக்கவும்</string>
|
||||||
<string name="settings_about_name">பற்றி</string>
|
<string name="settings_about_name">பற்றி</string>
|
||||||
|
<string name="settings_about_version">பதிப்பு %1$s</string>
|
||||||
<string name="settings_network_name">பிணையம்</string>
|
<string name="settings_network_name">பிணையம்</string>
|
||||||
<string name="settings_network_options_offline_mode">இணைப்பில்லாத பயன்முறை</string>
|
<string name="settings_network_options_offline_mode">இணைப்பில்லாத பயன்முறை</string>
|
||||||
<string name="settings_network_options_offline_mode_off">இசையை ஒத்திசைக்க இணையத்தைப் பயன்படுத்தவும்.</string>
|
<string name="settings_network_options_offline_mode_off">இசையை ஒத்திசைக்க இணையத்தைப் பயன்படுத்தவும்.</string>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<string name="resources_filter_genre">ประเภท</string>
|
<string name="resources_filter_genre">ประเภท</string>
|
||||||
<string name="resources_filter_starred">ให้ดาว</string>
|
<string name="resources_filter_starred">ให้ดาว</string>
|
||||||
<plurals name="resources_song_count">
|
<plurals name="resources_song_count">
|
||||||
<item quantity="one">%1$d เพลง</item>
|
|
||||||
<item quantity="other">%1$d เพลง</item>
|
<item quantity="other">%1$d เพลง</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="resources_sort_by_artist">ศิลปิน</string>
|
<string name="resources_sort_by_artist">ศิลปิน</string>
|
||||||
@@ -28,7 +27,10 @@
|
|||||||
<string name="resources_sort_by_year">ปี</string>
|
<string name="resources_sort_by_year">ปี</string>
|
||||||
<string name="search_input_placeholder">ค้นหา</string>
|
<string name="search_input_placeholder">ค้นหา</string>
|
||||||
<string name="settings_about_actions_licenses">ใบอนุญาต</string>
|
<string name="settings_about_actions_licenses">ใบอนุญาต</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">โฮมเพจของโครงการ</string>
|
||||||
|
<string name="settings_about_actions_support">ให้การสนับสนุนผู้พัฒนา</string>
|
||||||
<string name="settings_about_name">เกี่ยวกับ</string>
|
<string name="settings_about_name">เกี่ยวกับ</string>
|
||||||
|
<string name="settings_about_version">เวอร์ชั่น %1$s</string>
|
||||||
<string name="settings_network_name">เคลือข่าย</string>
|
<string name="settings_network_name">เคลือข่าย</string>
|
||||||
<string name="settings_network_options_offline_mode">โหมดอ๊อฟไลน์</string>
|
<string name="settings_network_options_offline_mode">โหมดอ๊อฟไลน์</string>
|
||||||
<string name="settings_network_options_offline_mode_off">เปิดใช้อินเตอร์เน็ตเพื่อให้ข้อมูลเพลงเหมือนกัน</string>
|
<string name="settings_network_options_offline_mode_off">เปิดใช้อินเตอร์เน็ตเพื่อให้ข้อมูลเพลงเหมือนกัน</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Yıla göre</string>
|
<string name="resources_sort_by_year">Yıla göre</string>
|
||||||
<string name="search_input_placeholder">Ara</string>
|
<string name="search_input_placeholder">Ara</string>
|
||||||
<string name="settings_about_actions_licenses">Lisanslar</string>
|
<string name="settings_about_actions_licenses">Lisanslar</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Proje Ana Sayfası</string>
|
||||||
|
<string name="settings_about_actions_support">Geliştiriciyi destekleyin</string>
|
||||||
<string name="settings_about_name">Hakkında</string>
|
<string name="settings_about_name">Hakkında</string>
|
||||||
|
<string name="settings_about_version">sürüm %1$s</string>
|
||||||
<string name="settings_network_name">Ağ</string>
|
<string name="settings_network_name">Ağ</string>
|
||||||
<string name="settings_network_options_offline_mode">Çevrim dışı mod</string>
|
<string name="settings_network_options_offline_mode">Çevrim dışı mod</string>
|
||||||
<string name="settings_network_options_offline_mode_off">İnterneti kullanarak müzikleri eşzamanla.</string>
|
<string name="settings_network_options_offline_mode_off">İnterneti kullanarak müzikleri eşzamanla.</string>
|
||||||
|
|||||||
@@ -29,7 +29,10 @@
|
|||||||
<string name="resources_sort_by_year">Рік</string>
|
<string name="resources_sort_by_year">Рік</string>
|
||||||
<string name="search_input_placeholder">Введіть заголовок/опис</string>
|
<string name="search_input_placeholder">Введіть заголовок/опис</string>
|
||||||
<string name="settings_about_actions_licenses">Ліцензії</string>
|
<string name="settings_about_actions_licenses">Ліцензії</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Головна сторінка проєкту</string>
|
||||||
|
<string name="settings_about_actions_support">Підтримайте розробника</string>
|
||||||
<string name="settings_about_name">Про застосунок</string>
|
<string name="settings_about_name">Про застосунок</string>
|
||||||
|
<string name="settings_about_version">версія %1$s</string>
|
||||||
<string name="settings_network_name">Мережа</string>
|
<string name="settings_network_name">Мережа</string>
|
||||||
<string name="settings_network_options_offline_mode">Автономний режим</string>
|
<string name="settings_network_options_offline_mode">Автономний режим</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Використовувати інтернет для синхронізації музики.</string>
|
<string name="settings_network_options_offline_mode_off">Використовувати інтернет для синхронізації музики.</string>
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
<string name="navigation_tabs_library">Thư viện</string>
|
<string name="navigation_tabs_library">Thư viện</string>
|
||||||
<string name="navigation_tabs_settings">Cài đặt</string>
|
<string name="navigation_tabs_settings">Cài đặt</string>
|
||||||
<plurals name="resources_album_count">
|
<plurals name="resources_album_count">
|
||||||
<item quantity="one">%1$d album</item>
|
|
||||||
<item quantity="other">%1$d album</item>
|
<item quantity="other">%1$d album</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="resources_filter_album">Theo album</string>
|
<string name="resources_filter_album">Theo album</string>
|
||||||
@@ -18,7 +17,6 @@
|
|||||||
<string name="resources_filter_genre">Theo thể loại</string>
|
<string name="resources_filter_genre">Theo thể loại</string>
|
||||||
<string name="resources_filter_starred">Theo yêu thích</string>
|
<string name="resources_filter_starred">Theo yêu thích</string>
|
||||||
<plurals name="resources_song_count">
|
<plurals name="resources_song_count">
|
||||||
<item quantity="one">%1$d bài hát</item>
|
|
||||||
<item quantity="other">%1$d bài hát</item>
|
<item quantity="other">%1$d bài hát</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="resources_sort_by_artist">Nghệ sĩ</string>
|
<string name="resources_sort_by_artist">Nghệ sĩ</string>
|
||||||
@@ -29,7 +27,10 @@
|
|||||||
<string name="resources_sort_by_year">Năm</string>
|
<string name="resources_sort_by_year">Năm</string>
|
||||||
<string name="search_input_placeholder">Tìm kiếm</string>
|
<string name="search_input_placeholder">Tìm kiếm</string>
|
||||||
<string name="settings_about_actions_licenses">Bản quyền</string>
|
<string name="settings_about_actions_licenses">Bản quyền</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Trang chủ dự án</string>
|
||||||
|
<string name="settings_about_actions_support">Hỗ trợ nhà phát triển</string>
|
||||||
<string name="settings_about_name">Về ứng dụng</string>
|
<string name="settings_about_name">Về ứng dụng</string>
|
||||||
|
<string name="settings_about_version">phiên bản %1$s</string>
|
||||||
<string name="settings_network_name">Mạng</string>
|
<string name="settings_network_name">Mạng</string>
|
||||||
<string name="settings_network_options_offline_mode">Chế độ ngoại tuyến</string>
|
<string name="settings_network_options_offline_mode">Chế độ ngoại tuyến</string>
|
||||||
<string name="settings_network_options_offline_mode_off">Đồng bộ nhạc qua mạng.</string>
|
<string name="settings_network_options_offline_mode_off">Đồng bộ nhạc qua mạng.</string>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
<string name="app_name">subtracks</string>
|
<string name="app_name">subtracks</string>
|
||||||
<string name="download_channel_description">Progress while songs download</string>
|
<string name="download_channel_description">Progress while songs download</string>
|
||||||
|
|
||||||
@@ -56,7 +56,10 @@
|
|||||||
<string name="search_input_placeholder">Search</string>
|
<string name="search_input_placeholder">Search</string>
|
||||||
|
|
||||||
<string name="settings_about_actions_licenses">Licenses</string>
|
<string name="settings_about_actions_licenses">Licenses</string>
|
||||||
|
<string name="settings_about_actions_project_homepage">Project homepage</string>
|
||||||
|
<string name="settings_about_actions_support">Support the developer</string>
|
||||||
<string name="settings_about_name">About</string>
|
<string name="settings_about_name">About</string>
|
||||||
|
<string name="settings_about_version">Version %1$s</string>
|
||||||
<string name="settings_music_name">Playback</string>
|
<string name="settings_music_name">Playback</string>
|
||||||
<string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string>
|
<string name="settings_music_options_scrobble_description_on">Send now playing and play counts</string>
|
||||||
<string name="settings_music_options_scrobble_title">Scrobble to server</string>
|
<string name="settings_music_options_scrobble_title">Scrobble to server</string>
|
||||||
@@ -124,7 +127,7 @@
|
|||||||
<string name="search">Search</string>
|
<string name="search">Search</string>
|
||||||
<string name="search_this_list">Search this list</string>
|
<string name="search_this_list">Search this list</string>
|
||||||
<string name="search_songs">Songs</string>
|
<string name="search_songs">Songs</string>
|
||||||
<string name="search_min_length">Type at least %1$d characters to search.</string>
|
<string name="search_min_length" tools:ignore="PluralsCandidate">Type at least %1$d characters to search.</string>
|
||||||
<string name="search_no_results">No results for “%1$s”.</string>
|
<string name="search_no_results">No results for “%1$s”.</string>
|
||||||
<string name="sort_by">Sort by</string>
|
<string name="sort_by">Sort by</string>
|
||||||
<string name="sort_ascending">Sort ascending</string>
|
<string name="sort_ascending">Sort ascending</string>
|
||||||
@@ -245,7 +248,7 @@
|
|||||||
<string name="error_server_unknown">Unknown error</string>
|
<string name="error_server_unknown">Unknown error</string>
|
||||||
<string name="error_server_malformed">Malformed response from the server</string>
|
<string name="error_server_malformed">Malformed response from the server</string>
|
||||||
<string name="error_server_truncated">Truncated response from the server</string>
|
<string name="error_server_truncated">Truncated response from the server</string>
|
||||||
<string name="error_library_too_large">Library is too large to sync: it exceeds %1$d rows</string>
|
<string name="error_library_too_large" tools:ignore="PluralsCandidate">Library is too large to sync: it exceeds %1$d rows</string>
|
||||||
<string name="error_http">HTTP %1$d</string>
|
<string name="error_http">HTTP %1$d</string>
|
||||||
|
|
||||||
<string name="home_section_recently_played_artists">Recently played artists</string>
|
<string name="home_section_recently_played_artists">Recently played artists</string>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<network-security-config>
|
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||||
<base-config cleartextTrafficPermitted="true">
|
<base-config
|
||||||
|
cleartextTrafficPermitted="true"
|
||||||
|
tools:ignore="InsecureBaseConfiguration">
|
||||||
<trust-anchors>
|
<trust-anchors>
|
||||||
<certificates src="system" />
|
<certificates src="system" />
|
||||||
<certificates src="user" />
|
<certificates
|
||||||
|
src="user"
|
||||||
|
tools:ignore="AcceptsUserCertificates" />
|
||||||
</trust-anchors>
|
</trust-anchors>
|
||||||
</base-config>
|
</base-config>
|
||||||
</network-security-config>
|
</network-security-config>
|
||||||
|
|||||||
@@ -34,17 +34,17 @@ class StringResourcesTest {
|
|||||||
val locale = parse(file)
|
val locale = parse(file)
|
||||||
locale.forEach { (name, resource) ->
|
locale.forEach { (name, resource) ->
|
||||||
val base = default[name]
|
val base = default[name]
|
||||||
assertTrue("$name in ${file.parentFile.name} is missing from values/strings.xml", base != null)
|
assertTrue("$name in ${file.parentFile?.name} is missing from values/strings.xml", base != null)
|
||||||
assertEquals("$name kind differs in ${file.parentFile.name}", base!!.kind, resource.kind)
|
assertEquals("$name kind differs in ${file.parentFile?.name}", base!!.kind, resource.kind)
|
||||||
if (resource.kind == "plurals") {
|
if (resource.kind == "plurals") {
|
||||||
assertTrue(
|
assertTrue(
|
||||||
"$name has an invalid quantity in ${file.parentFile.name}",
|
"$name has an invalid quantity in ${file.parentFile?.name}",
|
||||||
resource.quantities.all { it in validQuantities },
|
resource.quantities.all { it in validQuantities },
|
||||||
)
|
)
|
||||||
assertTrue("$name in ${file.parentFile.name} is missing the required 'other' quantity", "other" in resource.quantities)
|
assertTrue("$name in ${file.parentFile?.name} is missing the required 'other' quantity", "other" in resource.quantities)
|
||||||
}
|
}
|
||||||
assertEquals(
|
assertEquals(
|
||||||
"$name placeholders differ in ${file.parentFile.name}",
|
"$name placeholders differ in ${file.parentFile?.name}",
|
||||||
placeholders(base.text),
|
placeholders(base.text),
|
||||||
placeholders(resource.text),
|
placeholders(resource.text),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -275,8 +275,8 @@ class DemoScreenshotTest {
|
|||||||
private fun DemoData.MiniPlayerOverlay(
|
private fun DemoData.MiniPlayerOverlay(
|
||||||
song: Song,
|
song: Song,
|
||||||
album: Album,
|
album: Album,
|
||||||
progressInset: Boolean = false,
|
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
progressInset: Boolean = false,
|
||||||
) {
|
) {
|
||||||
MiniPlayer(
|
MiniPlayer(
|
||||||
state = playbackFor(song, album),
|
state = playbackFor(song, album),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Subtracks is a native Android client for Subsonic-compatible servers (Navidrome,
|
|||||||
## Goals and constraints
|
## Goals and constraints
|
||||||
|
|
||||||
- Talk to any Subsonic-compatible server over its REST API.
|
- Talk to any Subsonic-compatible server over its REST API.
|
||||||
- Behave well on a poor connection: a local Room mirror, paging, and (later) offline playback.
|
- Behave well on a poor connection: a local Room mirror, paging, and offline playback.
|
||||||
- F-Droid friendly: free/libre dependencies only, no telemetry, buildable from source without proprietary services.
|
- F-Droid friendly: free/libre dependencies only, no telemetry, buildable from source without proprietary services.
|
||||||
- `minSdk 24`. The app bundles its own SQLite so behaviour is identical across devices and does not depend on the platform version.
|
- `minSdk 24`. The app bundles its own SQLite so behaviour is identical across devices and does not depend on the platform version.
|
||||||
|
|
||||||
@@ -72,7 +72,7 @@ Compose UI -> ViewModel -> Repository -> MusicSource (remote, Subsonic)
|
|||||||
|
|
||||||
The library is one `LibraryScreen` with a pinned top bar: the current section as the title, a custom row of icon buttons (Albums, Artists, Playlists) on the left, and Sync and Settings icon actions on the right. The selected section is drawn as a filled box in the content colour with the icon inverted, like the Flutter app — this is a hand-rolled row, not Material `TabRow` (no underline indicator). Icons use the Material rounded variants. Tabs switch the content in place; album and playlist rows push a detail screen, and Settings is pushed from the top bar. The sync action doubles as the progress indicator while a sync runs. There is no bottom navigation. A mini player sits above the navigation bar across every screen whenever a track is loaded. It is a permanent part of the app shell (the screens above it consume the navigation-bar inset so nothing is padded twice); the Now Playing screen slides up over the whole shell as an overlay rather than being a nav destination, so the mini player never re-lays out during the transition.
|
The library is one `LibraryScreen` with a pinned top bar: the current section as the title, a custom row of icon buttons (Albums, Artists, Playlists) on the left, and Sync and Settings icon actions on the right. The selected section is drawn as a filled box in the content colour with the icon inverted, like the Flutter app — this is a hand-rolled row, not Material `TabRow` (no underline indicator). Icons use the Material rounded variants. Tabs switch the content in place; album and playlist rows push a detail screen, and Settings is pushed from the top bar. The sync action doubles as the progress indicator while a sync runs. There is no bottom navigation. A mini player sits above the navigation bar across every screen whenever a track is loaded. It is a permanent part of the app shell (the screens above it consume the navigation-bar inset so nothing is padded twice); the Now Playing screen slides up over the whole shell as an overlay rather than being a nav destination, so the mini player never re-lays out during the transition.
|
||||||
|
|
||||||
The Albums tab is a three-column grid of covers only — a later preference will toggle captions, and sorting will hang off a FAB, so Settings only manages servers and neither shows sort nor sync controls.
|
The Albums tab is a three-column grid of covers only. Sorting and filtering hang off a FAB, and Settings holds the server list and the network, playback, download and about sections rather than list sort or sync.
|
||||||
|
|
||||||
The default theme is Material You: on Android 12+ the root scheme is the wallpaper's dynamic dark palette, falling back to the Material 3 baseline dark scheme elsewhere. Screens that show cover art layer an artwork-derived palette (`ArtworkTheme`) on top, and when no art is available they fall back to the same theme colours rather than a fixed monochrome scheme.
|
The default theme is Material You: on Android 12+ the root scheme is the wallpaper's dynamic dark palette, falling back to the Material 3 baseline dark scheme elsewhere. Screens that show cover art layer an artwork-derived palette (`ArtworkTheme`) on top, and when no art is available they fall back to the same theme colours rather than a fixed monochrome scheme.
|
||||||
|
|
||||||
@@ -90,11 +90,11 @@ Because the resolver reports *which entry* a position falls in, `PlaybackState`
|
|||||||
|
|
||||||
Stream URLs come from `SourceRepository.streamUri`, which asks the active `SubsonicSource` for a freshly salted `stream` URL; because the URL changes every request, artwork is attached to the media item by identity (the `CoverArtRef` cache key) rather than by URL. The active source carries the stream quality (a max bitrate plus an optional transcode format) for the current network: `NetworkMonitor` reports Wi-Fi vs mobile (metered) and `UserPreferences` stores a separate `StreamQuality` per mode, so `SourceRepository` rebuilds the source when either the mode or a quality changes. `PlaybackController` observes that effective quality and reloads the window around the current position, preserving play/pause and the playback position, so a Wi-Fi↔mobile transition re-issues the stream URLs with the new settings (a no-op when the two modes share a quality). Transcoded responses are chunked with no length and (because ffmpeg cannot seek back in a stream) no MP3 duration header, so Media3 would treat them as live and ignore seeks; the server's `estimateContentLength` is no better, since its estimate under-counts and cuts the track short. So a transcoded URL carries a declared upper-bound length in its fragment (never sent to the server), `KnownLengthDataSource` hands that length to Media3 to keep the stream seekable without truncation, and each transcoded item is clipped to the library duration so the timeline ends where the audio does and the next track still advances.
|
Stream URLs come from `SourceRepository.streamUri`, which asks the active `SubsonicSource` for a freshly salted `stream` URL; because the URL changes every request, artwork is attached to the media item by identity (the `CoverArtRef` cache key) rather than by URL. The active source carries the stream quality (a max bitrate plus an optional transcode format) for the current network: `NetworkMonitor` reports Wi-Fi vs mobile (metered) and `UserPreferences` stores a separate `StreamQuality` per mode, so `SourceRepository` rebuilds the source when either the mode or a quality changes. `PlaybackController` observes that effective quality and reloads the window around the current position, preserving play/pause and the playback position, so a Wi-Fi↔mobile transition re-issues the stream URLs with the new settings (a no-op when the two modes share a quality). Transcoded responses are chunked with no length and (because ffmpeg cannot seek back in a stream) no MP3 duration header, so Media3 would treat them as live and ignore seeks; the server's `estimateContentLength` is no better, since its estimate under-counts and cuts the track short. So a transcoded URL carries a declared upper-bound length in its fragment (never sent to the server), `KnownLengthDataSource` hands that length to Media3 to keep the stream seekable without truncation, and each transcoded item is clipped to the library duration so the timeline ends where the audio does and the next track still advances.
|
||||||
|
|
||||||
The window is also the queue the media notification and Android Auto can browse — a deliberate trade-off, since the alternative is holding an entire large library in memory. The queue view resolves positions lazily too: it loads the queue around the cursor in chunks and extends by a chunk when you scroll near either end, so it never materializes the whole queue. Dragging uses `sh.calvin.reorderable`, which floats the lifted row and reorders the loaded list live (animating with `animateItem`), so editing is what a drag needs: the view reorders its loaded list while dragging and commits one move on drop. `queue_entries` is rewritten to edit: removing a position splits its entry into up to two ranges, and moving one turns the moved track into an explicit song entry inserted at the target index, so an edit costs a couple of rows rather than a copy of the queue. When the edit falls inside the player's window the controller mirrors it with `removeMediaItem`/`moveMediaItem` so playback is not interrupted; removing the playing track, or a move that crosses the window boundary, reloads the window around the cursor. The controller keeps the pre-edit entries and cursor, so the queue view can offer a one-step undo. The up-next block is fully editable in the same view (drag reorder and delete), while the context is read-only while shuffled. Timed lyrics are not wired yet.
|
The window is also the queue the media notification browses — a deliberate trade-off, since the alternative is holding an entire large library in memory. The queue view resolves positions lazily too: it loads the queue around the cursor in chunks and extends by a chunk when you scroll near either end, so it never materializes the whole queue. Dragging uses `sh.calvin.reorderable`, which floats the lifted row and reorders the loaded list live (animating with `animateItem`), so editing is what a drag needs: the view reorders its loaded list while dragging and commits one move on drop. `queue_entries` is rewritten to edit: removing a position splits its entry into up to two ranges, and moving one turns the moved track into an explicit song entry inserted at the target index, so an edit costs a couple of rows rather than a copy of the queue. When the edit falls inside the player's window the controller mirrors it with `removeMediaItem`/`moveMediaItem` so playback is not interrupted; removing the playing track, or a move that crosses the window boundary, reloads the window around the cursor. The controller keeps the pre-edit entries and cursor, so the queue view can offer a one-step undo. The up-next block is fully editable in the same view (drag reorder and delete), while the context is read-only while shuffled. Timed lyrics are not wired yet.
|
||||||
|
|
||||||
Scrobbling is a `Scrobbler` that collects the controller's `state` and `positionMs` and drives the Subsonic `scrobble` endpoint: a now-playing notification (`submission=false`) when a track starts playing, and one submission (`submission=true`, with the playback start time) when the track passes half its duration or four minutes, whichever comes first. That split is the endpoint's own `submission` flag, and the threshold is the Last.fm rule the server forwards to; servers such as Navidrome trust the client and apply no threshold themselves. Tracks under 30 seconds are never submitted, and only time actually played counts: forward and backward seeks are ignored, and a rewind only starts a fresh listen when it lands in the track's first few seconds (a repeat or an explicit restart). The duration is the player's resolved value, falling back to the library row when the player has not reported one. Sending is decoupled from the collector so a slow or offline write cannot stall position tracking, while a lock keeps the writes in order. The policy is a pure state machine with no Android dependencies. A Settings toggle (`UserPreferences.scrobbling`, on by default) gates both calls. A submission also bumps the played song's local `playCount`/`played` and its album and artist ranks, so the play sorts move before the next sync; the sync then overwrites them with the server's own numbers, so a play the server never accepted is rolled back at that point.
|
Scrobbling is a `Scrobbler` that collects the controller's `state` and `positionMs` and drives the Subsonic `scrobble` endpoint: a now-playing notification (`submission=false`) when a track starts playing, and one submission (`submission=true`, with the playback start time) when the track passes half its duration or four minutes, whichever comes first. That split is the endpoint's own `submission` flag, and the threshold is the Last.fm rule the server forwards to; servers such as Navidrome trust the client and apply no threshold themselves. Tracks under 30 seconds are never submitted, and only time actually played counts: forward and backward seeks are ignored, and a rewind only starts a fresh listen when it lands in the track's first few seconds (a repeat or an explicit restart). The duration is the player's resolved value, falling back to the library row when the player has not reported one. Sending is decoupled from the collector so a slow or offline write cannot stall position tracking, while a lock keeps the writes in order. The policy is a pure state machine with no Android dependencies. A Settings toggle (`UserPreferences.scrobbling`, on by default) gates both calls. A submission also bumps the played song's local `playCount`/`played` and its album and artist ranks, so the play sorts move before the next sync; the sync then overwrites them with the server's own numbers, so a play the server never accepted is rolled back at that point.
|
||||||
|
|
||||||
Server writes — now-playing, scrobbles and stars — go through a `ServerActionSink`. The only implementation today is `NetworkServerActionSink`, which resolves the active source and calls it; a persistent, ordered outbox can replace it later so plays and stars taken offline are queued and replayed when the server is reachable. Now-playing is deliberately not durable (Last.fm says failed now-playing requests must not be retried), while submissions and stars carry everything a queue would need (the song or entity id and, for a play, the time it was listened).
|
Server writes — now-playing, scrobbles and stars — go through a `ServerActionSink`. `PendingServerActionSink` wraps `NetworkServerActionSink` (which resolves the active source and calls it) with a persistent, ordered outbox: a scrobble or star that fails on a retryable error, or that is taken while offline mode is on, is stored in `pending_actions` and replayed once offline mode is off and the network is back. Now-playing is deliberately not durable (Last.fm says failed now-playing requests must not be retried), while submissions and stars carry everything the queue needs (the song or entity id and, for a play, the time it was listened). A server that answers and refuses drops the action rather than blocking the queue.
|
||||||
|
|
||||||
The media notification carries a star button for the playing track. `PlaybackService` adds a `com.subtracks.STAR` session command and a `CommandButton` to the `MediaSession`, and drives its filled/unfilled icon from the playing song's Room row. That path — `MediaSession.Callback`, `SessionCommand`, `CommandButton`, `SessionResult`/`SessionError` and `MediaSession.setMediaButtonPreferences` — is annotated `@UnstableApi` in Media3 1.11.1 and is therefore opted into explicitly: when bumping Media3, re-check those signatures and the notification's button resolution. The stable `setCustomLayout` is not a substitute here, because the notification manager only refreshes on `onMediaButtonPreferencesChanged`, so the icon would not update mid-playback.
|
The media notification carries a star button for the playing track. `PlaybackService` adds a `com.subtracks.STAR` session command and a `CommandButton` to the `MediaSession`, and drives its filled/unfilled icon from the playing song's Room row. That path — `MediaSession.Callback`, `SessionCommand`, `CommandButton`, `SessionResult`/`SessionError` and `MediaSession.setMediaButtonPreferences` — is annotated `@UnstableApi` in Media3 1.11.1 and is therefore opted into explicitly: when bumping Media3, re-check those signatures and the notification's button resolution. The stable `setCustomLayout` is not a substitute here, because the notification manager only refreshes on `onMediaButtonPreferencesChanged`, so the icon would not update mid-playback.
|
||||||
|
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
# Implementation plan
|
|
||||||
|
|
||||||
The rewrite is delivered in vertical slices. Each phase leaves the app in a working, testable state.
|
|
||||||
|
|
||||||
## Phase 0 - Foundation (done)
|
|
||||||
|
|
||||||
Nix devshell (JDK, Gradle, Android SDK, nushell, navidrome, gonic), Gradle Kotlin DSL at the repo root, a Compose skeleton, Roborazzi/Robolectric screenshot testing, and the carried-over launcher and notification assets. The app ships as `com.subtracks.next`.
|
|
||||||
|
|
||||||
## Phase 1 - Data foundation (done)
|
|
||||||
|
|
||||||
Everything needed to mirror a server library locally:
|
|
||||||
|
|
||||||
- `MusicSource` abstraction with `SubsonicClient`, `SubsonicXml` and `SubsonicSource`.
|
|
||||||
- Room 3 schema on bundled SQLite, so the SQLite version is identical on every device.
|
|
||||||
- `SyncService`: stream each entity in batches, upserting per batch and pruning the IDs the server no longer lists; the sync is not atomic.
|
|
||||||
- Unit tests (client, XML, sync), native integration tests against navidrome and gonic, and CI (`unit`, `lint`, `integration`) with warm caches.
|
|
||||||
|
|
||||||
## Phase 2 - UI vertical slice (done)
|
|
||||||
|
|
||||||
Goal: a usable app for browsing a server, end to end.
|
|
||||||
|
|
||||||
- Koin DI and DataStore-backed preferences (album sort); the unused `app_settings` Room table was removed in favour of DataStore.
|
|
||||||
- App shell: a root gate that shows the add-server flow until a server exists, then the tabbed library screen matching the Flutter app (icon-only tabs for albums, artists and playlists, with sync and settings actions), plus album and playlist detail screens and a monochrome theme.
|
|
||||||
- Source setup: name/address/username/password with token-auth toggle, a connection test, save-and-sync, plus server switching and removal in settings.
|
|
||||||
- Library browsing: albums (covers-only grid), artists and playlists paged from Room (`PagingSource`) and rendered with `LazyPagingItems`, with Coil cover art served from stable media URLs.
|
|
||||||
- Artist detail: tapping an artist opens their albums, newest first, and an album can be opened from there.
|
|
||||||
- Roborazzi screenshots for the new screens, rendered on demand for review (not committed).
|
|
||||||
|
|
||||||
Not done yet in this slice (deliberately): cover-art tonal colour extraction, long-press menus, editing an existing server, and per-section sort/filter controls (only album sort is wired).
|
|
||||||
|
|
||||||
## Phase 3 - Playback (done)
|
|
||||||
|
|
||||||
- (done) Media3/ExoPlayer playback service, queue and now-playing UI: a `MediaSessionService` plus a mini player and a Now Playing screen.
|
|
||||||
- (done) Media notifications and headset/Bluetooth controls, from the media session.
|
|
||||||
- (done) A SQL-backed queue of references (whole playlist / album / song, each with an optional ordinal range) resolved lazily, with only a bounded window in memory and in the player.
|
|
||||||
- (done) Streaming bitrate and preferred format from settings (`maxBitRate`/`format`, so the server transcodes as configured).
|
|
||||||
- (done) A queue view that lists the resolved queue, jumps to a track on tap, removes tracks and reorders them by dragging, with a one-step undo.
|
|
||||||
- (done) Adding to the queue from the library (play next / add to queue), backed by a separate, editable up-next list that plays between the current track and the rest of the context.
|
|
||||||
|
|
||||||
## Phase 4 - Search, playlists, offline (done)
|
|
||||||
|
|
||||||
- (done) Search: a substring filter on the library queries, scoped to the active tab, with a docked search field and per-tab sort/filter controls. Queries of three or more characters run through per-entity FTS5 trigram tables (external content, trigger-maintained), with the `instr` scan as the below-three-character fallback.
|
|
||||||
- (done) Offline mode and downloads.
|
|
||||||
|
|
||||||
## Phase 5 - Release
|
|
||||||
|
|
||||||
- Change `applicationId` from `com.subtracks.next` to `com.subtracks`.
|
|
||||||
- Signing, versioning and Fastlane metadata.
|
|
||||||
- F-Droid submission. Their buildserver must have Gradle >= 9.6 and SDK 37, which this project targets; do not rely on the Gradle wrapper there (fdroidserver deletes `gradlew` and uses its own Gradle).
|
|
||||||
|
|
||||||
## Scale hardening (done)
|
|
||||||
|
|
||||||
Follow-up work for very large libraries, playlists and long sessions, grouped so each block could be one PR. All blocks have landed.
|
|
||||||
|
|
||||||
- **Library read path.** Landed: the sort keys used by album/artist/playlist browsing are indexed (a denormalized `albumArtist` on `albums`, `NOCASE` text, one index per order). Row resolution seeks through a cached keyset cursor (`QueueRepository.keyedRows` over `QueueDao`'s `...From`/`...Before`/`...After` queries); the old offset query is kept only as the cold and far-jump fallback.
|
|
||||||
- **Shuffle.** Landed: the permutation is derived from `(seed, size)` (a seekable Feistel in `Shuffle.kt`) and positions resolve through the same positional lookup, so `shuffle_order` and the retained whole-order/whole-id arrays are gone. The context is read-only while shuffled; manual ordering goes through the up-next block.
|
|
||||||
- **Sync pipeline.** Landed: the prune diffs against a memory-bounded set of primitive `long` id hashes and walks the local ids in keyset pages; `fetchRanks` and the write-only rank columns are gone; playlists and the album-song fallback are fetched with a bounded, user-configurable concurrency; the page cap is raised to 20k pages and overrun fails with a "Library is too large to sync" error; unchanged rows are not rewritten (`upsertChanged`'s changed-only `WHERE`).
|
|
||||||
- **Queue view and edits.** Landed: the queue view loads a bounded window (`QUEUE_CHUNK * 3`) and extends and trims it as you scroll, its ViewModel is scoped to the overlay, adjacent same-ref ranges are compacted after an edit, and resolved entry lengths are cached.
|
|
||||||
- **Artwork.** Landed: `artwork_seeds` carries a `sourceId` with `ON DELETE CASCADE` and an index, is pruned as an LRU by `storedAt`, and the seed prefetch requests the thumbnail.
|
|
||||||
- **FTS5 search.** Landed: per-entity external-content trigram tables (`album_search`, `artist_search`, `playlist_search`) with Room-generated content-sync triggers, a three-character floor and an `instr` fallback below it.
|
|
||||||
- **UI/flow overhead.** Landed: only the active tab's paging flow is collected, and the position ticker updates its own `positionMs` flow instead of the shared `PlaybackState`.
|
|
||||||
|
|
||||||
A `syncGen` column was considered for the sync prune and rejected: it is part of the upsert's update set, so its always-changing value defeats `upsertChanged`'s changed-only `WHERE` predicate and rewrites every row on every full sync. The prune should instead keep the two-phase diff with a memory-bounded id set (primitive long hashes) or diff a staging `seen` table in SQL.
|
|
||||||
|
|
||||||
## Deferred decisions
|
|
||||||
|
|
||||||
- Robolectric runs at SDK 35 while `targetSdk` is 37; aligning them changes the on-demand Roborazzi renders (nothing committed to re-record).
|
|
||||||
- Whether to extract `:core:data` and friends once the UI has grown.
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
subtracks is an open source Android client for Subsonic-compatible servers, including Navidrome, gonic, LMS, Nextcloud Music and more. It is built natively with Kotlin and Jetpack Compose, and gives you clean, convenient access to your music library in the style of a modern media player.
|
subtracks is an open source Android client for Subsonic-compatible servers, including Navidrome, gonic, LMS, Nextcloud Music and more. It is built natively with Kotlin and Jetpack Compose, and gives you clean, convenient access to your music library in the style of a modern media player.
|
||||||
|
|
||||||
Features
|
Features
|
||||||
- Home screen to browse your library over many categories, genres, and more
|
- Large Libraries: built for very large libraries, from syncing the whole collection to playing playlists with thousands of tracks.
|
||||||
- Album and artist artwork, shown full-resolution in the detail and now playing views
|
- Downloads: song and bulk downloads, with download quality (encoding, bitrate) and metered-connection settings.
|
||||||
- Downloads with album, artist and playlist bulk downloads, download quality and a metered-connection toggle
|
- Offline Mode: stars, scrobbles and play counts queue up while you're offline and sync back to the server when you're online again.
|
||||||
- Search across albums, artists, playlists and songs, with per-list sort and filter controls
|
- Artwork-first: album and artist art everywhere, shown full-resolution in the detail and now playing views.
|
||||||
- Gapless playback, an editable queue you can reorder and jump through, and loop and shuffle modes
|
- Adaptive Theming: colours drawn from the current song's artwork.
|
||||||
- Up next queue to add items into the currently playing queue without rewriting it
|
- Search: fast search across the whole library, with per-list sort and filter controls.
|
||||||
- Multiple server support with quick switching
|
- Playback: gapless playback, an editable queue you can reorder and jump through, plus loop and shuffle modes.
|
||||||
- Stream quality (bitrate and format) settings for Wi-Fi and mobile and automatic switching
|
- Up Next: add items into the currently playing queue without rewriting it.
|
||||||
- Scrobbling and starred items, cached while offline and sent back to the server when online
|
- Multiple Servers: connect several servers and switch between them quickly.
|
||||||
- Media notification and headset/Bluetooth controls
|
- Stream Quality: bitrate and format settings for Wi-Fi and mobile, with automatic switching.
|
||||||
- Adaptive theming from the current song's artwork
|
- System Controls: media notification and headset/Bluetooth controls.
|
||||||
|
|
||||||
The full library is mirrored at sync, so browsing, searching, sorting and filtering work offline. Only media and artwork are fetched on demand, and not even that when they're already downloaded. subtracks is free software (GPL-3.0), has no ads and no telemetry, and uses only free/libre dependencies.
|
subtracks is free software (GPL-3.0), with no ads or telemetry.
|
||||||
|
|||||||