Compare commits
125 Commits
Author | SHA1 | Date |
---|---|---|
Heimen Stoffels | 0b0663e1aa | |
Jhoan Sebastian Espinosa Borrero | 7c0032133e | |
@liimee | 70fdfe3236 | |
@liimee | 7067c6807a | |
ghose | b024264750 | |
adil | 232fc0aae1 | |
emptyList() | e59a369661 | |
Sergio Varela | 5d4944e87a | |
ghose | e1d41ed675 | |
Allan Nordhøy | 9d741bc6eb | |
Allan Nordhøy | ec1e4ff629 | |
milotype | dfe9783048 | |
Homer S | bfa7a99015 | |
Homer S | 8cd278be7f | |
Dignified Silence | 9a1dfe59ad | |
Dignified Silence | 6f24d4906b | |
Dignified Silence | 1bf7f2ef38 | |
Creak | db171fb406 | |
Ryan Harg | 5cfb0cbdaf | |
helabasa | a77d3c0222 | |
helabasa | fe014cde1a | |
x | 641ead2c53 | |
Luka Filipović | 4972d0de47 | |
Luka Filipović | c6899d7254 | |
David | dccb3f3520 | |
Storozhenko Evgeny Vladimirovich | 5febaa5837 | |
anonymous | ff8eabb514 | |
vicdorke | 509133c654 | |
Philipp Wolfer | f630b0165c | |
Daniel | 0d39d1f628 | |
ghose | 8eac040142 | |
x | d46d599fc3 | |
x | a1cf0c5f5b | |
ghose | f188b5c449 | |
ghose | 13cd81825a | |
serxoz | 1397bdd449 | |
Daniel | 95fd3a0a6a | |
Dominik Danelski | ef67fa65c0 | |
Dominik Danelski | e20e941c7e | |
vicdorke | 6d718747db | |
Antoine POPINEAU | a7b469d690 | |
Antoine POPINEAU | 6bdefa1936 | |
Antoine POPINEAU | 785fa6ce19 | |
Antoine POPINEAU | 300cc54e97 | |
Antoine POPINEAU | 7feac4e400 | |
Antoine POPINEAU | b0747658ae | |
Antoine POPINEAU | ab654a08c4 | |
Antoine POPINEAU | d2a981c368 | |
Antoine POPINEAU | 049822005e | |
Antoine POPINEAU | d796fca26b | |
Antoine POPINEAU | 54d4dc2235 | |
Antoine POPINEAU | 64ea222f08 | |
Ventura Pérez García | 1380d1d2b9 | |
Antoine POPINEAU | e60814d28f | |
Antoine POPINEAU | ce8d956cee | |
Ventura Pérez García | 1e73ef6ee4 | |
Antoine POPINEAU | 63c8dbe09e | |
Antoine POPINEAU | 9b0c8b0bf6 | |
Antoine POPINEAU | b87766dad2 | |
Antoine POPINEAU | 50c8dac297 | |
Antoine POPINEAU | 1dd38e87fb | |
Antoine POPINEAU | 4cf77404a1 | |
Antoine POPINEAU | 9beb5e6641 | |
Antoine POPINEAU | 998dab0fb5 | |
dulz | 0056faee8e | |
Arne | 964c510312 | |
Keunes | f999745a0c | |
Antoine POPINEAU | 002ebec7ce | |
Antoine POPINEAU | d76f76a222 | |
Antoine POPINEAU | f062e62299 | |
Antoine POPINEAU | d2e472d770 | |
vicdorke | 04d0dd9c09 | |
Arne | 3dafb1c51f | |
anonymous | cbb147fc4b | |
Arne Schlag | 748ef0d935 | |
anonymous | 2c672ecbfa | |
Arne Schlag | 79140f829e | |
anonymous | 326fcefa62 | |
Arne Schlag | 2c657ee85a | |
Arne Schlag | c2ac66d992 | |
anonymous | 684e11d904 | |
Bread Factory | 0bec180cc5 | |
Bread Factory | 89db2a3880 | |
Antoine POPINEAU | a7968e9a87 | |
Antoine POPINEAU | 5c684b6e67 | |
Antoine POPINEAU | 85e9f14e2a | |
Antoine POPINEAU | 1e62cc1f4e | |
Antoine POPINEAU | b0640cf1b2 | |
Antoine POPINEAU | e7cb5e4c6e | |
Antoine POPINEAU | 7035f073f2 | |
Antoine POPINEAU | 931cd0b42d | |
Antoine POPINEAU | ba31a4efcf | |
Antoine POPINEAU | 9fb9d45e05 | |
Antoine POPINEAU | 8d7836172b | |
Antoine POPINEAU | 308e7d7567 | |
Antoine POPINEAU | 7d95618ff5 | |
Antoine POPINEAU | e4da4af3f3 | |
Antoine POPINEAU | b9e9272336 | |
Antoine POPINEAU | 61fdb116ad | |
Antoine POPINEAU | d75e8ae17f | |
Antoine POPINEAU | dd86988518 | |
Antoine POPINEAU | b6b9e4c053 | |
Antoine POPINEAU | eb6b7a807b | |
Antoine POPINEAU | 3a81d26cd9 | |
Antoine POPINEAU | 28949a8e17 | |
Antoine POPINEAU | bc1e911b41 | |
Antoine POPINEAU | 57692f2e42 | |
Antoine POPINEAU | fe224b097a | |
Antoine POPINEAU | 080c07eeab | |
Antoine POPINEAU | b34810d631 | |
Antoine POPINEAU | b14b703f05 | |
Antoine POPINEAU | 4ecb607f45 | |
Antoine POPINEAU | a3f84cc56c | |
Antoine POPINEAU | 4b2cf10e78 | |
Antoine POPINEAU | 5d397ab1fe | |
Antoine POPINEAU | f3bbca9c27 | |
Antoine POPINEAU | 37d5c7b7be | |
Antoine POPINEAU | 97bb621d7f | |
Antoine POPINEAU | b2e6ec43a8 | |
Antoine POPINEAU | de0a494b43 | |
Antoine POPINEAU | 0facf09c94 | |
Antoine POPINEAU | 2c4f8a4329 | |
Antoine POPINEAU | e17c706ae3 | |
Antoine POPINEAU | 37f4b1da9e | |
Antoine POPINEAU | b0d7ff393d |
|
@ -63,6 +63,10 @@ android {
|
|||
buildTypes {
|
||||
getByName("debug") {
|
||||
isDebuggable = true
|
||||
applicationIdSuffix = ".dev"
|
||||
manifestPlaceholders = mapOf(
|
||||
"app_name" to "Otter (develop)"
|
||||
)
|
||||
|
||||
resValue("string", "debug.hostname", props.getProperty("debug.hostname", ""))
|
||||
resValue("string", "debug.username", props.getProperty("debug.username", ""))
|
||||
|
@ -70,6 +74,10 @@ android {
|
|||
}
|
||||
|
||||
getByName("release") {
|
||||
manifestPlaceholders = mapOf(
|
||||
"app_name" to "Otter"
|
||||
)
|
||||
|
||||
if (props.hasProperty("signing.store")) {
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
|
@ -111,26 +119,32 @@ dependencies {
|
|||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.7")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
|
||||
|
||||
implementation("androidx.appcompat:appcompat:1.1.0")
|
||||
implementation("androidx.core:core-ktx:1.5.0-alpha01")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha05")
|
||||
implementation("androidx.appcompat:appcompat:1.2.0")
|
||||
implementation("androidx.core:core-ktx:1.5.0-alpha02")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.3.0-alpha07")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
|
||||
implementation("androidx.preference:preference:1.1.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.1.0")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("com.google.android.material:material:1.3.0-alpha01")
|
||||
implementation("com.android.support.constraint:constraint-layout:1.1.3")
|
||||
implementation("com.google.android.material:material:1.3.0-alpha02")
|
||||
implementation("com.android.support.constraint:constraint-layout:2.0.1")
|
||||
|
||||
implementation("com.google.android.exoplayer:exoplayer-core:2.11.5")
|
||||
implementation("com.google.android.exoplayer:exoplayer-ui:2.11.5")
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.11.5")
|
||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:2.11.4") {
|
||||
isTransitive = false
|
||||
}
|
||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-flac:2.11.4" ){
|
||||
isTransitive = false
|
||||
}
|
||||
|
||||
implementation("com.aliassadi:power-preference-lib:1.4.1")
|
||||
implementation("com.github.kittinunf.fuel:fuel:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-coroutines:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-android:2.1.0")
|
||||
implementation("com.github.kittinunf.fuel:fuel-gson:2.1.0")
|
||||
implementation("com.google.code.gson:gson:2.8.5")
|
||||
implementation("com.google.code.gson:gson:2.8.6")
|
||||
implementation("com.squareup.picasso:picasso:2.71828")
|
||||
implementation("jp.wasabeef:picasso-transformations:2.2.1")
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="com.github.apognu.otter">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
@ -11,48 +12,70 @@
|
|||
android:name="com.github.apognu.otter.Otter"
|
||||
android:allowBackup="false"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:label="${app_name}"
|
||||
android:networkSecurityConfig="@xml/security"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme"
|
||||
android:usesCleartextTraffic="true">
|
||||
|
||||
<activity
|
||||
android:name="com.github.apognu.otter.activities.SplashActivity"
|
||||
android:name=".activities.SplashActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name="com.github.apognu.otter.activities.LoginActivity"
|
||||
android:name=".activities.LoginActivity"
|
||||
android:configChanges="screenSize|orientation"
|
||||
android:launchMode="singleInstance" />
|
||||
<activity android:name="com.github.apognu.otter.activities.MainActivity" />
|
||||
<activity
|
||||
android:name="com.github.apognu.otter.activities.SearchActivity"
|
||||
android:launchMode="singleTop" />
|
||||
<activity android:name="com.github.apognu.otter.activities.DownloadsActivity" />
|
||||
<activity android:name="com.github.apognu.otter.activities.SettingsActivity" />
|
||||
<activity android:name="com.github.apognu.otter.activities.LicencesActivity" />
|
||||
|
||||
<service android:name="com.github.apognu.otter.playback.PlayerService" />
|
||||
<activity android:name=".activities.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.SearchActivity"
|
||||
android:launchMode="singleTop" />
|
||||
|
||||
<activity android:name=".activities.DownloadsActivity" />
|
||||
|
||||
<activity android:name=".activities.SettingsActivity" />
|
||||
|
||||
<activity android:name=".activities.LicencesActivity" />
|
||||
|
||||
<service
|
||||
android:name=".playback.PlayerService"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<service
|
||||
android:name=".playback.PinService"
|
||||
android:exported="false">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="com.google.android.exoplayer.downloadService.action.RESTART" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
|
||||
</service>
|
||||
|
||||
<receiver android:name="com.github.apognu.otter.playback.MediaControlActionReceiver" />
|
||||
<receiver android:name="androidx.media.session.MediaButtonReceiver">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
</application>
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package com.github.apognu.otter
|
|||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.github.apognu.otter.playback.MediaSession
|
||||
import com.github.apognu.otter.playback.QueueManager
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.database.ExoDatabaseProvider
|
||||
|
@ -60,6 +61,8 @@ class Otter : Application() {
|
|||
}
|
||||
}
|
||||
|
||||
val mediaSession = MediaSession(this)
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
@ -84,8 +87,6 @@ class Otter : Application() {
|
|||
}
|
||||
|
||||
cacheDir.resolve("picasso-cache").deleteRecursively()
|
||||
|
||||
exoDownloadManager.removeAllDownloads()
|
||||
}
|
||||
|
||||
inner class CrashReportHandler : Thread.UncaughtExceptionHandler {
|
||||
|
|
|
@ -6,6 +6,7 @@ import android.animation.ObjectAnimator
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.*
|
||||
|
@ -18,7 +19,9 @@ import androidx.core.graphics.drawable.toDrawable
|
|||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.*
|
||||
import com.github.apognu.otter.playback.MediaControlsManager
|
||||
|
@ -28,6 +31,7 @@ import com.github.apognu.otter.repositories.FavoritedRepository
|
|||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.views.DisableableFrameLayout
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
|
||||
import com.google.android.exoplayer2.Player
|
||||
|
@ -51,7 +55,8 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private val favoriteRepository = FavoritesRepository(this)
|
||||
private val favoriteCheckRepository = FavoritedRepository(this)
|
||||
private val favoritedRepository = FavoritedRepository(this)
|
||||
private var menu: Menu? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -76,6 +81,18 @@ class MainActivity : AppCompatActivity() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
(container as? DisableableFrameLayout)?.setShouldRegisterTouch { _ ->
|
||||
if (now_playing.isOpened()) {
|
||||
now_playing.close()
|
||||
|
||||
return@setShouldRegisterTouch false
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
favoritedRepository.update(this, lifecycleScope)
|
||||
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
DownloadService.start(this, PinService::class.java)
|
||||
|
||||
|
@ -131,10 +148,22 @@ class MainActivity : AppCompatActivity() {
|
|||
super.onBackPressed()
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu?): Boolean {
|
||||
this.menu = menu
|
||||
|
||||
return super.onPrepareOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
|
||||
menuInflater.inflate(R.menu.toolbar, menu)
|
||||
|
||||
menu?.findItem(R.id.nav_only_my_music)?.isChecked = Settings.getScope() == "me"
|
||||
menu?.findItem(R.id.nav_all_music)?.let {
|
||||
it.isChecked = Settings.getScopes().contains("all")
|
||||
it.isEnabled = !it.isChecked
|
||||
}
|
||||
|
||||
menu?.findItem(R.id.nav_my_music)?.isChecked = Settings.getScopes().contains("me")
|
||||
menu?.findItem(R.id.nav_followed)?.isChecked = Settings.getScopes().contains("subscribed")
|
||||
|
||||
return true
|
||||
}
|
||||
|
@ -155,15 +184,61 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
R.id.nav_queue -> launchDialog(QueueFragment())
|
||||
R.id.nav_search -> startActivity(Intent(this, SearchActivity::class.java))
|
||||
R.id.nav_only_my_music -> {
|
||||
item.isChecked = !item.isChecked
|
||||
R.id.nav_all_music, R.id.nav_my_music, R.id.nav_followed -> {
|
||||
menu?.let { menu ->
|
||||
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW)
|
||||
item.actionView = View(this)
|
||||
item.setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
|
||||
override fun onMenuItemActionExpand(item: MenuItem?) = false
|
||||
override fun onMenuItemActionCollapse(item: MenuItem?) = false
|
||||
})
|
||||
|
||||
when (item.isChecked) {
|
||||
true -> PowerPreference.getDefaultFile().set("scope", "me")
|
||||
false -> PowerPreference.getDefaultFile().set("scope", "all")
|
||||
item.isChecked = !item.isChecked
|
||||
|
||||
val scopes = Settings.getScopes().toMutableSet()
|
||||
|
||||
val new = when (item.itemId) {
|
||||
R.id.nav_my_music -> "me"
|
||||
R.id.nav_followed -> "subscribed"
|
||||
|
||||
else -> {
|
||||
menu.findItem(R.id.nav_all_music).isEnabled = false
|
||||
menu.findItem(R.id.nav_my_music).isChecked = false
|
||||
menu.findItem(R.id.nav_followed).isChecked = false
|
||||
|
||||
PowerPreference.getDefaultFile().set("scope", "all")
|
||||
EventBus.send(Event.ListingsChanged)
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
menu.findItem(R.id.nav_all_music).let {
|
||||
it.isChecked = false
|
||||
it.isEnabled = true
|
||||
}
|
||||
|
||||
scopes.remove("all")
|
||||
|
||||
when (item.isChecked) {
|
||||
true -> scopes.add(new)
|
||||
false -> scopes.remove(new)
|
||||
}
|
||||
|
||||
if (scopes.isEmpty()) {
|
||||
menu.findItem(R.id.nav_all_music).let {
|
||||
it.isChecked = true
|
||||
it.isEnabled = false
|
||||
}
|
||||
|
||||
scopes.add("all")
|
||||
}
|
||||
|
||||
PowerPreference.getDefaultFile().set("scope", scopes.joinToString(","))
|
||||
EventBus.send(Event.ListingsChanged)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
EventBus.send(Event.ListingsChanged)
|
||||
}
|
||||
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
|
||||
R.id.settings -> startActivityForResult(Intent(this, SettingsActivity::class.java), 0)
|
||||
|
@ -177,8 +252,11 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
if (resultCode == ResultCode.LOGOUT.code) {
|
||||
Intent(this, LoginActivity::class.java).apply {
|
||||
Otter.get().deleteAllData()
|
||||
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
|
||||
stopService(Intent(this@MainActivity, PlayerService::class.java))
|
||||
startActivity(this)
|
||||
finish()
|
||||
}
|
||||
|
@ -212,7 +290,7 @@ class MainActivity : AppCompatActivity() {
|
|||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.LogOut -> {
|
||||
PowerPreference.clearAllData()
|
||||
Otter.get().deleteAllData()
|
||||
|
||||
startActivity(Intent(this@MainActivity, LoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
|
@ -286,7 +364,26 @@ class MainActivity : AppCompatActivity() {
|
|||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.StartService -> {
|
||||
Build.VERSION_CODES.O.onApi(
|
||||
{
|
||||
startForegroundService(Intent(this@MainActivity, PlayerService::class.java).apply {
|
||||
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
|
||||
})
|
||||
},
|
||||
{
|
||||
startService(Intent(this@MainActivity, PlayerService::class.java).apply {
|
||||
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.command.toString())
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
|
||||
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
AddToPlaylistDialog.show(this@MainActivity, lifecycleScope, command.tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -340,14 +437,14 @@ class MainActivity : AppCompatActivity() {
|
|||
now_playing_details_toggle.icon = getDrawable(R.drawable.pause)
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover?.urls?.original))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.into(now_playing_cover)
|
||||
|
||||
now_playing_details_cover?.let { now_playing_details_cover ->
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
|
@ -361,7 +458,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}.widthPixels
|
||||
|
||||
val backgroundCover = Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
|
||||
.get()
|
||||
.run { Bitmap.createScaledBitmap(this, width, width, false).toDrawable(resources) }
|
||||
.apply {
|
||||
|
@ -392,7 +489,7 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.track_info_artist -> ArtistsFragment.openAlbums(this@MainActivity, track.artist, art = track.album.cover.original)
|
||||
R.id.track_info_artist -> ArtistsFragment.openAlbums(this@MainActivity, track.artist, art = track.album?.cover())
|
||||
R.id.track_info_album -> AlbumsFragment.openTracks(this@MainActivity, track.album)
|
||||
R.id.track_info_details -> TrackInfoDetailsFragment.new(track).show(supportFragmentManager, "dialog")
|
||||
}
|
||||
|
@ -408,7 +505,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
now_playing_details_favorite?.let { now_playing_details_favorite ->
|
||||
favoriteCheckRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _ ->
|
||||
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
|
||||
lifecycleScope.launch(Main) {
|
||||
track.favorite = favorites.contains(track.id)
|
||||
|
||||
|
@ -436,6 +533,10 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
favoriteRepository.fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
now_playing_details_add_to_playlist.setOnClickListener {
|
||||
CommandBus.send(Command.AddToPlaylist(listOf(track)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,17 +3,22 @@ package com.github.apognu.otter.activities
|
|||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.SearchAdapter
|
||||
import com.github.apognu.otter.fragments.AddToPlaylistDialog
|
||||
import com.github.apognu.otter.fragments.AlbumsFragment
|
||||
import com.github.apognu.otter.fragments.ArtistsFragment
|
||||
import com.github.apognu.otter.repositories.*
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.untilNetwork
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import kotlinx.android.synthetic.main.activity_search.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.net.URLEncoder
|
||||
import java.util.*
|
||||
|
||||
|
@ -37,12 +42,35 @@ class SearchActivity : AppCompatActivity() {
|
|||
results.layoutManager = LinearLayoutManager(this)
|
||||
results.adapter = it
|
||||
}
|
||||
|
||||
search.requestFocus()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
search.requestFocus()
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.AddToPlaylist -> if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
AddToPlaylistDialog.show(this@SearchActivity, lifecycleScope, command.tracks)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
EventBus.get().collect { message ->
|
||||
when (message) {
|
||||
is Event.DownloadChanged -> refreshDownloadedTrack(message.download)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
artistsRepository = ArtistsSearchRepository(this@SearchActivity, "")
|
||||
albumsRepository = AlbumsSearchRepository(this@SearchActivity, "")
|
||||
tracksRepository = TracksSearchRepository(this@SearchActivity, "")
|
||||
favoritesRepository = FavoritesRepository(this@SearchActivity)
|
||||
|
||||
search.setOnQueryTextListener(object : androidx.appcompat.widget.SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(rawQuery: String?): Boolean {
|
||||
|
@ -53,10 +81,9 @@ class SearchActivity : AppCompatActivity() {
|
|||
|
||||
val query = URLEncoder.encode(it, "UTF-8")
|
||||
|
||||
tracksRepository = TracksSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
|
||||
albumsRepository = AlbumsSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
|
||||
artistsRepository = ArtistsSearchRepository(this@SearchActivity, query.toLowerCase(Locale.ROOT))
|
||||
favoritesRepository = FavoritesRepository(this@SearchActivity)
|
||||
artistsRepository.query = query.toLowerCase(Locale.ROOT)
|
||||
albumsRepository.query = query.toLowerCase(Locale.ROOT)
|
||||
tracksRepository.query = query.toLowerCase(Locale.ROOT)
|
||||
|
||||
search_spinner.visibility = View.VISIBLE
|
||||
search_empty.visibility = View.GONE
|
||||
|
@ -67,21 +94,21 @@ class SearchActivity : AppCompatActivity() {
|
|||
adapter.tracks.clear()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _ ->
|
||||
artistsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { artists, _, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.artists.addAll(artists)
|
||||
refresh()
|
||||
}
|
||||
|
||||
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _ ->
|
||||
albumsRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { albums, _, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.albums.addAll(albums)
|
||||
refresh()
|
||||
}
|
||||
|
||||
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _ ->
|
||||
tracksRepository.fetch(Repository.Origin.Network.origin).untilNetwork(lifecycleScope) { tracks, _, _, _ ->
|
||||
done++
|
||||
|
||||
adapter.tracks.addAll(tracks)
|
||||
|
@ -110,6 +137,19 @@ class SearchActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshDownloadedTrack(download: Download) {
|
||||
if (download.state == Download.STATE_COMPLETED) {
|
||||
download.getMetadata()?.let { info ->
|
||||
adapter.tracks.withIndex().associate { it.value to it.index }.filter { it.key.id == info.id }.toList().getOrNull(0)?.let { match ->
|
||||
withContext(Dispatchers.Main) {
|
||||
adapter.tracks[match.second].downloaded = true
|
||||
adapter.notifyItemChanged(adapter.getPositionOf(SearchAdapter.ResultType.Track, match.second))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class SearchResultClickListener : SearchAdapter.OnSearchResultClickListener {
|
||||
override fun onArtistClick(holder: View?, artist: Artist) {
|
||||
ArtistsFragment.openAlbums(this@SearchActivity, artist)
|
||||
|
|
|
@ -112,6 +112,14 @@ class SettingsFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedP
|
|||
}
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<ListPreference>("play_order")?.let {
|
||||
it.summary = when (it.value) {
|
||||
"shuffle" -> activity.getString(R.string.settings_play_order_shuffle_summary)
|
||||
"in_order" -> activity.getString(R.string.settings_play_order_in_order_summary)
|
||||
else -> activity.getString(R.string.settings_play_order_shuffle_summary)
|
||||
}
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<ListPreference>("night_mode")?.let {
|
||||
when (it.value) {
|
||||
"on" -> {
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Settings
|
||||
|
||||
|
@ -20,6 +21,8 @@ class SplashActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
false -> Intent(this@SplashActivity, LoginActivity::class.java).apply {
|
||||
Otter.get().deleteAllData()
|
||||
|
||||
flags = Intent.FLAG_ACTIVITY_NO_ANIMATION
|
||||
|
||||
startActivity(this)
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
|
@ -15,11 +15,13 @@ import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
|||
import kotlinx.android.synthetic.main.row_album.view.*
|
||||
import kotlinx.android.synthetic.main.row_artist.view.art
|
||||
|
||||
class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickListener) : FunkwhaleAdapter<Album, AlbumsAdapter.ViewHolder>() {
|
||||
class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsAdapter.ViewHolder>() {
|
||||
interface OnAlbumClickListener {
|
||||
fun onClick(view: View?, album: Album)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
|
@ -34,19 +36,28 @@ class AlbumsAdapter(val context: Context?, private val listener: OnAlbumClickLis
|
|||
val album = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(album.cover()))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
|
||||
holder.title.text = album.title
|
||||
holder.artist.text = album.artist.name
|
||||
holder.release_date.visibility = View.GONE
|
||||
|
||||
album.release_date?.split('-')?.getOrNull(0)?.let { year ->
|
||||
if (year.isNotEmpty()) {
|
||||
holder.release_date.visibility = View.VISIBLE
|
||||
holder.release_date.text = year
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, private val listener: OnAlbumClickListener) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
val art = view.art
|
||||
val title = view.title
|
||||
val artist = view.artist
|
||||
val release_date = view.release_date
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(view, data[layoutPosition])
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
|
@ -14,11 +14,13 @@ import com.squareup.picasso.Picasso
|
|||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_album_grid.view.*
|
||||
|
||||
class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClickListener) : FunkwhaleAdapter<Album, AlbumsGridAdapter.ViewHolder>() {
|
||||
class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClickListener) : OtterAdapter<Album, AlbumsGridAdapter.ViewHolder>() {
|
||||
interface OnAlbumClickListener {
|
||||
fun onClick(view: View?, album: Album)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
|
@ -33,7 +35,7 @@ class AlbumsGridAdapter(val context: Context?, private val listener: OnAlbumClic
|
|||
val album = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(album.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.maybeLoad
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
|
@ -14,14 +14,32 @@ import com.squareup.picasso.Picasso
|
|||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_artist.view.*
|
||||
|
||||
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : FunkwhaleAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
||||
class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickListener) : OtterAdapter<Artist, ArtistsAdapter.ViewHolder>() {
|
||||
private var active: List<Artist> = mutableListOf()
|
||||
|
||||
interface OnArtistClickListener {
|
||||
fun onClick(holder: View?, artist: Artist)
|
||||
}
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
init {
|
||||
registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
||||
override fun onChanged() {
|
||||
active = data.filter { it.albums?.isNotEmpty() ?: false }
|
||||
|
||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||
super.onChanged()
|
||||
}
|
||||
|
||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
||||
active = data.filter { it.albums?.isNotEmpty() ?: false }
|
||||
|
||||
super.onItemRangeInserted(positionStart, itemCount)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun getItemCount() = active.size
|
||||
|
||||
override fun getItemId(position: Int) = active[position].id.toLong()
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val view = LayoutInflater.from(context).inflate(R.layout.row_artist, parent, false)
|
||||
|
@ -32,12 +50,12 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
|||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val artist = data[position]
|
||||
val artist = active[position]
|
||||
|
||||
artist.albums?.let { albums ->
|
||||
if (albums.isNotEmpty()) {
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(albums[0].cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(albums[0].cover?.urls?.original))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
|
@ -59,7 +77,7 @@ class ArtistsAdapter(val context: Context?, private val listener: OnArtistClickL
|
|||
val albums = view.albums
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
listener.onClick(view, data[layoutPosition])
|
||||
listener.onClick(view, active[layoutPosition])
|
||||
}
|
||||
}
|
||||
}
|
|
@ -79,7 +79,7 @@ class DownloadsAdapter(private val context: Context, private val listener: OnDow
|
|||
Download.STATE_QUEUED, Download.STATE_DOWNLOADING -> DownloadService.sendSetStopReason(context, PinService::class.java, download.contentId, 1, false)
|
||||
|
||||
Download.STATE_FAILED -> {
|
||||
Track(download.id, download.title, Artist(0, download.artist, listOf()), Album(0, Album.Artist(""), "", Covers("")), 0, listOf(Track.Upload(download.contentId, 0, 0))).also {
|
||||
Track.fromDownload(download).also {
|
||||
PinService.download(context, it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,8 +4,6 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.graphics.PorterDuff
|
||||
import android.graphics.PorterDuffColorFilter
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -13,14 +11,14 @@ import android.view.ViewGroup
|
|||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
import java.util.*
|
||||
|
||||
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : FunkwhaleAdapter<Track, FavoritesAdapter.ViewHolder>() {
|
||||
class FavoritesAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener, val fromQueue: Boolean = false) : OtterAdapter<Track, FavoritesAdapter.ViewHolder>() {
|
||||
interface OnFavoriteListener {
|
||||
fun onToggleFavorite(id: Int, state: Boolean)
|
||||
}
|
||||
|
@ -46,7 +44,7 @@ class FavoritesAdapter(private val context: Context?, private val favoriteListen
|
|||
val favorite = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(favorite.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(favorite.album?.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
|
|
|
@ -3,26 +3,30 @@ package com.github.apognu.otter.adapters
|
|||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
import java.util.*
|
||||
|
||||
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : FunkwhaleAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
|
||||
class PlaylistTracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, private val playlistListener: OnPlaylistListener? = null) : OtterAdapter<PlaylistTrack, PlaylistTracksAdapter.ViewHolder>() {
|
||||
interface OnFavoriteListener {
|
||||
fun onToggleFavorite(id: Int, state: Boolean)
|
||||
}
|
||||
|
||||
interface OnPlaylistListener {
|
||||
fun onMoveTrack(from: Int, to: Int)
|
||||
fun onRemoveTrackFromPlaylist(track: Track, index: Int)
|
||||
}
|
||||
|
||||
private lateinit var touchHelper: ItemTouchHelper
|
||||
|
||||
var currentTrack: Track? = null
|
||||
|
@ -36,10 +40,8 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
|
||||
if (fromQueue) {
|
||||
touchHelper = ItemTouchHelper(TouchHelperCallback()).also {
|
||||
it.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
touchHelper = ItemTouchHelper(TouchHelperCallback()).also {
|
||||
it.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -56,7 +58,7 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
val track = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.track.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.track.album?.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
|
@ -65,19 +67,14 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
holder.title.text = track.track.title
|
||||
holder.artist.text = track.track.artist.name
|
||||
|
||||
Build.VERSION_CODES.P.onApi(
|
||||
{
|
||||
holder.title.setTypeface(holder.title.typeface, Typeface.DEFAULT.weight)
|
||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.DEFAULT.weight)
|
||||
},
|
||||
{
|
||||
holder.title.setTypeface(holder.title.typeface, Typeface.NORMAL)
|
||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.NORMAL)
|
||||
})
|
||||
context?.let {
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
|
||||
}
|
||||
|
||||
if (track.track == currentTrack) {
|
||||
holder.title.setTypeface(holder.title.typeface, Typeface.BOLD)
|
||||
holder.artist.setTypeface(holder.artist.typeface, Typeface.BOLD)
|
||||
if (track.track == currentTrack || track.track.current) {
|
||||
context?.let {
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
|
||||
}
|
||||
}
|
||||
|
||||
context?.let {
|
||||
|
@ -99,13 +96,16 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
holder.actions.setOnClickListener {
|
||||
context?.let { context ->
|
||||
PopupMenu(context, holder.actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
|
||||
inflate(if (fromQueue) R.menu.row_queue else R.menu.row_track)
|
||||
inflate(R.menu.row_track)
|
||||
|
||||
menu.findItem(R.id.track_remove_from_playlist).isVisible = true
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track.track)))
|
||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track.track))
|
||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track.track))
|
||||
R.id.track_remove_from_playlist -> playlistListener?.onRemoveTrackFromPlaylist(track.track, position)
|
||||
}
|
||||
|
||||
true
|
||||
|
@ -116,16 +116,14 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
}
|
||||
}
|
||||
|
||||
if (fromQueue) {
|
||||
holder.handle.visibility = View.VISIBLE
|
||||
holder.handle.visibility = View.VISIBLE
|
||||
|
||||
holder.handle.setOnTouchListener { _, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
touchHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
true
|
||||
holder.handle.setOnTouchListener { _, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
touchHelper.startDrag(holder)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,13 +133,12 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
Collections.swap(data, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in newPosition.downTo(oldPosition)) {
|
||||
for (i in oldPosition.downTo(newPosition + 1)) {
|
||||
Collections.swap(data, i, i - 1)
|
||||
}
|
||||
}
|
||||
|
||||
notifyItemMoved(oldPosition, newPosition)
|
||||
CommandBus.send(Command.MoveFromQueue(oldPosition, newPosition))
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
|
@ -154,20 +151,18 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
val actions = view.actions
|
||||
|
||||
override fun onClick(view: View?) {
|
||||
when (fromQueue) {
|
||||
true -> CommandBus.send(Command.PlayTrack(layoutPosition))
|
||||
false -> {
|
||||
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
|
||||
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
|
||||
data.subList(layoutPosition, data.size).plus(data.subList(0, layoutPosition)).apply {
|
||||
CommandBus.send(Command.ReplaceQueue(this.map { it.track }))
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
}
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class TouchHelperCallback : ItemTouchHelper.Callback() {
|
||||
var from = -1
|
||||
var to = -1
|
||||
|
||||
override fun isLongPressDragEnabled() = false
|
||||
|
||||
override fun isItemViewSwipeEnabled() = false
|
||||
|
@ -176,6 +171,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
makeMovementFlags(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0)
|
||||
|
||||
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
|
||||
if (from == -1) from = viewHolder.adapterPosition
|
||||
to = target.adapterPosition
|
||||
|
||||
onItemMove(viewHolder.adapterPosition, target.adapterPosition)
|
||||
|
||||
return true
|
||||
|
@ -185,7 +183,9 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
|
||||
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
|
||||
if (actionState == ItemTouchHelper.ACTION_STATE_DRAG) {
|
||||
viewHolder?.itemView?.background = ColorDrawable(Color.argb(255, 100, 100, 100))
|
||||
context?.let {
|
||||
viewHolder?.itemView?.background = ColorDrawable(context.getColor(R.color.colorSelected))
|
||||
}
|
||||
}
|
||||
|
||||
super.onSelectedChanged(viewHolder, actionState)
|
||||
|
@ -194,6 +194,13 @@ class PlaylistTracksAdapter(private val context: Context?, private val favoriteL
|
|||
override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) {
|
||||
viewHolder.itemView.background = ColorDrawable(Color.TRANSPARENT)
|
||||
|
||||
if (from != -1 && to != -1 && from != to) {
|
||||
playlistListener?.onMoveTrack(from, to)
|
||||
|
||||
from = -1
|
||||
to = -1
|
||||
}
|
||||
|
||||
super.clearView(recyclerView, viewHolder)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,16 +4,17 @@ import android.content.Context
|
|||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.Playlist
|
||||
import com.github.apognu.otter.utils.toDurationString
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_playlist.view.*
|
||||
|
||||
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : FunkwhaleAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
|
||||
class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistClickListener) : OtterAdapter<Playlist, PlaylistsAdapter.ViewHolder>() {
|
||||
interface OnPlaylistClickListener {
|
||||
fun onClick(holder: View?, playlist: Playlist)
|
||||
}
|
||||
|
@ -36,6 +37,15 @@ class PlaylistsAdapter(val context: Context?, private val listener: OnPlaylistCl
|
|||
holder.name.text = playlist.name
|
||||
holder.summary.text = context?.resources?.getQuantityString(R.plurals.playlist_description, playlist.tracks_count, playlist.tracks_count, toDurationString(playlist.duration.toLong())) ?: ""
|
||||
|
||||
context?.let {
|
||||
ContextCompat.getDrawable(context, R.drawable.cover).let {
|
||||
holder.cover_top_left.setImageDrawable(it)
|
||||
holder.cover_top_right.setImageDrawable(it)
|
||||
holder.cover_bottom_left.setImageDrawable(it)
|
||||
holder.cover_bottom_right.setImageDrawable(it)
|
||||
}
|
||||
}
|
||||
|
||||
playlist.album_covers.shuffled().take(4).forEachIndexed { index, url ->
|
||||
val imageView = when (index) {
|
||||
0 -> holder.cover_top_left
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
|
@ -20,7 +20,7 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : FunkwhaleAdapter<Radio, RadiosAdapter.ViewHolder>() {
|
||||
class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private val listener: OnRadioClickListener) : OtterAdapter<Radio, RadiosAdapter.ViewHolder>() {
|
||||
interface OnRadioClickListener {
|
||||
fun onClick(holder: ViewHolder, radio: Radio)
|
||||
}
|
||||
|
@ -57,9 +57,13 @@ class RadiosAdapter(val context: Context?, val scope: CoroutineScope, private va
|
|||
}
|
||||
}
|
||||
|
||||
override fun getItemCount() = instanceRadios.size + data.size + 2
|
||||
override fun getItemId(position: Int) = when (getItemViewType(position)) {
|
||||
RowType.InstanceRadio.ordinal -> (-position - 1).toLong()
|
||||
RowType.Header.ordinal -> Long.MIN_VALUE
|
||||
else -> getRadioAt(position).id.toLong()
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int) = data[position].id.toLong()
|
||||
override fun getItemCount() = instanceRadios.size + data.size + 2
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return when {
|
||||
|
|
|
@ -214,6 +214,8 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
|||
when (it.itemId) {
|
||||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
||||
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
|
||||
R.id.track_add_to_playlist -> CommandBus.send(Command.AddToPlaylist(listOf(track)))
|
||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
||||
}
|
||||
|
||||
|
@ -228,6 +230,15 @@ class SearchAdapter(private val context: Context?, private val listener: OnSearc
|
|||
}
|
||||
}
|
||||
|
||||
fun getPositionOf(type: ResultType, position: Int): Int {
|
||||
return when (type) {
|
||||
ResultType.Artist -> position + 1
|
||||
ResultType.Album -> position + artists.size + 2
|
||||
ResultType.Track -> artists.size + albums.size + SECTION_COUNT + position
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
inner class ViewHolder(view: View, val context: Context?) : RecyclerView.ViewHolder(view), View.OnClickListener {
|
||||
val handle = view.handle
|
||||
val cover = view.cover
|
||||
|
|
|
@ -4,20 +4,20 @@ import android.annotation.SuppressLint
|
|||
import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.Build
|
||||
import android.view.*
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.ItemTouchHelper
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.fragments.FunkwhaleAdapter
|
||||
import com.github.apognu.otter.fragments.OtterAdapter
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.row_track.view.*
|
||||
import java.util.*
|
||||
|
||||
class TracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : FunkwhaleAdapter<Track, TracksAdapter.ViewHolder>() {
|
||||
class TracksAdapter(private val context: Context?, private val favoriteListener: OnFavoriteListener? = null, val fromQueue: Boolean = false) : OtterAdapter<Track, TracksAdapter.ViewHolder>() {
|
||||
interface OnFavoriteListener {
|
||||
fun onToggleFavorite(id: Int, state: Boolean)
|
||||
}
|
||||
|
@ -26,11 +26,9 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
|
||||
var currentTrack: Track? = null
|
||||
|
||||
override fun getItemCount() = data.size
|
||||
override fun getItemId(position: Int): Long = data[position].id.toLong()
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return data[position].id.toLong()
|
||||
}
|
||||
override fun getItemCount() = data.size
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
|
@ -55,7 +53,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
val track = data[position]
|
||||
|
||||
Picasso.get()
|
||||
.maybeLoad(maybeNormalizeUrl(track.album.cover.original))
|
||||
.maybeLoad(maybeNormalizeUrl(track.album?.cover()))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.cover)
|
||||
|
@ -64,12 +62,12 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
holder.artist.text = track.artist.name
|
||||
|
||||
context?.let {
|
||||
holder.itemView.background = context.getDrawable(R.drawable.ripple)
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.ripple)
|
||||
}
|
||||
|
||||
if (track == currentTrack || track.current) {
|
||||
context?.let {
|
||||
holder.itemView.background = context.getDrawable(R.drawable.current)
|
||||
holder.itemView.background = ContextCompat.getDrawable(context, R.drawable.current)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -117,6 +115,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
R.id.track_add_to_queue -> CommandBus.send(Command.AddToQueue(listOf(track)))
|
||||
R.id.track_play_next -> CommandBus.send(Command.PlayNext(track))
|
||||
R.id.track_pin -> CommandBus.send(Command.PinTrack(track))
|
||||
R.id.track_add_to_playlist -> CommandBus.send(Command.AddToPlaylist(listOf(track)))
|
||||
R.id.queue_remove -> CommandBus.send(Command.RemoveFromQueue(track))
|
||||
}
|
||||
|
||||
|
@ -147,7 +146,7 @@ class TracksAdapter(private val context: Context?, private val favoriteListener:
|
|||
Collections.swap(data, i, i + 1)
|
||||
}
|
||||
} else {
|
||||
for (i in newPosition.downTo(oldPosition)) {
|
||||
for (i in oldPosition.downTo(newPosition + 1)) {
|
||||
Collections.swap(data, i, i - 1)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,105 @@
|
|||
package com.github.apognu.otter.fragments
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.PlaylistsAdapter
|
||||
import com.github.apognu.otter.repositories.ManagementPlaylistsRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.dialog_add_to_playlist.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
object AddToPlaylistDialog {
|
||||
fun show(activity: Activity, lifecycleScope: CoroutineScope, tracks: List<Track>) {
|
||||
val dialog = AlertDialog.Builder(activity).run {
|
||||
setTitle(activity.getString(R.string.playlist_add_to))
|
||||
setView(activity.layoutInflater.inflate(R.layout.dialog_add_to_playlist, null))
|
||||
|
||||
create()
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
|
||||
val repository = ManagementPlaylistsRepository(activity)
|
||||
|
||||
dialog.name.editText?.addTextChangedListener {
|
||||
dialog.create.isEnabled = !(dialog.name.editText?.text?.trim()?.isBlank() ?: true)
|
||||
}
|
||||
|
||||
dialog.create.setOnClickListener {
|
||||
val name = dialog.name.editText?.text?.toString()?.trim() ?: ""
|
||||
|
||||
if (name.isEmpty()) return@setOnClickListener
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
repository.new(name)?.let { id ->
|
||||
repository.add(id, tracks)
|
||||
|
||||
withContext(Main) {
|
||||
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, name), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val adapter = PlaylistsAdapter(activity, object : PlaylistsAdapter.OnPlaylistClickListener {
|
||||
override fun onClick(holder: View?, playlist: Playlist) {
|
||||
repository.add(playlist.id, tracks)
|
||||
|
||||
Toast.makeText(activity, activity.getString(R.string.playlist_added_to, playlist.name), Toast.LENGTH_SHORT).show()
|
||||
|
||||
dialog.dismiss()
|
||||
}
|
||||
})
|
||||
|
||||
dialog.playlists.layoutManager = LinearLayoutManager(activity)
|
||||
dialog.playlists.adapter = adapter
|
||||
|
||||
repository.apply {
|
||||
var first = true
|
||||
|
||||
fetch().untilNetwork(lifecycleScope) { data, isCache, _, hasMore ->
|
||||
if (isCache) {
|
||||
adapter.data = data.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
return@untilNetwork
|
||||
}
|
||||
|
||||
if (first) {
|
||||
adapter.data.clear()
|
||||
first = false
|
||||
}
|
||||
|
||||
adapter.data.addAll(data)
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
try {
|
||||
Cache.set(
|
||||
context,
|
||||
cacheId,
|
||||
Gson().toJson(cache(adapter.data)).toByteArray()
|
||||
)
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasMore) {
|
||||
adapter.notifyDataSetChanged()
|
||||
first = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,11 +30,12 @@ import kotlinx.coroutines.flow.toList
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
||||
class AlbumsFragment : OtterFragment<Album, AlbumsAdapter>() {
|
||||
override val viewRes = R.layout.fragment_albums
|
||||
override val recycler: RecyclerView get() = albums
|
||||
override val alwaysRefresh = false
|
||||
|
||||
lateinit var artistTracksRepository: ArtistTracksRepository
|
||||
private lateinit var artistTracksRepository: ArtistTracksRepository
|
||||
|
||||
var artistId = 0
|
||||
var artistName = ""
|
||||
|
@ -42,7 +43,7 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
|
||||
companion object {
|
||||
fun new(artist: Artist, _art: String? = null): AlbumsFragment {
|
||||
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.albums[0].cover.original else ""
|
||||
val art = _art ?: if (artist.albums?.isNotEmpty() == true) artist.cover() else ""
|
||||
|
||||
return AlbumsFragment().apply {
|
||||
arguments = bundleOf(
|
||||
|
@ -53,7 +54,11 @@ class AlbumsFragment : FunkwhaleFragment<Album, AlbumsAdapter>() {
|
|||
}
|
||||
}
|
||||
|
||||
fun openTracks(context: Context?, album: Album, fragment: Fragment? = null) {
|
||||
fun openTracks(context: Context?, album: Album?, fragment: Fragment? = null) {
|
||||
if (album == null) {
|
||||
return
|
||||
}
|
||||
|
||||
(context as? MainActivity)?.let {
|
||||
fragment?.let { fragment ->
|
||||
fragment.onViewPager {
|
||||
|
|
|
@ -15,10 +15,11 @@ import com.github.apognu.otter.utils.Album
|
|||
import com.github.apognu.otter.utils.AppContext
|
||||
import kotlinx.android.synthetic.main.fragment_albums_grid.*
|
||||
|
||||
class AlbumsGridFragment : FunkwhaleFragment<Album, AlbumsGridAdapter>() {
|
||||
class AlbumsGridFragment : OtterFragment<Album, AlbumsGridAdapter>() {
|
||||
override val viewRes = R.layout.fragment_albums_grid
|
||||
override val recycler: RecyclerView get() = albums
|
||||
override val layoutManager get() = GridLayoutManager(context, 3)
|
||||
override val alwaysRefresh = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -18,9 +18,10 @@ import com.github.apognu.otter.utils.Artist
|
|||
import com.github.apognu.otter.utils.onViewPager
|
||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||
|
||||
class ArtistsFragment : FunkwhaleFragment<Artist, ArtistsAdapter>() {
|
||||
class ArtistsFragment : OtterFragment<Artist, ArtistsAdapter>() {
|
||||
override val viewRes = R.layout.fragment_artists
|
||||
override val recycler: RecyclerView get() = artists
|
||||
override val alwaysRefresh = false
|
||||
|
||||
companion object {
|
||||
fun openAlbums(context: Context?, artist: Artist, fragment: Fragment? = null, art: String? = null) {
|
||||
|
|
|
@ -16,9 +16,10 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class FavoritesFragment : FunkwhaleFragment<Track, FavoritesAdapter>() {
|
||||
class FavoritesFragment : OtterFragment<Track, FavoritesAdapter>() {
|
||||
override val viewRes = R.layout.fragment_favorites
|
||||
override val recycler: RecyclerView get() = favorites
|
||||
override val alwaysRefresh = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -40,6 +40,20 @@ class LandscapeQueueFragment : Fragment() {
|
|||
queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
|
||||
queue_shuffle.setOnClickListener {
|
||||
CommandBus.send(Command.ShuffleQueue)
|
||||
}
|
||||
|
||||
queue_save.setOnClickListener {
|
||||
adapter?.data?.let {
|
||||
CommandBus.send(Command.AddToPlaylist(it))
|
||||
}
|
||||
}
|
||||
|
||||
queue_clear.setOnClickListener {
|
||||
CommandBus.send(Command.ClearQueue)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
|
|
@ -8,12 +8,10 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.recyclerview.widget.SimpleItemAnimator
|
||||
import com.github.apognu.otter.repositories.HttpUpstream
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.Event
|
||||
import com.github.apognu.otter.utils.EventBus
|
||||
import com.github.apognu.otter.utils.untilNetwork
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.gson.Gson
|
||||
import kotlinx.android.synthetic.main.fragment_artists.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
|
@ -23,19 +21,30 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
abstract class FunkwhaleAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
abstract class OtterAdapter<D, VH : RecyclerView.ViewHolder> : RecyclerView.Adapter<VH>() {
|
||||
var data: MutableList<D> = mutableListOf()
|
||||
|
||||
init {
|
||||
super.setHasStableIds(true)
|
||||
}
|
||||
|
||||
abstract override fun getItemId(position: Int): Long
|
||||
}
|
||||
|
||||
abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment() {
|
||||
abstract class OtterFragment<D : Any, A : OtterAdapter<D, *>> : Fragment() {
|
||||
companion object {
|
||||
const val OFFSCREEN_PAGES = 20
|
||||
}
|
||||
|
||||
abstract val viewRes: Int
|
||||
abstract val recycler: RecyclerView
|
||||
open val layoutManager: RecyclerView.LayoutManager get() = LinearLayoutManager(context)
|
||||
open val alwaysRefresh = true
|
||||
|
||||
lateinit var repository: Repository<D, *>
|
||||
lateinit var adapter: A
|
||||
|
||||
private var initialFetched = false
|
||||
private var moreLoading = false
|
||||
private var listener: Job? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
|
@ -46,32 +55,23 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
recycler.layoutManager = layoutManager
|
||||
(recycler.itemAnimator as? SimpleItemAnimator)?.supportsChangeAnimations = false
|
||||
recycler.adapter = adapter
|
||||
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
if (upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
recycler.setOnScrollChangeListener { _, _, _, _, _ ->
|
||||
if (recycler.computeVerticalScrollOffset() > 0 && !recycler.canScrollVertically(1)) {
|
||||
val offset = recycler.computeVerticalScrollOffset()
|
||||
|
||||
if (!moreLoading && offset > 0 && needsMoreOffscreenPages()) {
|
||||
moreLoading = true
|
||||
|
||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(Repository.Origin.Cache.origin)
|
||||
|
||||
if (adapter.data.isEmpty()) {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
swiper?.setOnRefreshListener {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
if (listener == null) {
|
||||
listener = lifecycleScope.launch(IO) {
|
||||
EventBus.get().collect { event ->
|
||||
|
@ -84,6 +84,25 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
fetch(Repository.Origin.Cache.origin)
|
||||
|
||||
if (alwaysRefresh && adapter.data.isEmpty()) {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
swiper?.setOnRefreshListener {
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
}
|
||||
|
||||
fun update() {
|
||||
swiper?.isRefreshing = true
|
||||
fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
open fun onDataFetched(data: List<D>) {}
|
||||
|
@ -91,20 +110,32 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
private fun fetch(upstreams: Int = Repository.Origin.Network.origin, size: Int = 0) {
|
||||
var first = size == 0
|
||||
|
||||
if (upstreams == Repository.Origin.Network.origin) {
|
||||
swiper?.isRefreshing = true
|
||||
if (!moreLoading && upstreams == Repository.Origin.Network.origin) {
|
||||
lifecycleScope.launch(Main) {
|
||||
swiper?.isRefreshing = true
|
||||
}
|
||||
}
|
||||
|
||||
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, hasMore ->
|
||||
moreLoading = true
|
||||
|
||||
repository.fetch(upstreams, size).untilNetwork(lifecycleScope, IO) { data, isCache, _, hasMore ->
|
||||
if (isCache && data.isEmpty()) {
|
||||
moreLoading = false
|
||||
|
||||
return@untilNetwork fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
if (isCache) {
|
||||
moreLoading = false
|
||||
|
||||
adapter.data = data.toMutableList()
|
||||
adapter.notifyDataSetChanged()
|
||||
|
||||
return@launch
|
||||
}
|
||||
|
||||
if (first && data.isNotEmpty()) {
|
||||
if (first) {
|
||||
adapter.data.clear()
|
||||
}
|
||||
|
||||
|
@ -112,22 +143,38 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
|
||||
adapter.data.addAll(data)
|
||||
|
||||
if (!hasMore) {
|
||||
swiper?.isRefreshing = false
|
||||
|
||||
withContext(IO) {
|
||||
if (adapter.data.isNotEmpty()) {
|
||||
try {
|
||||
repository.cacheId?.let { cacheId ->
|
||||
Cache.set(
|
||||
context,
|
||||
cacheId,
|
||||
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||
)
|
||||
}
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
}
|
||||
withContext(IO) {
|
||||
try {
|
||||
repository.cacheId?.let { cacheId ->
|
||||
Cache.set(
|
||||
context,
|
||||
cacheId,
|
||||
Gson().toJson(repository.cache(adapter.data)).toByteArray()
|
||||
)
|
||||
}
|
||||
} catch (e: ConcurrentModificationException) {
|
||||
}
|
||||
}
|
||||
|
||||
if (hasMore) {
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
if (!isCache && upstream.behavior == HttpUpstream.Behavior.Progressive) {
|
||||
if (first || needsMoreOffscreenPages()) {
|
||||
fetch(Repository.Origin.Network.origin, adapter.data.size)
|
||||
} else {
|
||||
moreLoading = false
|
||||
}
|
||||
} else {
|
||||
moreLoading = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(repository.upstream as? HttpUpstream<*, *>)?.let { upstream ->
|
||||
when (upstream.behavior) {
|
||||
HttpUpstream.Behavior.Progressive -> if (!hasMore || !moreLoading) swiper?.isRefreshing = false
|
||||
HttpUpstream.Behavior.AtOnce -> if (!hasMore) swiper?.isRefreshing = false
|
||||
HttpUpstream.Behavior.Single -> if (!hasMore) swiper?.isRefreshing = false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -142,4 +189,15 @@ abstract class FunkwhaleFragment<D : Any, A : FunkwhaleAdapter<D, *>> : Fragment
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun needsMoreOffscreenPages(): Boolean {
|
||||
view?.let {
|
||||
val offset = recycler.computeVerticalScrollOffset()
|
||||
val left = recycler.computeVerticalScrollRange() - recycler.height - offset
|
||||
|
||||
return left < (recycler.height * OFFSCREEN_PAGES)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
|
@ -10,7 +10,9 @@ import androidx.recyclerview.widget.RecyclerView
|
|||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.PlaylistTracksAdapter
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.ManagementPlaylistsRepository
|
||||
import com.github.apognu.otter.repositories.PlaylistTracksRepository
|
||||
import com.github.apognu.otter.repositories.Repository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
|
@ -19,11 +21,12 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAdapter>() {
|
||||
class PlaylistTracksFragment : OtterFragment<PlaylistTrack, PlaylistTracksAdapter>() {
|
||||
override val viewRes = R.layout.fragment_tracks
|
||||
override val recycler: RecyclerView get() = tracks
|
||||
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
lateinit var playlistsRepository: ManagementPlaylistsRepository
|
||||
|
||||
var albumId = 0
|
||||
var albumArtist = ""
|
||||
|
@ -53,9 +56,10 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
|
|||
albumCover = getString("albumCover") ?: ""
|
||||
}
|
||||
|
||||
adapter = PlaylistTracksAdapter(context, FavoriteListener())
|
||||
adapter = PlaylistTracksAdapter(context, FavoriteListener(), PlaylistListener())
|
||||
repository = PlaylistTracksRepository(context, albumId)
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
playlistsRepository = ManagementPlaylistsRepository(context)
|
||||
|
||||
watchEventBus()
|
||||
}
|
||||
|
@ -126,7 +130,7 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
|
|||
}
|
||||
|
||||
override fun onDataFetched(data: List<PlaylistTrack>) {
|
||||
data.map { it.track.album }.toSet().map { it.cover.original }.take(4).forEachIndexed { index, url ->
|
||||
data.map { it.track.album }.toSet().map { it?.cover() }.take(4).forEachIndexed { index, url ->
|
||||
val imageView = when (index) {
|
||||
0 -> cover_top_left
|
||||
1 -> cover_top_right
|
||||
|
@ -181,4 +185,17 @@ class PlaylistTracksFragment : FunkwhaleFragment<PlaylistTrack, PlaylistTracksAd
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class PlaylistListener : PlaylistTracksAdapter.OnPlaylistListener {
|
||||
override fun onMoveTrack(from: Int, to: Int) {
|
||||
playlistsRepository.move(albumId, from, to)
|
||||
}
|
||||
|
||||
override fun onRemoveTrackFromPlaylist(track: Track, index: Int) {
|
||||
lifecycleScope.launch(Main) {
|
||||
playlistsRepository.remove(albumId, track, index)
|
||||
update()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,9 +14,10 @@ import com.github.apognu.otter.utils.AppContext
|
|||
import com.github.apognu.otter.utils.Playlist
|
||||
import kotlinx.android.synthetic.main.fragment_playlists.*
|
||||
|
||||
class PlaylistsFragment : FunkwhaleFragment<Playlist, PlaylistsAdapter>() {
|
||||
class PlaylistsFragment : OtterFragment<Playlist, PlaylistsAdapter>() {
|
||||
override val viewRes = R.layout.fragment_playlists
|
||||
override val recycler: RecyclerView get() = playlists
|
||||
override val alwaysRefresh = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -62,6 +62,20 @@ class QueueFragment : BottomSheetDialogFragment() {
|
|||
included.queue?.visibility = View.GONE
|
||||
placeholder?.visibility = View.VISIBLE
|
||||
|
||||
queue_shuffle.setOnClickListener {
|
||||
CommandBus.send(Command.ShuffleQueue)
|
||||
}
|
||||
|
||||
queue_save.setOnClickListener {
|
||||
adapter?.data?.let {
|
||||
CommandBus.send(Command.AddToPlaylist(it))
|
||||
}
|
||||
}
|
||||
|
||||
queue_clear.setOnClickListener {
|
||||
CommandBus.send(Command.ClearQueue)
|
||||
}
|
||||
|
||||
refresh()
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,10 @@ import kotlinx.coroutines.Dispatchers.Main
|
|||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class RadiosFragment : FunkwhaleFragment<Radio, RadiosAdapter>() {
|
||||
class RadiosFragment : OtterFragment<Radio, RadiosAdapter>() {
|
||||
override val viewRes = R.layout.fragment_radios
|
||||
override val recycler: RecyclerView get() = radios
|
||||
override val alwaysRefresh = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
|
|
@ -22,8 +22,10 @@ class TrackInfoDetailsFragment : DialogFragment() {
|
|||
return TrackInfoDetailsFragment().apply {
|
||||
arguments = bundleOf(
|
||||
"artistName" to track.artist.name,
|
||||
"albumTitle" to track.album.title,
|
||||
"albumTitle" to track.album?.title,
|
||||
"trackTitle" to track.title,
|
||||
"trackCopyright" to track.copyright,
|
||||
"trackLicense" to track.license,
|
||||
"trackPosition" to track.position,
|
||||
"trackDuration" to track.bestUpload()?.duration?.toLong()?.let { toDurationString(it, showSeconds = true) },
|
||||
"trackBitrate" to track.bestUpload()?.bitrate?.let { "${it / 1000} Kbps" },
|
||||
|
@ -48,8 +50,10 @@ class TrackInfoDetailsFragment : DialogFragment() {
|
|||
properties.add(Pair(R.string.track_info_details_artist, getString("artistName")))
|
||||
properties.add(Pair(R.string.track_info_details_album, getString("albumTitle")))
|
||||
properties.add(Pair(R.string.track_info_details_track_title, getString("trackTitle")))
|
||||
properties.add(Pair(R.string.track_info_details_track_copyright, getString("trackCopyright")))
|
||||
properties.add(Pair(R.string.track_info_details_track_license, getString("trackLicense")))
|
||||
properties.add(Pair(R.string.track_info_details_track_duration, getString("trackDuration")))
|
||||
properties.add(Pair(R.string.track_info_details_track_position, getString("trackPosition")))
|
||||
properties.add(Pair(R.string.track_info_details_track_position, getInt("trackPosition").toString()))
|
||||
properties.add(Pair(R.string.track_info_details_track_bitrate, getString("trackBitrate")))
|
||||
properties.add(Pair(R.string.track_info_details_track_instance, getString("trackInstance")))
|
||||
}
|
||||
|
|
|
@ -3,16 +3,19 @@ package com.github.apognu.otter.fragments
|
|||
import android.os.Bundle
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.adapters.TracksAdapter
|
||||
import com.github.apognu.otter.repositories.FavoritedRepository
|
||||
import com.github.apognu.otter.repositories.FavoritesRepository
|
||||
import com.github.apognu.otter.repositories.TracksRepository
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.offline.Download
|
||||
import com.preference.PowerPreference
|
||||
import com.squareup.picasso.Picasso
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.android.synthetic.main.fragment_tracks.*
|
||||
|
@ -22,11 +25,12 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
||||
class TracksFragment : OtterFragment<Track, TracksAdapter>() {
|
||||
override val viewRes = R.layout.fragment_tracks
|
||||
override val recycler: RecyclerView get() = tracks
|
||||
|
||||
lateinit var favoritesRepository: FavoritesRepository
|
||||
lateinit var favoritedRepository: FavoritedRepository
|
||||
|
||||
private var albumId = 0
|
||||
private var albumArtist = ""
|
||||
|
@ -40,7 +44,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
|||
"albumId" to album.id,
|
||||
"albumArtist" to album.artist.name,
|
||||
"albumTitle" to album.title,
|
||||
"albumCover" to album.cover.original
|
||||
"albumCover" to album.cover()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +63,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
|||
adapter = TracksAdapter(context, FavoriteListener())
|
||||
repository = TracksRepository(context, albumId)
|
||||
favoritesRepository = FavoritesRepository(context)
|
||||
favoritedRepository = FavoritedRepository(context)
|
||||
|
||||
watchEventBus()
|
||||
}
|
||||
|
@ -104,8 +109,16 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
|||
}
|
||||
}
|
||||
|
||||
when (PowerPreference.getDefaultFile().getString("play_order")) {
|
||||
"in_order" -> play.text = getString(R.string.playback_play)
|
||||
else -> play.text = getString(R.string.playback_shuffle)
|
||||
}
|
||||
|
||||
play.setOnClickListener {
|
||||
CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||
when (PowerPreference.getDefaultFile().getString("play_order")) {
|
||||
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data))
|
||||
else -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||
}
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
|
@ -115,10 +128,25 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
|||
PopupMenu(context, actions, Gravity.START, R.attr.actionOverflowMenuStyle, 0).apply {
|
||||
inflate(R.menu.album)
|
||||
|
||||
menu.findItem(R.id.play_secondary)?.let { item ->
|
||||
when (PowerPreference.getDefaultFile().getString("play_order")) {
|
||||
"in_order" -> item.title = getString(R.string.playback_shuffle)
|
||||
else -> item.title = getString(R.string.playback_play)
|
||||
}
|
||||
}
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.play_secondary -> when (PowerPreference.getDefaultFile().getString("play_order")) {
|
||||
"in_order" -> CommandBus.send(Command.ReplaceQueue(adapter.data.shuffled()))
|
||||
else -> CommandBus.send(Command.ReplaceQueue(adapter.data))
|
||||
}
|
||||
|
||||
R.id.add_to_queue -> {
|
||||
CommandBus.send(Command.AddToQueue(adapter.data))
|
||||
when (PowerPreference.getDefaultFile().getString("play_order")) {
|
||||
"in_order" -> CommandBus.send(Command.AddToQueue(adapter.data))
|
||||
else -> CommandBus.send(Command.AddToQueue(adapter.data.shuffled()))
|
||||
}
|
||||
|
||||
context.toast("All tracks were added to your queue")
|
||||
}
|
||||
|
@ -185,6 +213,7 @@ class TracksFragment : FunkwhaleFragment<Track, TracksAdapter>() {
|
|||
adapter.currentTrack = track.apply {
|
||||
current = true
|
||||
}
|
||||
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,18 +3,19 @@ package com.github.apognu.otter.playback
|
|||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.media.MediaMetadata
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.app.NotificationCompat.MediaStyle
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.activities.MainActivity
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.maybeNormalizeUrl
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
|
@ -23,9 +24,6 @@ import kotlinx.coroutines.launch
|
|||
class MediaControlsManager(val context: Service, private val scope: CoroutineScope, private val mediaSession: MediaSessionCompat) {
|
||||
companion object {
|
||||
const val NOTIFICATION_ACTION_OPEN_QUEUE = 0
|
||||
const val NOTIFICATION_ACTION_PREVIOUS = 1
|
||||
const val NOTIFICATION_ACTION_TOGGLE = 2
|
||||
const val NOTIFICATION_ACTION_NEXT = 3
|
||||
}
|
||||
|
||||
private var notification: Notification? = null
|
||||
|
@ -43,18 +41,7 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
val openIntent = Intent(context, MainActivity::class.java).apply { action = NOTIFICATION_ACTION_OPEN_QUEUE.toString() }
|
||||
val openPendingIntent = PendingIntent.getActivity(context, 0, openIntent, 0)
|
||||
|
||||
val coverUrl = maybeNormalizeUrl(track.album.cover.original)
|
||||
val cover = coverUrl?.run { Picasso.get().load(coverUrl) }
|
||||
|
||||
mediaSession.setMetadata(MediaMetadataCompat.Builder().apply {
|
||||
putString(MediaMetadata.METADATA_KEY_ARTIST, track.artist.name)
|
||||
putString(MediaMetadata.METADATA_KEY_TITLE, track.title)
|
||||
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||
|
||||
cover?.let {
|
||||
try { putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, it.get()) } catch (_: Exception) {}
|
||||
}
|
||||
}.build())
|
||||
val coverUrl = maybeNormalizeUrl(track.album?.cover())
|
||||
|
||||
notification = NotificationCompat.Builder(
|
||||
context,
|
||||
|
@ -69,11 +56,16 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
)
|
||||
.setSmallIcon(R.drawable.ottershape)
|
||||
.run {
|
||||
if (cover != null) {
|
||||
try { setLargeIcon(cover.get()) } catch (_: Exception) {}
|
||||
coverUrl?.let {
|
||||
try {
|
||||
setLargeIcon(Picasso.get().load(coverUrl).get())
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
this
|
||||
} else this
|
||||
return@run this
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
.setContentTitle(track.title)
|
||||
.setContentText(track.artist.name)
|
||||
|
@ -82,58 +74,43 @@ class MediaControlsManager(val context: Service, private val scope: CoroutineSco
|
|||
.addAction(
|
||||
action(
|
||||
R.drawable.previous, context.getString(R.string.control_previous),
|
||||
NOTIFICATION_ACTION_PREVIOUS
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS
|
||||
)
|
||||
)
|
||||
.addAction(
|
||||
action(
|
||||
stateIcon, context.getString(R.string.control_toggle),
|
||||
NOTIFICATION_ACTION_TOGGLE
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE
|
||||
)
|
||||
)
|
||||
.addAction(
|
||||
action(
|
||||
R.drawable.next, context.getString(R.string.control_next),
|
||||
NOTIFICATION_ACTION_NEXT
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT
|
||||
)
|
||||
)
|
||||
.build()
|
||||
|
||||
notification?.let {
|
||||
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
if (playing) {
|
||||
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
} else {
|
||||
NotificationManagerCompat.from(context).notify(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
}
|
||||
}
|
||||
|
||||
if (playing) tick()
|
||||
Otter.get().mediaSession.connector.invalidateMediaSessionMetadata()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun tick() {
|
||||
notification?.let {
|
||||
context.startForeground(AppContext.NOTIFICATION_MEDIA_CONTROL, it)
|
||||
}
|
||||
fun remove() {
|
||||
NotificationManagerCompat.from(context).cancel(AppContext.NOTIFICATION_MEDIA_CONTROL)
|
||||
}
|
||||
|
||||
private fun action(icon: Int, title: String, id: Int): NotificationCompat.Action {
|
||||
val intent = Intent(context, MediaControlActionReceiver::class.java).apply { action = id.toString() }
|
||||
val pendingIntent = PendingIntent.getBroadcast(context, id, intent, 0)
|
||||
|
||||
return NotificationCompat.Action.Builder(icon, title, pendingIntent).build()
|
||||
}
|
||||
}
|
||||
|
||||
class MediaControlActionReceiver : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
when (intent?.action) {
|
||||
MediaControlsManager.NOTIFICATION_ACTION_PREVIOUS.toString() -> CommandBus.send(
|
||||
Command.PreviousTrack
|
||||
)
|
||||
MediaControlsManager.NOTIFICATION_ACTION_TOGGLE.toString() -> CommandBus.send(
|
||||
Command.ToggleState
|
||||
)
|
||||
MediaControlsManager.NOTIFICATION_ACTION_NEXT.toString() -> CommandBus.send(
|
||||
Command.NextTrack
|
||||
)
|
||||
private fun action(icon: Int, title: String, id: Long): NotificationCompat.Action {
|
||||
return MediaButtonReceiver.buildMediaButtonPendingIntent(context, id).run {
|
||||
NotificationCompat.Action.Builder(icon, title, this).build()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,88 @@
|
|||
package com.github.apognu.otter.playback
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.ResultReceiver
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import com.github.apognu.otter.utils.Command
|
||||
import com.github.apognu.otter.utils.CommandBus
|
||||
import com.google.android.exoplayer2.ControlDispatcher
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
|
||||
class MediaSession(private val context: Context) {
|
||||
var active = false
|
||||
|
||||
private val playbackStateBuilder = PlaybackStateCompat.Builder().apply {
|
||||
setActions(
|
||||
PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_PLAY or
|
||||
PlaybackStateCompat.ACTION_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||
PlaybackStateCompat.ACTION_SEEK_TO or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
)
|
||||
}
|
||||
|
||||
val session: MediaSessionCompat by lazy {
|
||||
MediaSessionCompat(context, context.packageName).apply {
|
||||
setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
|
||||
setPlaybackState(playbackStateBuilder.build())
|
||||
|
||||
isActive = true
|
||||
active = true
|
||||
}
|
||||
}
|
||||
|
||||
val connector: MediaSessionConnector by lazy {
|
||||
MediaSessionConnector(session).also {
|
||||
it.setQueueNavigator(OtterQueueNavigator())
|
||||
|
||||
it.setMediaButtonEventHandler { _, _, intent ->
|
||||
if (!active) {
|
||||
context.startService(Intent(context, PlayerService::class.java).apply {
|
||||
action = intent.action
|
||||
|
||||
intent.extras?.let { extras -> putExtras(extras) }
|
||||
})
|
||||
|
||||
return@setMediaButtonEventHandler true
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class OtterQueueNavigator : MediaSessionConnector.QueueNavigator {
|
||||
override fun onSkipToQueueItem(player: Player, controlDispatcher: ControlDispatcher, id: Long) {
|
||||
CommandBus.send(Command.PlayTrack(id.toInt()))
|
||||
}
|
||||
|
||||
override fun onCurrentWindowIndexChanged(player: Player) {}
|
||||
|
||||
override fun onCommand(player: Player, controlDispatcher: ControlDispatcher, command: String, extras: Bundle?, cb: ResultReceiver?) = true
|
||||
|
||||
override fun getSupportedQueueNavigatorActions(player: Player): Long {
|
||||
return PlaybackStateCompat.ACTION_PLAY_PAUSE or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_NEXT or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS or
|
||||
PlaybackStateCompat.ACTION_SKIP_TO_QUEUE_ITEM
|
||||
}
|
||||
|
||||
override fun onSkipToNext(player: Player, controlDispatcher: ControlDispatcher) {
|
||||
CommandBus.send(Command.NextTrack)
|
||||
}
|
||||
|
||||
override fun getActiveQueueItemId(player: Player?) = player?.currentWindowIndex?.toLong() ?: 0
|
||||
|
||||
override fun onSkipToPrevious(player: Player, controlDispatcher: ControlDispatcher) {
|
||||
CommandBus.send(Command.PreviousTrack)
|
||||
}
|
||||
|
||||
override fun onTimelineChanged(player: Player) {}
|
||||
}
|
|
@ -8,28 +8,33 @@ import android.content.IntentFilter
|
|||
import android.media.AudioAttributes
|
||||
import android.media.AudioFocusRequest
|
||||
import android.media.AudioManager
|
||||
import android.media.MediaMetadata
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
import com.github.apognu.otter.Otter
|
||||
import com.github.apognu.otter.R
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.google.android.exoplayer2.C
|
||||
import com.google.android.exoplayer2.ExoPlaybackException
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.source.TrackGroupArray
|
||||
import com.google.android.exoplayer2.trackselection.TrackSelectionArray
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import com.squareup.picasso.Picasso
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class PlayerService : Service() {
|
||||
companion object {
|
||||
const val INITIAL_COMMAND_KEY = "start_command"
|
||||
}
|
||||
|
||||
private var started = false
|
||||
private val scope: CoroutineScope = CoroutineScope(Job() + Main)
|
||||
|
||||
|
@ -40,9 +45,10 @@ class PlayerService : Service() {
|
|||
|
||||
private lateinit var queue: QueueManager
|
||||
private lateinit var mediaControlsManager: MediaControlsManager
|
||||
private lateinit var mediaSession: MediaSessionCompat
|
||||
private lateinit var player: SimpleExoPlayer
|
||||
|
||||
private val mediaMetadataBuilder = MediaMetadataCompat.Builder()
|
||||
|
||||
private lateinit var playerEventListener: PlayerEventListener
|
||||
private val headphonesUnpluggedReceiver = HeadphonesUnpluggedReceiver()
|
||||
|
||||
|
@ -51,13 +57,30 @@ class PlayerService : Service() {
|
|||
private lateinit var radioPlayer: RadioPlayer
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (!started) watchEventBus()
|
||||
intent?.action?.let {
|
||||
if (it == Intent.ACTION_MEDIA_BUTTON) {
|
||||
intent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
|
||||
when (key.keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE -> {
|
||||
if (hasAudioFocus(true)) MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
|
||||
Unit
|
||||
}
|
||||
else -> MediaButtonReceiver.handleIntent(Otter.get().mediaSession.session, intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!started) {
|
||||
watchEventBus()
|
||||
}
|
||||
|
||||
started = true
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
|
@ -66,7 +89,7 @@ class PlayerService : Service() {
|
|||
|
||||
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Build.VERSION_CODES.O.onApi {
|
||||
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).run {
|
||||
setAudioAttributes(AudioAttributes.Builder().run {
|
||||
setUsage(AudioAttributes.USAGE_MEDIA)
|
||||
|
@ -82,11 +105,7 @@ class PlayerService : Service() {
|
|||
}
|
||||
}
|
||||
|
||||
mediaSession = MediaSessionCompat(this, applicationContext.packageName).apply {
|
||||
isActive = true
|
||||
}
|
||||
|
||||
mediaControlsManager = MediaControlsManager(this, scope, mediaSession)
|
||||
mediaControlsManager = MediaControlsManager(this, scope, Otter.get().mediaSession.session)
|
||||
|
||||
player = SimpleExoPlayer.Builder(this).build().apply {
|
||||
playWhenReady = false
|
||||
|
@ -94,33 +113,25 @@ class PlayerService : Service() {
|
|||
playerEventListener = PlayerEventListener().also {
|
||||
addListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
MediaSessionConnector(mediaSession).also {
|
||||
it.setPlayer(this)
|
||||
it.setMediaButtonEventHandler { player, _, mediaButtonEvent ->
|
||||
mediaButtonEvent.extras?.getParcelable<KeyEvent>(Intent.EXTRA_KEY_EVENT)?.let { key ->
|
||||
if (key.action == KeyEvent.ACTION_UP) {
|
||||
when (key.keyCode) {
|
||||
KeyEvent.KEYCODE_MEDIA_PLAY -> state(true)
|
||||
KeyEvent.KEYCODE_MEDIA_PAUSE -> state(false)
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT -> player.next()
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS -> previousTrack()
|
||||
}
|
||||
}
|
||||
}
|
||||
Otter.get().mediaSession.active = true
|
||||
|
||||
true
|
||||
}
|
||||
Otter.get().mediaSession.connector.apply {
|
||||
setPlayer(player)
|
||||
|
||||
setMediaMetadataProvider {
|
||||
buildTrackMetadata(queue.current())
|
||||
}
|
||||
}
|
||||
|
||||
if (queue.current > -1) {
|
||||
player.prepare(queue.datasources, true, true)
|
||||
player.prepare(queue.datasources)
|
||||
|
||||
Cache.get(this, "progress")?.let { progress ->
|
||||
player.seekTo(queue.current, progress.readLine().toLong())
|
||||
|
||||
val (current, duration, percent) = progress(true)
|
||||
val (current, duration, percent) = getProgress(true)
|
||||
|
||||
ProgressBus.send(current, duration, percent)
|
||||
}
|
||||
|
@ -134,8 +145,6 @@ class PlayerService : Service() {
|
|||
CommandBus.get().collect { command ->
|
||||
when (command) {
|
||||
is Command.RefreshService -> {
|
||||
EventBus.send(Event.QueueChanged)
|
||||
|
||||
if (queue.metadata.isNotEmpty()) {
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
EventBus.send(Event.StateChanged(player.playWhenReady))
|
||||
|
@ -148,7 +157,7 @@ class PlayerService : Service() {
|
|||
queue.replace(command.queue)
|
||||
player.prepare(queue.datasources, true, true)
|
||||
|
||||
state(true)
|
||||
setPlaybackState(true)
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
@ -162,24 +171,23 @@ class PlayerService : Service() {
|
|||
queue.current = command.index
|
||||
player.seekTo(command.index, C.TIME_UNSET)
|
||||
|
||||
state(true)
|
||||
setPlaybackState(true)
|
||||
|
||||
CommandBus.send(Command.RefreshTrack(queue.current()))
|
||||
}
|
||||
|
||||
is Command.ToggleState -> toggle()
|
||||
is Command.SetState -> state(command.state)
|
||||
is Command.ToggleState -> togglePlayback()
|
||||
is Command.SetState -> setPlaybackState(command.state)
|
||||
|
||||
is Command.NextTrack -> {
|
||||
player.next()
|
||||
is Command.NextTrack -> skipToNextTrack()
|
||||
is Command.PreviousTrack -> skipToPreviousTrack()
|
||||
is Command.Seek -> seek(command.progress)
|
||||
|
||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||
ProgressBus.send(0, 0, 0)
|
||||
is Command.ClearQueue -> {
|
||||
queue.clear()
|
||||
player.stop()
|
||||
}
|
||||
is Command.PreviousTrack -> previousTrack()
|
||||
is Command.Seek -> progress(command.progress)
|
||||
|
||||
is Command.ClearQueue -> queue.clear()
|
||||
is Command.ShuffleQueue -> queue.shuffle()
|
||||
|
||||
is Command.PlayRadio -> {
|
||||
queue.clear()
|
||||
|
@ -191,10 +199,6 @@ class PlayerService : Service() {
|
|||
is Command.PinTrack -> PinService.download(this@PlayerService, command.track)
|
||||
is Command.PinTracks -> command.tracks.forEach { PinService.download(this@PlayerService, it) }
|
||||
}
|
||||
|
||||
if (player.playWhenReady) {
|
||||
mediaControlsManager.tick()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -212,7 +216,7 @@ class PlayerService : Service() {
|
|||
while (true) {
|
||||
delay(1000)
|
||||
|
||||
val (current, duration, percent) = progress()
|
||||
val (current, duration, percent) = getProgress()
|
||||
|
||||
if (player.playWhenReady) {
|
||||
ProgressBus.send(current, duration, percent)
|
||||
|
@ -223,8 +227,19 @@ class PlayerService : Service() {
|
|||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
|
||||
if (!player.playWhenReady) {
|
||||
NotificationManagerCompat.from(this).cancelAll()
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
override fun onDestroy() {
|
||||
scope.cancel()
|
||||
|
||||
try {
|
||||
unregisterReceiver(headphonesUnpluggedReceiver)
|
||||
} catch (_: Exception) {
|
||||
|
@ -241,23 +256,18 @@ class PlayerService : Service() {
|
|||
audioManager.abandonAudioFocus(audioFocusChangeListener)
|
||||
})
|
||||
|
||||
mediaSession.isActive = false
|
||||
mediaSession.release()
|
||||
|
||||
player.removeListener(playerEventListener)
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
player.release()
|
||||
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
Otter.get().mediaSession.active = false
|
||||
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun state(state: Boolean) {
|
||||
private fun setPlaybackState(state: Boolean) {
|
||||
if (!state) {
|
||||
val (progress, _, _) = progress()
|
||||
val (progress, _, _) = getProgress()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", progress.toString().toByteArray())
|
||||
}
|
||||
|
@ -266,6 +276,76 @@ class PlayerService : Service() {
|
|||
player.prepare(queue.datasources)
|
||||
}
|
||||
|
||||
if (hasAudioFocus(state)) {
|
||||
player.playWhenReady = state
|
||||
|
||||
EventBus.send(Event.StateChanged(state))
|
||||
}
|
||||
}
|
||||
|
||||
private fun togglePlayback() {
|
||||
setPlaybackState(!player.playWhenReady)
|
||||
}
|
||||
|
||||
private fun skipToPreviousTrack() {
|
||||
if (player.currentPosition > 5000) {
|
||||
return player.seekTo(0)
|
||||
}
|
||||
|
||||
player.previous()
|
||||
}
|
||||
|
||||
private fun skipToNextTrack() {
|
||||
player.next()
|
||||
|
||||
Cache.set(this@PlayerService, "progress", "0".toByteArray())
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
|
||||
private fun getProgress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||
if (!player.playWhenReady && !force) return progressCache
|
||||
|
||||
return queue.current()?.bestUpload()?.let { upload ->
|
||||
val current = player.currentPosition
|
||||
val duration = upload.duration.toFloat()
|
||||
val percent = ((current / (duration * 1000)) * 100).toInt()
|
||||
|
||||
progressCache = Triple(current.toInt(), duration.toInt(), percent)
|
||||
progressCache
|
||||
} ?: Triple(0, 0, 0)
|
||||
}
|
||||
|
||||
private fun seek(value: Int) {
|
||||
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
|
||||
|
||||
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
|
||||
|
||||
player.seekTo(duration.toLong())
|
||||
}
|
||||
|
||||
private fun buildTrackMetadata(track: Track?): MediaMetadataCompat {
|
||||
track?.let {
|
||||
val coverUrl = maybeNormalizeUrl(track.album?.cover())
|
||||
|
||||
return mediaMetadataBuilder.apply {
|
||||
putString(MediaMetadataCompat.METADATA_KEY_TITLE, track.title)
|
||||
putString(MediaMetadataCompat.METADATA_KEY_ARTIST, track.artist.name)
|
||||
putLong(MediaMetadata.METADATA_KEY_DURATION, (track.bestUpload()?.duration?.toLong() ?: 0L) * 1000)
|
||||
|
||||
try {
|
||||
runBlocking(IO) {
|
||||
this@apply.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, Picasso.get().load(coverUrl).get())
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}.build()
|
||||
}
|
||||
|
||||
return mediaMetadataBuilder.build()
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
private fun hasAudioFocus(state: Boolean): Boolean {
|
||||
var allowed = !state
|
||||
|
||||
if (!allowed) {
|
||||
|
@ -291,46 +371,10 @@ class PlayerService : Service() {
|
|||
)
|
||||
}
|
||||
|
||||
if (allowed) {
|
||||
player.playWhenReady = state
|
||||
|
||||
EventBus.send(Event.StateChanged(state))
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggle() {
|
||||
state(!player.playWhenReady)
|
||||
}
|
||||
|
||||
private fun previousTrack() {
|
||||
if (player.currentPosition > 5000) {
|
||||
return player.seekTo(0)
|
||||
}
|
||||
|
||||
player.previous()
|
||||
}
|
||||
|
||||
private fun progress(force: Boolean = false): Triple<Int, Int, Int> {
|
||||
if (!player.playWhenReady && !force) return progressCache
|
||||
|
||||
return queue.current()?.bestUpload()?.let { upload ->
|
||||
val current = player.currentPosition
|
||||
val duration = upload.duration.toFloat()
|
||||
val percent = ((current / (duration * 1000)) * 100).toInt()
|
||||
|
||||
progressCache = Triple(current.toInt(), duration.toInt(), percent)
|
||||
progressCache
|
||||
} ?: Triple(0, 0, 0)
|
||||
}
|
||||
|
||||
private fun progress(value: Int) {
|
||||
val duration = ((queue.current()?.bestUpload()?.duration ?: 0) * (value.toFloat() / 100)) * 1000
|
||||
|
||||
progressCache = Triple(duration.toInt(), queue.current()?.bestUpload()?.duration ?: 0, value)
|
||||
|
||||
player.seekTo(duration.toLong())
|
||||
return allowed
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
inner class PlayerEventListener : Player.EventListener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
super.onPlayerStateChanged(playWhenReady, playbackState)
|
||||
|
@ -346,7 +390,20 @@ class PlayerService : Service() {
|
|||
when (playbackState) {
|
||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), true)
|
||||
Player.STATE_BUFFERING -> EventBus.send(Event.Buffering(true))
|
||||
Player.STATE_ENDED -> EventBus.send(Event.PlaybackStopped)
|
||||
Player.STATE_ENDED -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
queue.current = 0
|
||||
player.seekTo(0, C.TIME_UNSET)
|
||||
|
||||
ProgressBus.send(0, 0, 0)
|
||||
}
|
||||
|
||||
Player.STATE_IDLE -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
return EventBus.send(Event.PlaybackStopped)
|
||||
}
|
||||
}
|
||||
|
||||
if (playbackState != Player.STATE_BUFFERING) EventBus.send(Event.Buffering(false))
|
||||
|
@ -355,9 +412,14 @@ class PlayerService : Service() {
|
|||
false -> {
|
||||
EventBus.send(Event.Buffering(false))
|
||||
|
||||
if (playbackState == Player.STATE_READY) {
|
||||
mediaControlsManager.updateNotification(queue.current(), false)
|
||||
stopForeground(false)
|
||||
Build.VERSION_CODES.N.onApi(
|
||||
{ stopForeground(STOP_FOREGROUND_DETACH) },
|
||||
{ stopForeground(false) }
|
||||
)
|
||||
|
||||
when (playbackState) {
|
||||
Player.STATE_READY -> mediaControlsManager.updateNotification(queue.current(), false)
|
||||
Player.STATE_IDLE -> mediaControlsManager.remove()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -366,8 +428,10 @@ class PlayerService : Service() {
|
|||
override fun onTracksChanged(trackGroups: TrackGroupArray, trackSelections: TrackSelectionArray) {
|
||||
super.onTracksChanged(trackGroups, trackSelections)
|
||||
|
||||
queue.current = player.currentWindowIndex
|
||||
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
||||
if (queue.current != player.currentWindowIndex) {
|
||||
queue.current = player.currentWindowIndex
|
||||
mediaControlsManager.updateNotification(queue.current(), player.playWhenReady)
|
||||
}
|
||||
|
||||
if (queue.get().isNotEmpty() && queue.current() == queue.get().last() && radioPlayer.isActive()) {
|
||||
scope.launch(IO) {
|
||||
|
@ -410,18 +474,18 @@ class PlayerService : Service() {
|
|||
AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
player.volume = 1f
|
||||
|
||||
state(stateWhenLostFocus)
|
||||
setPlaybackState(stateWhenLostFocus)
|
||||
stateWhenLostFocus = false
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
stateWhenLostFocus = false
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT -> {
|
||||
stateWhenLostFocus = player.playWhenReady
|
||||
state(false)
|
||||
setPlaybackState(false)
|
||||
}
|
||||
|
||||
AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
|
@ -431,4 +495,4 @@ class PlayerService : Service() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -178,5 +178,34 @@ class QueueManager(val context: Context) {
|
|||
metadata = mutableListOf()
|
||||
datasources.clear()
|
||||
current = -1
|
||||
|
||||
persist()
|
||||
}
|
||||
|
||||
fun shuffle() {
|
||||
if (metadata.size < 2) return
|
||||
|
||||
if (current == -1) {
|
||||
replace(metadata.shuffled())
|
||||
} else {
|
||||
move(current, 0)
|
||||
current = 0
|
||||
|
||||
val shuffled =
|
||||
metadata
|
||||
.drop(1)
|
||||
.shuffled()
|
||||
|
||||
while (metadata.size > 1) {
|
||||
datasources.removeMediaSource(metadata.size - 1)
|
||||
metadata.removeAt(metadata.size - 1)
|
||||
}
|
||||
|
||||
append(shuffled)
|
||||
}
|
||||
|
||||
persist()
|
||||
|
||||
EventBus.send(Event.QueueChanged)
|
||||
}
|
||||
}
|
|
@ -38,11 +38,11 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
|||
Cache.get(context, "radio_type")?.readLine()?.let { radio_type ->
|
||||
Cache.get(context, "radio_id")?.readLine()?.toInt()?.let { radio_id ->
|
||||
Cache.get(context, "radio_session")?.readLine()?.toInt()?.let { radio_session ->
|
||||
Cache.get(context, "radio_cookie")?.readLine()?.let { radio_cookie ->
|
||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||
session = radio_session
|
||||
cookie = radio_cookie
|
||||
}
|
||||
val cachedCookie = Cache.get(context, "radio_cookie")?.readLine()
|
||||
|
||||
currentRadio = Radio(radio_id, radio_type, "", "")
|
||||
session = radio_session
|
||||
cookie = cachedCookie
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -121,7 +121,7 @@ class RadioPlayer(val context: Context, val scope: CoroutineScope) {
|
|||
.authorize()
|
||||
.awaitObjectResult(gsonDeserializerOf(Track::class.java))
|
||||
|
||||
val favorites = favoritedRepository.fetch(Repository.Origin.Network.origin)
|
||||
val favorites = favoritedRepository.fetch(Repository.Origin.Cache.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
|
|
|
@ -4,7 +4,7 @@ import android.content.Context
|
|||
import com.github.apognu.otter.utils.Album
|
||||
import com.github.apognu.otter.utils.AlbumsCache
|
||||
import com.github.apognu.otter.utils.AlbumsResponse
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
|
@ -17,10 +17,10 @@ class AlbumsRepository(override val context: Context?, artistId: Int? = null) :
|
|||
|
||||
override val upstream: Upstream<Album> by lazy {
|
||||
val url =
|
||||
if (artistId == null) "/api/v1/albums/?playable=true"
|
||||
else "/api/v1/albums/?playable=true&artist=$artistId"
|
||||
if (artistId == null) "/api/v1/albums/?playable=true&ordering=title"
|
||||
else "/api/v1/albums/?playable=true&artist=$artistId&ordering=release_date"
|
||||
|
||||
HttpUpstream<Album, FunkwhaleResponse<Album>>(
|
||||
HttpUpstream<Album, OtterResponse<Album>>(
|
||||
HttpUpstream.Behavior.Progressive,
|
||||
url,
|
||||
object : TypeToken<AlbumsResponse>() {}.type
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.Track
|
||||
import com.github.apognu.otter.utils.TracksCache
|
||||
import com.github.apognu.otter.utils.TracksResponse
|
||||
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class ArtistTracksRepository(override val context: Context?, private val artistId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-artist-$artistId"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&artist=$artistId", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -4,14 +4,14 @@ import android.content.Context
|
|||
import com.github.apognu.otter.utils.Artist
|
||||
import com.github.apognu.otter.utils.ArtistsCache
|
||||
import com.github.apognu.otter.utils.ArtistsResponse
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import java.io.BufferedReader
|
||||
|
||||
class ArtistsRepository(override val context: Context?) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId = "artists"
|
||||
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Artist, OtterResponse<Artist>>(HttpUpstream.Behavior.Progressive, "/api/v1/artists/?playable=true&ordering=name", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -8,6 +8,7 @@ import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
|||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
@ -15,11 +16,13 @@ import java.io.BufferedReader
|
|||
|
||||
class FavoritesRepository(override val context: Context?) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "favorites.v2"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?favorites=true&playable=true&ordering=title", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
||||
private val favoritedRepository = FavoritedRepository(context)
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val downloaded = TracksRepository.getDownloadedIds() ?: listOf()
|
||||
|
||||
|
@ -51,6 +54,8 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
|
||||
favoritedRepository.update(context, scope)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -68,14 +73,22 @@ class FavoritesRepository(override val context: Context?) : Repository<Track, Tr
|
|||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
|
||||
favoritedRepository.update(context, scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class FavoritedRepository(override val context: Context?) : Repository<Int, FavoritedCache>() {
|
||||
override val cacheId = "favorited"
|
||||
override val upstream = HttpUpstream<Int, FunkwhaleResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Int, OtterResponse<Int>>(HttpUpstream.Behavior.Single, "/api/v1/favorites/tracks/all/?playable=true", object : TypeToken<FavoritedResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Int>) = FavoritedCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(FavoritedCache::class.java).deserialize(reader)
|
||||
|
||||
fun update(context: Context?, scope: CoroutineScope) {
|
||||
fetch(Origin.Network.origin).untilNetwork(scope, IO) { favorites, _, _, _ ->
|
||||
Cache.set(context, cacheId, Gson().toJson(cache(favorites)).toByteArray())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ import java.io.Reader
|
|||
import java.lang.reflect.Type
|
||||
import kotlin.math.ceil
|
||||
|
||||
class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
class HttpUpstream<D : Any, R : OtterResponse<D>>(val behavior: Behavior, private val url: String, private val type: Type) : Upstream<D> {
|
||||
enum class Behavior {
|
||||
Single, AtOnce, Progressive
|
||||
}
|
||||
|
@ -33,7 +33,7 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
|
|||
.buildUpon()
|
||||
.appendQueryParameter("page_size", AppContext.PAGE_SIZE.toString())
|
||||
.appendQueryParameter("page", page.toString())
|
||||
.appendQueryParameter("scope", Settings.getScope())
|
||||
.appendQueryParameter("scope", Settings.getScopes().joinToString(","))
|
||||
.build()
|
||||
.toString()
|
||||
|
||||
|
@ -41,24 +41,27 @@ class HttpUpstream<D : Any, R : FunkwhaleResponse<D>>(val behavior: Behavior, pr
|
|||
{ response ->
|
||||
val data = response.getData()
|
||||
|
||||
if (behavior == Behavior.Progressive || response.next == null) {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, false))
|
||||
} else {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, true))
|
||||
when (behavior) {
|
||||
Behavior.Single -> emit(Repository.Response(Repository.Origin.Network, data, page, false))
|
||||
Behavior.Progressive -> emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
|
||||
fetch(size + data.size).collect { emit(it) }
|
||||
else -> {
|
||||
emit(Repository.Response(Repository.Origin.Network, data, page, response.next != null))
|
||||
|
||||
if (response.next != null) fetch(size + data.size).collect { emit(it) }
|
||||
}
|
||||
}
|
||||
},
|
||||
{ error ->
|
||||
when (error.exception) {
|
||||
is RefreshError -> EventBus.send(Event.LogOut)
|
||||
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), false))
|
||||
else -> emit(Repository.Response(Repository.Origin.Network, listOf(), page, false))
|
||||
}
|
||||
}
|
||||
)
|
||||
}.flowOn(IO)
|
||||
|
||||
class GenericDeserializer<T : FunkwhaleResponse<*>>(val type: Type) : ResponseDeserializable<T> {
|
||||
class GenericDeserializer<T : OtterResponse<*>>(val type: Type) : ResponseDeserializable<T> {
|
||||
override fun deserialize(reader: Reader): T? {
|
||||
return Gson().fromJson(reader, type)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.PlaylistTrack
|
||||
import com.github.apognu.otter.utils.PlaylistTracksCache
|
||||
import com.github.apognu.otter.utils.PlaylistTracksResponse
|
||||
|
@ -14,7 +14,7 @@ import java.io.BufferedReader
|
|||
|
||||
class PlaylistTracksRepository(override val context: Context?, playlistId: Int) : Repository<PlaylistTrack, PlaylistTracksCache>() {
|
||||
override val cacheId = "tracks-playlist-$playlistId"
|
||||
override val upstream = HttpUpstream<PlaylistTrack, FunkwhaleResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<PlaylistTrack, OtterResponse<PlaylistTrack>>(HttpUpstream.Behavior.Single, "/api/v1/playlists/$playlistId/tracks/?playable=true", object : TypeToken<PlaylistTracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<PlaylistTrack>) = PlaylistTracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistTracksCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -1,18 +1,99 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.Playlist
|
||||
import com.github.apognu.otter.utils.PlaylistsCache
|
||||
import com.github.apognu.otter.utils.PlaylistsResponse
|
||||
import com.github.apognu.otter.utils.*
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitByteArrayResponseResult
|
||||
import com.github.kittinunf.fuel.coroutines.awaitObjectResponseResult
|
||||
import com.github.kittinunf.fuel.gson.gsonDeserializerOf
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.BufferedReader
|
||||
|
||||
data class PlaylistAdd(val tracks: List<Int>, val allow_duplicates: Boolean)
|
||||
|
||||
class PlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||
override val cacheId = "tracks-playlists"
|
||||
override val upstream = HttpUpstream<Playlist, FunkwhaleResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.Progressive, "/api/v1/playlists/?playable=true&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
||||
}
|
||||
|
||||
class ManagementPlaylistsRepository(override val context: Context?) : Repository<Playlist, PlaylistsCache>() {
|
||||
override val cacheId = "tracks-playlists-management"
|
||||
override val upstream = HttpUpstream<Playlist, OtterResponse<Playlist>>(HttpUpstream.Behavior.AtOnce, "/api/v1/playlists/?scope=me&ordering=name", object : TypeToken<PlaylistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Playlist>) = PlaylistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(PlaylistsCache::class.java).deserialize(reader)
|
||||
|
||||
suspend fun new(name: String): Int? {
|
||||
val body = mapOf("name" to name, "privacy_level" to "me")
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
val (_, response, result) = request
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitObjectResponseResult(gsonDeserializerOf(Playlist::class.java))
|
||||
|
||||
if (response.statusCode != 201) return null
|
||||
|
||||
return result.get().id
|
||||
}
|
||||
|
||||
fun add(id: Int, tracks: List<Track>) {
|
||||
val body = PlaylistAdd(tracks.map { it.id }, false)
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/add/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
request
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun remove(id: Int, track: Track, index: Int) {
|
||||
val body = mapOf("index" to index)
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/remove/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
request
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
}
|
||||
|
||||
fun move(id: Int, from: Int, to: Int) {
|
||||
val body = mapOf("from" to from, "to" to to)
|
||||
|
||||
val request = Fuel.post(mustNormalizeUrl("/api/v1/playlists/${id}/move/")).apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
header("Authorization", "Bearer ${Settings.getAccessToken()}")
|
||||
}
|
||||
}
|
||||
|
||||
scope.launch(Dispatchers.IO) {
|
||||
request
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(body))
|
||||
.awaitByteArrayResponseResult()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.FunkwhaleResponse
|
||||
import com.github.apognu.otter.utils.OtterResponse
|
||||
import com.github.apognu.otter.utils.Radio
|
||||
import com.github.apognu.otter.utils.RadiosCache
|
||||
import com.github.apognu.otter.utils.RadiosResponse
|
||||
|
@ -11,7 +11,7 @@ import java.io.BufferedReader
|
|||
|
||||
class RadiosRepository(override val context: Context?) : Repository<Radio, RadiosCache>() {
|
||||
override val cacheId = "radios"
|
||||
override val upstream = HttpUpstream<Radio, FunkwhaleResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/", object : TypeToken<RadiosResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Radio, OtterResponse<Radio>>(HttpUpstream.Behavior.Progressive, "/api/v1/radios/radios/?ordering=name", object : TypeToken<RadiosResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Radio>) = RadiosCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(RadiosCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package com.github.apognu.otter.repositories
|
||||
|
||||
import android.content.Context
|
||||
import com.github.apognu.otter.utils.AppContext
|
||||
import com.github.apognu.otter.utils.Cache
|
||||
import com.github.apognu.otter.utils.CacheItem
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -8,6 +9,7 @@ import kotlinx.coroutines.Dispatchers.IO
|
|||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import java.io.BufferedReader
|
||||
import kotlin.math.ceil
|
||||
|
||||
interface Upstream<D> {
|
||||
fun fetch(size: Int = 0): Flow<Repository.Response<D>>
|
||||
|
@ -21,7 +23,7 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
|||
Network(0b10)
|
||||
}
|
||||
|
||||
data class Response<D>(val origin: Origin, val data: List<D>, val hasMore: Boolean)
|
||||
data class Response<D>(val origin: Origin, val data: List<D>, val page: Int, val hasMore: Boolean)
|
||||
|
||||
abstract val context: Context?
|
||||
abstract val cacheId: String?
|
||||
|
@ -39,17 +41,19 @@ abstract class Repository<D : Any, C : CacheItem<D>> {
|
|||
cacheId?.let { cacheId ->
|
||||
Cache.get(context, cacheId)?.let { reader ->
|
||||
uncache(reader)?.let { cache ->
|
||||
emit(Response(Origin.Cache, cache.data, false))
|
||||
return@flow emit(Response(Origin.Cache, cache.data, ceil(cache.data.size / AppContext.PAGE_SIZE.toDouble()).toInt(), false))
|
||||
}
|
||||
}
|
||||
|
||||
return@flow emit(Response(Origin.Cache, listOf(), 1, false))
|
||||
}
|
||||
}.flowOn(IO)
|
||||
|
||||
private fun fromNetwork(size: Int) = flow {
|
||||
upstream
|
||||
.fetch(size)
|
||||
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.hasMore) }
|
||||
.collect { response -> emit(Response(Origin.Network, response.data, response.hasMore)) }
|
||||
.map { response -> Response(Origin.Network, onDataFetched(response.data), response.page, response.hasMore) }
|
||||
.collect { response -> emit(Response(Origin.Network, response.data, response.page, response.hasMore)) }
|
||||
}
|
||||
|
||||
protected open fun onDataFetched(data: List<D>) = data
|
||||
|
|
|
@ -10,15 +10,16 @@ import kotlinx.coroutines.flow.toList
|
|||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.BufferedReader
|
||||
|
||||
class TracksSearchRepository(override val context: Context?, query: String) : Repository<Track, TracksCache>() {
|
||||
class TracksSearchRepository(override val context: Context?, var query: String) : Repository<Track, TracksCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream: Upstream<Track>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&q=$query", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
|
@ -40,17 +41,19 @@ class TracksSearchRepository(override val context: Context?, query: String) : Re
|
|||
}
|
||||
}
|
||||
|
||||
class ArtistsSearchRepository(override val context: Context?, query: String) : Repository<Artist, ArtistsCache>() {
|
||||
class ArtistsSearchRepository(override val context: Context?, var query: String) : Repository<Artist, ArtistsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream = HttpUpstream<Artist, FunkwhaleResponse<Artist>>(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
override val upstream: Upstream<Artist>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/artists/?playable=true&q=$query", object : TypeToken<ArtistsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Artist>) = ArtistsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(ArtistsCache::class.java).deserialize(reader)
|
||||
}
|
||||
|
||||
class AlbumsSearchRepository(override val context: Context?, query: String) : Repository<Album, AlbumsCache>() {
|
||||
class AlbumsSearchRepository(override val context: Context?, var query: String) : Repository<Album, AlbumsCache>() {
|
||||
override val cacheId: String? = null
|
||||
override val upstream = HttpUpstream<Album, FunkwhaleResponse<Album>>(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
|
||||
override val upstream: Upstream<Album>
|
||||
get() = HttpUpstream(HttpUpstream.Behavior.AtOnce, "/api/v1/albums/?playable=true&q=$query", object : TypeToken<AlbumsResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Album>) = AlbumsCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(AlbumsCache::class.java).deserialize(reader)
|
||||
|
|
|
@ -13,7 +13,7 @@ import java.io.BufferedReader
|
|||
|
||||
class TracksRepository(override val context: Context?, albumId: Int) : Repository<Track, TracksCache>() {
|
||||
override val cacheId = "tracks-album-$albumId"
|
||||
override val upstream = HttpUpstream<Track, FunkwhaleResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId", object : TypeToken<TracksResponse>() {}.type)
|
||||
override val upstream = HttpUpstream<Track, OtterResponse<Track>>(HttpUpstream.Behavior.AtOnce, "/api/v1/tracks/?playable=true&album=$albumId&ordering=disc_number,position", object : TypeToken<TracksResponse>() {}.type)
|
||||
|
||||
override fun cache(data: List<Track>) = TracksCache(data)
|
||||
override fun uncache(reader: BufferedReader) = gsonDeserializerOf(TracksCache::class.java).deserialize(reader)
|
||||
|
@ -38,7 +38,7 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
|
|||
}
|
||||
|
||||
override fun onDataFetched(data: List<Track>): List<Track> = runBlocking {
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Network.origin)
|
||||
val favorites = FavoritedRepository(context).fetch(Origin.Cache.origin)
|
||||
.map { it.data }
|
||||
.toList()
|
||||
.flatten()
|
||||
|
@ -56,6 +56,6 @@ class TracksRepository(override val context: Context?, albumId: Int) : Repositor
|
|||
}
|
||||
|
||||
track
|
||||
}.sortedBy { it.position }
|
||||
}.sortedWith(compareBy({ it.disc_number }, { it.position }))
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import kotlinx.coroutines.flow.conflate
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
sealed class Command {
|
||||
class StartService(val command: Command) : Command()
|
||||
object RefreshService : Command()
|
||||
|
||||
object ToggleState : Command()
|
||||
|
@ -21,11 +22,13 @@ sealed class Command {
|
|||
class Seek(val progress: Int) : Command()
|
||||
|
||||
class AddToQueue(val tracks: List<Track>) : Command()
|
||||
class AddToPlaylist(val tracks: List<Track>) : Command()
|
||||
class PlayNext(val track: Track) : Command()
|
||||
class ReplaceQueue(val queue: List<Track>, val fromRadio: Boolean = false) : Command()
|
||||
class RemoveFromQueue(val track: Track) : Command()
|
||||
class MoveFromQueue(val oldPosition: Int, val newPosition: Int) : Command()
|
||||
object ClearQueue : Command()
|
||||
object ShuffleQueue : Command()
|
||||
class PlayRadio(val radio: Radio) : Command()
|
||||
|
||||
class SetRepeatMode(val mode: Int) : Command()
|
||||
|
|
|
@ -17,10 +17,10 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.launch
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, hasMore: Boolean) -> Unit) {
|
||||
inline fun <D> Flow<Repository.Response<D>>.untilNetwork(scope: CoroutineScope, context: CoroutineContext = Main, crossinline callback: (data: List<D>, isCache: Boolean, page: Int, hasMore: Boolean) -> Unit) {
|
||||
scope.launch(context) {
|
||||
collect { data ->
|
||||
callback(data.data, data.origin == Repository.Origin.Cache, data.hasMore)
|
||||
callback(data.data, data.origin == Repository.Origin.Cache, data.page, data.hasMore)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,46 +17,47 @@ class RadiosCache(data: List<Radio>) : CacheItem<Radio>(data)
|
|||
class FavoritedCache(data: List<Int>) : CacheItem<Int>(data)
|
||||
class QueueCache(data: List<Track>) : CacheItem<Track>(data)
|
||||
|
||||
abstract class FunkwhaleResponse<D : Any> {
|
||||
abstract class OtterResponse<D : Any> {
|
||||
abstract val count: Int
|
||||
abstract val next: String?
|
||||
|
||||
abstract fun getData(): List<D>
|
||||
}
|
||||
|
||||
data class UserResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
|
||||
data class UserResponse(override val count: Int, override val next: String?, val results: List<Artist>) : OtterResponse<Artist>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : FunkwhaleResponse<Artist>() {
|
||||
data class ArtistsResponse(override val count: Int, override val next: String?, val results: List<Artist>) : OtterResponse<Artist>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class AlbumsResponse(override val count: Int, override val next: String?, val results: AlbumList) : FunkwhaleResponse<Album>() {
|
||||
data class AlbumsResponse(override val count: Int, override val next: String?, val results: AlbumList) : OtterResponse<Album>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class TracksResponse(override val count: Int, override val next: String?, val results: List<Track>) : FunkwhaleResponse<Track>() {
|
||||
data class TracksResponse(override val count: Int, override val next: String?, val results: List<Track>) : OtterResponse<Track>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class FavoritedResponse(override val count: Int, override val next: String?, val results: List<Favorited>) : FunkwhaleResponse<Int>() {
|
||||
data class FavoritedResponse(override val count: Int, override val next: String?, val results: List<Favorited>) : OtterResponse<Int>() {
|
||||
override fun getData() = results.map { it.track }
|
||||
}
|
||||
|
||||
data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : FunkwhaleResponse<Playlist>() {
|
||||
data class PlaylistsResponse(override val count: Int, override val next: String?, val results: List<Playlist>) : OtterResponse<Playlist>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class PlaylistTracksResponse(override val count: Int, override val next: String?, val results: List<PlaylistTrack>) : FunkwhaleResponse<PlaylistTrack>() {
|
||||
data class PlaylistTracksResponse(override val count: Int, override val next: String?, val results: List<PlaylistTrack>) : OtterResponse<PlaylistTrack>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class RadiosResponse(override val count: Int, override val next: String?, val results: List<Radio>) : FunkwhaleResponse<Radio>() {
|
||||
data class RadiosResponse(override val count: Int, override val next: String?, val results: List<Radio>) : OtterResponse<Radio>() {
|
||||
override fun getData() = results
|
||||
}
|
||||
|
||||
data class Covers(val original: String)
|
||||
data class Covers(val urls: CoverUrls)
|
||||
data class CoverUrls(val original: String)
|
||||
|
||||
typealias AlbumList = List<Album>
|
||||
|
||||
|
@ -70,11 +71,12 @@ data class Album(
|
|||
val id: Int,
|
||||
val artist: Artist,
|
||||
val title: String,
|
||||
val cover: Covers
|
||||
val cover: Covers?,
|
||||
val release_date: String?
|
||||
) : SearchResult {
|
||||
data class Artist(val name: String)
|
||||
|
||||
override fun cover() = cover.original
|
||||
override fun cover() = cover?.urls?.original
|
||||
override fun title() = title
|
||||
override fun subtitle() = artist.name
|
||||
}
|
||||
|
@ -86,27 +88,40 @@ data class Artist(
|
|||
) : SearchResult {
|
||||
data class Album(
|
||||
val title: String,
|
||||
val cover: Covers
|
||||
val cover: Covers?
|
||||
)
|
||||
|
||||
override fun cover() = albums?.getOrNull(0)?.cover?.original
|
||||
override fun cover(): String? = albums?.getOrNull(0)?.cover?.urls?.original
|
||||
override fun title() = name
|
||||
override fun subtitle() = "Artist"
|
||||
}
|
||||
|
||||
data class Track(
|
||||
val id: Int,
|
||||
val id: Int = 0,
|
||||
val title: String,
|
||||
val artist: Artist,
|
||||
val album: Album,
|
||||
val position: Int,
|
||||
val uploads: List<Upload>
|
||||
val album: Album?,
|
||||
val disc_number: Int = 0,
|
||||
val position: Int = 0,
|
||||
val uploads: List<Upload> = listOf(),
|
||||
val copyright: String? = null,
|
||||
val license: String? = null
|
||||
) : SearchResult {
|
||||
var current: Boolean = false
|
||||
var favorite: Boolean = false
|
||||
var cached: Boolean = false
|
||||
var downloaded: Boolean = false
|
||||
|
||||
companion object {
|
||||
fun fromDownload(download: DownloadInfo): Track = Track(
|
||||
id = download.id,
|
||||
title = download.title,
|
||||
artist = Artist(0, download.artist, listOf()),
|
||||
album = Album(0, Album.Artist(""), "", Covers(CoverUrls("")), ""),
|
||||
uploads = listOf(Upload(download.contentId, 0, 0))
|
||||
)
|
||||
}
|
||||
|
||||
data class Upload(
|
||||
val listen_url: String,
|
||||
val duration: Int,
|
||||
|
@ -130,7 +145,7 @@ data class Track(
|
|||
}
|
||||
}
|
||||
|
||||
override fun cover() = album.cover.original
|
||||
override fun cover() = album?.cover?.urls?.original
|
||||
override fun title() = title
|
||||
override fun subtitle() = artist.name
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import android.widget.Toast
|
|||
import com.google.android.exoplayer2.util.Log
|
||||
import com.preference.PowerPreference
|
||||
import java.net.URI
|
||||
import java.net.URL
|
||||
|
||||
fun Context?.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
||||
if (this != null) {
|
||||
|
@ -13,12 +12,29 @@ fun Context?.toast(message: String, length: Int = Toast.LENGTH_SHORT) {
|
|||
}
|
||||
}
|
||||
|
||||
fun Any.log(message: String) {
|
||||
Log.d("FUNKWHALE", "${this.javaClass.simpleName}: $message")
|
||||
private fun logClassName(): String {
|
||||
val known = setOf(
|
||||
"dalvik.system.VMStack",
|
||||
"java.lang.Thread",
|
||||
"com.github.apognu.otter.utils.UtilKt"
|
||||
)
|
||||
|
||||
Thread.currentThread().stackTrace.forEach {
|
||||
if (!known.contains(it.className)) {
|
||||
val className = it.className.split('.').last()
|
||||
val line = it.lineNumber
|
||||
|
||||
return "$className:$line"
|
||||
}
|
||||
}
|
||||
|
||||
return "UNKNOWN"
|
||||
}
|
||||
|
||||
fun Any.log() {
|
||||
Log.d("FUNKWHALE", this.toString())
|
||||
fun Any?.log(prefix: String? = null) {
|
||||
prefix?.let {
|
||||
Log.d("OTTER", "${logClassName()} - $prefix: $this")
|
||||
} ?: Log.d("OTTER", "${logClassName()} - $this")
|
||||
}
|
||||
|
||||
fun maybeNormalizeUrl(rawUrl: String?): String? {
|
||||
|
@ -35,9 +51,7 @@ fun mustNormalizeUrl(rawUrl: String): String {
|
|||
val fallbackHost = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("hostname")
|
||||
val uri = URI(rawUrl).takeIf { it.host != null } ?: URI("$fallbackHost$rawUrl")
|
||||
|
||||
return uri.toURL().run {
|
||||
URL("https", host, file)
|
||||
}.toString()
|
||||
return uri.toString()
|
||||
}
|
||||
|
||||
fun toDurationString(duration: Long, showSeconds: Boolean = false): String {
|
||||
|
@ -61,5 +75,6 @@ object Settings {
|
|||
fun getAccessToken(): String = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getString("access_token", "")
|
||||
fun isAnonymous() = PowerPreference.getFileByName(AppContext.PREFS_CREDENTIALS).getBoolean("anonymous", false)
|
||||
fun areExperimentsEnabled() = PowerPreference.getDefaultFile().getBoolean("experiments", false)
|
||||
fun getScope() = PowerPreference.getDefaultFile().getString("scope", "all")
|
||||
|
||||
fun getScopes() = PowerPreference.getDefaultFile().getString("scope", "all").split(",")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
package com.github.apognu.otter.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
|
||||
class DisableableFrameLayout : FrameLayout {
|
||||
var callback: ((MotionEvent?) -> Boolean)? = null
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style)
|
||||
|
||||
override fun onInterceptTouchEvent(event: MotionEvent?): Boolean {
|
||||
callback?.let {
|
||||
return !it(event)
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
fun setShouldRegisterTouch(callback: (event: MotionEvent?) -> Boolean) {
|
||||
this.callback = callback
|
||||
}
|
||||
}
|
|
@ -72,7 +72,7 @@ class NowPlayingView : MaterialCardView {
|
|||
}
|
||||
|
||||
inner class OnGestureDetection : GestureDetector.SimpleOnGestureListener() {
|
||||
var maxHeight = 0
|
||||
private var maxHeight = 0
|
||||
private var minHeight = 0
|
||||
private var maxMargin = 0
|
||||
|
||||
|
@ -100,8 +100,6 @@ class NowPlayingView : MaterialCardView {
|
|||
initialTouchY = e.rawY
|
||||
lastTouchY = e.rawY
|
||||
|
||||
flingAnimator?.cancel()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
|
|
@ -1 +1 @@
|
|||
apognu@gmail.com
|
||||
otter@support.popineau.eu
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
https://github.com/apognu/otter
|
|
@ -2,5 +2,8 @@ Otter is a simple music player that allows you to stream the audio content of yo
|
|||
|
||||
This app requires an account on a Funkwhale instance to work.
|
||||
|
||||
You can get support or take a part in Otter's development by visiting our GitHub project or join us on Matrix.
|
||||
|
||||
Source code : https://github.com/apognu/otter
|
||||
Matrix room: https://matrix.to/#/#otter:matrix.org
|
||||
Funkwhale : https://funkwhale.audio
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
Otter est un lecteur de musique basique qui vous permet de profiter du contenu audio de votre instance Funkwhale.
|
||||
|
||||
Cette application nécessite un compte sur un instance Funkwhale pour fonctionner.
|
||||
Cette application nécessite un compte sur une instance Funkwhale pour fonctionner.
|
||||
|
||||
Vous pouvez obtenir de l'aide ou participer au développement d'Otter en vous rendant sur notre projet GitHub ou nous rejoindre sur Matrix.
|
||||
|
||||
Code source : https://github.com/apognu/otter
|
||||
Funkwhale : https://funkwhale.audio
|
||||
|
|
|
@ -1 +1 @@
|
|||
../../../../../../fastlane/metadata/android/en-US/changelogs/1000020.txt
|
||||
../../../../../../fastlane/metadata/android/en-US/changelogs/1000021.txt
|
|
@ -1 +1 @@
|
|||
../../../../../../fastlane/metadata/android/fr-FR/changelogs/1000020.txt
|
||||
../../../../../../fastlane/metadata/android/fr-FR/changelogs/1000021.txt
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,10L2,10v2h12v-2zM14,6L2,6v2h12L14,6zM18,14v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zM2,16h8v-2L2,14v2z"/>
|
||||
</vector>
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<solid android:color="@color/elevatedSurface" />
|
||||
<padding android:top="5dp" android:left="5dp" android:right="5dp" android:bottom="5dp" />
|
||||
|
||||
</shape>
|
Binary file not shown.
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners android:radius="3dp" />
|
||||
<solid android:color="@color/ripple" />
|
||||
<padding android:top="4dp" android:left="8dp" android:right="8dp" android:bottom="4dp" />
|
||||
|
||||
</shape>
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,10h12v2L4,12zM4,6h12v2L4,8zM4,14h8v2L4,16zM14,14v6l5,-3z"/>
|
||||
</vector>
|
|
@ -5,6 +5,7 @@
|
|||
android:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/elevatedSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -97,24 +98,67 @@
|
|||
android:id="@+id/now_playing_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingEnd="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp">
|
||||
android:paddingStart="32dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="32dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_title"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/itemTitle"
|
||||
android:textSize="18sp"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Muse" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/itemTitle"
|
||||
android:textSize="18sp"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Muse" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_add_to_playlist"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/alt_album_cover"
|
||||
android:src="@drawable/add_to_playlist" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_favorite"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/alt_album_cover"
|
||||
android:src="@drawable/favorite" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_info"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/alt_track_info"
|
||||
android:src="@drawable/more"
|
||||
android:tint="@color/controlForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/now_playing_details_progress"
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
<com.github.apognu.otter.views.DisableableFrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:id="@+id/name"
|
||||
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="@string/playlist_add_to_new"
|
||||
app:boxStrokeColor="@color/controlForeground"
|
||||
app:hintTextColor="@color/controlForeground"
|
||||
app:placeholderTextColor="@color/controlForeground">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/create"
|
||||
style="@style/Widget.MaterialComponents.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginHorizontal="16dp"
|
||||
android:enabled="false"
|
||||
android:text="@string/playlist_add_to_create"
|
||||
android:textColor="@color/controlForeground"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/playlists"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/row_playlist" />
|
||||
|
||||
</LinearLayout>
|
|
@ -5,6 +5,7 @@
|
|||
android:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/elevatedSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
|
@ -126,18 +127,10 @@
|
|||
android:layout_height="32dp"
|
||||
android:layout_gravity="top|end"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/alt_track_info"
|
||||
android:src="@drawable/more" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_favorite"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/alt_album_cover"
|
||||
android:src="@drawable/favorite" />
|
||||
android:src="@drawable/more"
|
||||
android:tint="@color/controlForeground" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -150,19 +143,52 @@
|
|||
android:orientation="vertical"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_title"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/itemTitle"
|
||||
android:textSize="18sp"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Muse" />
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/itemTitle"
|
||||
android:textSize="18sp"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_artist"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Muse" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_add_to_playlist"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/playlist_add_to"
|
||||
android:src="@drawable/add_to_playlist" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_favorite"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/alt_album_cover"
|
||||
android:src="@drawable/favorite" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/now_playing_details_progress"
|
||||
|
@ -204,8 +230,8 @@
|
|||
<ImageButton
|
||||
android:id="@+id/now_playing_details_previous"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/control_previous"
|
||||
android:src="@drawable/previous" />
|
||||
|
@ -222,8 +248,8 @@
|
|||
<ImageButton
|
||||
android:id="@+id/now_playing_details_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next" />
|
||||
|
|
|
@ -1,8 +1,60 @@
|
|||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/surface"
|
||||
android:layout_height="wrap_content">
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="8dp"
|
||||
android:paddingVertical="4dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/queue_save"
|
||||
style="@style/Widget.MaterialComponents.Button.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginEnd="4dp"
|
||||
android:backgroundTint="@color/colorPrimary"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/playback_queue_save"
|
||||
android:textColor="@android:color/white"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/playlist"
|
||||
app:iconTint="@android:color/white"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/queue_shuffle"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton.Icon"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginHorizontal="4dp"
|
||||
android:layout_weight="1"
|
||||
android:text="@string/playback_shuffle"
|
||||
android:textColor="@color/controlForeground"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/shuffle"
|
||||
app:iconTint="@color/controlForeground"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/queue_clear"
|
||||
style="@style/AppTheme.IconButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="4dp"
|
||||
android:layout_weight="0"
|
||||
android:textSize="12sp"
|
||||
app:icon="@drawable/delete"
|
||||
app:iconTint="@color/controlForeground"
|
||||
app:rippleColor="@color/ripple" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/queue"
|
||||
|
@ -26,4 +78,4 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
</LinearLayout>
|
|
@ -22,6 +22,7 @@
|
|||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
@ -44,4 +45,12 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/release_date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_weight="0"
|
||||
android:background="@drawable/pill" />
|
||||
|
||||
</LinearLayout>
|
|
@ -8,11 +8,29 @@
|
|||
android:title="@string/toolbar_search"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_only_my_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/only_my_music"
|
||||
app:showAsAction="never" />
|
||||
<item android:title="@string/filters">
|
||||
<menu>
|
||||
<group android:checkableBehavior="all">
|
||||
<item
|
||||
android:id="@+id/nav_all_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/fiters_all"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_my_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/filters_my_music"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_followed"
|
||||
android:checkable="true"
|
||||
android:title="@string/filters_followed"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_downloads"
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/play_secondary"
|
||||
android:title="@string/playback_shuffle" />
|
||||
|
||||
<item
|
||||
android:id="@+id/add_to_queue"
|
||||
android:title="@string/playback_queue" />
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<item
|
||||
android:id="@+id/track_add_to_playlist"
|
||||
android:title="@string/playlist_add_to" />
|
||||
|
||||
<item
|
||||
android:id="@+id/queue_remove"
|
||||
android:title="@string/playback_queue_remove_item" />
|
||||
|
|
|
@ -9,8 +9,17 @@
|
|||
android:id="@+id/track_play_next"
|
||||
android:title="@string/playback_queue_play_next" />
|
||||
|
||||
<item
|
||||
android:id="@+id/track_add_to_playlist"
|
||||
android:title="@string/playlist_add_to" />
|
||||
|
||||
<item
|
||||
android:id="@+id/track_pin"
|
||||
android:title="@string/playback_queue_download" />
|
||||
|
||||
<item
|
||||
android:id="@+id/track_remove_from_playlist"
|
||||
android:visible="false"
|
||||
android:title="@string/playback_queue_remove_item" />
|
||||
|
||||
</menu>
|
|
@ -14,11 +14,29 @@
|
|||
android:title="@string/toolbar_search"
|
||||
app:showAsAction="ifRoom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_only_my_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/only_my_music"
|
||||
app:showAsAction="never" />
|
||||
<item android:title="@string/filters">
|
||||
<menu>
|
||||
<group android:checkableBehavior="all">
|
||||
<item
|
||||
android:id="@+id/nav_all_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/fiters_all"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_my_music"
|
||||
android:checkable="true"
|
||||
android:title="@string/filters_my_music"
|
||||
app:showAsAction="never" />
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_followed"
|
||||
android:checkable="true"
|
||||
android:title="@string/filters_followed"
|
||||
app:showAsAction="never" />
|
||||
</group>
|
||||
</menu>
|
||||
</item>
|
||||
|
||||
<item
|
||||
android:id="@+id/nav_downloads"
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
<string name="login_submit">Anmelden</string>
|
||||
<string name="login_logging_in">Einloggen</string>
|
||||
<string name="login_error_hostname">Dies konnte nicht als eine URL verstanden werden</string>
|
||||
<string name="login_error_hostname_https">Der Zugriff auf die Funkwhale Server sollte über https erfolgen</string>
|
||||
<string name="login_error_hostname_https">Der Zugriff auf den Funkwhale Server sollte über https erfolgen</string>
|
||||
<string name="toolbar_search">Suche</string>
|
||||
<string name="title_settings">Einstellungen</string>
|
||||
<string name="title_oss_licences">Open Source Lizenz</string>
|
||||
|
@ -20,16 +20,16 @@
|
|||
<string name="settings_media_quality">Medienqualität</string>
|
||||
<string name="settings_media_quality_quality">Beste Qualität</string>
|
||||
<string name="settings_media_quality_size">Kleine Dateigröße</string>
|
||||
<string name="settings_media_quality_summary_quality">Songs mit größerer Dateigröße werden verwendet</string>
|
||||
<string name="settings_media_quality_summary_quality">Versionen mit größerer Dateigröße werden verwendet</string>
|
||||
<string name="settings_media_quality_summary_size">Songs mit kleinerer Dateigröße werden verwendet</string>
|
||||
<string name="settings_media_cache_size">Zwischenspeichergröße</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB werden für Songs verwendet, welche Offline abspielbar sind</string>
|
||||
<string name="settings_other">Andere</string>
|
||||
<string name="settings_night_mode">Nachtmodus</string>
|
||||
<string name="settings_night_mode">Dunkler Modus</string>
|
||||
<string name="settings_night_mode_on">Immer eingeschaltet (Dunkelmodus)</string>
|
||||
<string name="settings_night_mode_on_summary">Nachtmodus wird bevorzugt</string>
|
||||
<string name="settings_night_mode_on_summary">Dunkler Modus ist immer eingeschaltet</string>
|
||||
<string name="settings_night_mode_off">Immer ausgeschaltet (Heller Modus)</string>
|
||||
<string name="settings_night_mode_off_summary">Heller Modus wird bevorzugt</string>
|
||||
<string name="settings_night_mode_off_summary">Heller Modus ist immer eingeschaltet</string>
|
||||
<string name="settings_night_mode_system">Folge den Systemeinstellungen</string>
|
||||
<string name="settings_night_mode_system_summary">Der Nachtmodus wird den Systemeinstellungen folgen</string>
|
||||
<string name="settings_experiments">Experimentielle Funktionen aktivieren</string>
|
||||
|
@ -49,14 +49,14 @@
|
|||
<string name="playlists">Playlisten</string>
|
||||
<string name="radios">Sender</string>
|
||||
<string name="favorites">Favoriten</string>
|
||||
<string name="playback_media_controls">Mediansteuerung</string>
|
||||
<string name="playback_media_controls">Mediensteuerung</string>
|
||||
<string name="playback_media_controls_description">Medienwiedergabe steuern</string>
|
||||
<string name="playback_shuffle">Zufallswiedergabe</string>
|
||||
<string name="playback_queue">Warteschlange</string>
|
||||
<string name="playback_queue_empty">Deine Warteschlange ist leer</string>
|
||||
<string name="playback_queue_remove_item">Entfernen</string>
|
||||
<string name="playback_queue_add_item">Zur Warteschlange hinzufügen</string>
|
||||
<string name="playback_queue_play_next">Als nächstes spielen</string>
|
||||
<string name="playback_queue_play_next">Als nächstes abspielen</string>
|
||||
<string name="manage_add_to_favorites">Als Favorit hinzufügen</string>
|
||||
<string name="control_toggle">Abspielen / Pause</string>
|
||||
<string name="control_previous">Vorheriger Song</string>
|
||||
|
@ -84,13 +84,48 @@
|
|||
<string name="track_info_details_track_instance">Funkwhale Instanz</string>
|
||||
<string name="radio_playback_error">Ein Fehler ist beim Abspielen des Radios aufgetreten</string>
|
||||
<string name="radio_random_title">Zufällig</string>
|
||||
<string name="radio_random_description">Zufällig ausgewählte Songs, vielleicht entdeckt du neue Musik?</string>
|
||||
<string name="radio_random_description">Zufällig ausgewählte Songs, vielleicht entdeckst du neue Musik\?</string>
|
||||
<string name="radio_less_listened_title">Weniger angehört</string>
|
||||
<string name="radio_less_listened_description">Höre Songs, welche du nicht oft hörst. Perfekt, um ein Gleichgewicht herzustellen.</string>
|
||||
<string name="radio_less_listened_description">Höre Songs, welche du nicht oft anhörst. Perfekt, um ein Gleichgewicht wieder herzustellen.</string>
|
||||
<string name="logout_title">Abmelden</string>
|
||||
<string name="logout_content">Bist du dir sicher, dass du dich aus deiner Funkwhale Instanz abmelden möchtest?</string>
|
||||
<string name="logout_content">Bist du dir sicher, dass du dich von dieser Funkwhale Instanz abmelden möchtest\?</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d Song • %2$s</item>
|
||||
<item quantity="other">%1$d Songs • %2$s</item>
|
||||
</plurals>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="login_error_userinfo">Wir konnten keine Informationen zu deinem Nutzer finden</string>
|
||||
<string name="login_cleartext">Erlaube unverschlüsselte Verbindungen (HTTP)</string>
|
||||
<string name="radio_your_content_description">Auswahl aus deiner eigenen Bibliothek</string>
|
||||
<string name="radio_your_content_title">Dein Inhalt</string>
|
||||
<string name="radio_user_radios">Nutzersender</string>
|
||||
<string name="radio_instance_radios">Instance Sender</string>
|
||||
<string name="track_info_details_track_license">Lizenz</string>
|
||||
<string name="track_info_details_track_copyright">Urheberrecht</string>
|
||||
<string name="playback_queue_download">Herunterladen</string>
|
||||
<string name="settings_crash_report_copied">Der letzte Absturzbericht wurde in deine Zwischenablage kopiert</string>
|
||||
<string name="settings_crash_report_description">Nur Otters Protokolle der letzten 5 Minuten vor dem Crash werden werden gespeichert</string>
|
||||
<string name="settings_crash_report_title">Kopiere Absturzberichte</string>
|
||||
<string name="radio_favorites_description">Spielen Sie Ihre Lieblingsmusik in einer nicht endenen Glücksschleife.</string>
|
||||
<string name="only_my_music">Nur meine Musik</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Lade %1$d Lied herunter</item>
|
||||
<item quantity="other">Lade %1$d Lieder herunter</item>
|
||||
</plurals>
|
||||
<string name="settings_play_order_in_order">Alben in reihenfolgen spielen</string>
|
||||
<string name="playback_queue_save">Speichern</string>
|
||||
<string name="playlist_add_to_new">Neue Playlist…</string>
|
||||
<string name="playlist_add_to_create">Playlist erstellen</string>
|
||||
<string name="filters_my_music">Meine Musik</string>
|
||||
<string name="settings_play_order">Bevorzugte Wiedergabereihenfolge</string>
|
||||
<string name="filters">Filter</string>
|
||||
<string name="playlist_added_to">Zur Wiedergabeliste %s hinzugefügt</string>
|
||||
<string name="playlist_add_to">Zur Wiedergabeliste hinzufügen</string>
|
||||
<string name="playback_queue_clear">Leeren</string>
|
||||
<string name="playback_play">Abspielen</string>
|
||||
<string name="fiters_all">Alle Musikinhalte</string>
|
||||
<string name="filters_followed">Gefolgte Inhalte</string>
|
||||
<string name="settings_play_order_shuffle">Alben zufällig wiedergeben</string>
|
||||
<string name="settings_play_order_shuffle_summary">Alben werden in zufälliger Reihenfolge abgespielt</string>
|
||||
<string name="settings_play_order_in_order_summary">Alben der Reihe nach abspielen</string>
|
||||
</resources>
|
|
@ -0,0 +1,130 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Descargando %1$d pista…</item>
|
||||
<item quantity="other">Descargando %1$d pistas…</item>
|
||||
</plurals>
|
||||
<string name="only_my_music">Sólo mi música</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d pista • %2$s</item>
|
||||
<item quantity="other">%1$d pistas • %2$s</item>
|
||||
</plurals>
|
||||
<string name="logout_content">¿Cerrar sesión de este servidor de Funkwhale\?</string>
|
||||
<string name="logout_title">Cerrar sesión</string>
|
||||
<string name="radio_less_listened_description">Escucha canciones que no sueles escuchar.</string>
|
||||
<string name="radio_less_listened_title">Menos reproducidas</string>
|
||||
<string name="radio_random_description">Canciones completamente aleatorias. ¿Tal vez descubras algo nuevo\?</string>
|
||||
<string name="radio_random_title">Aleatorio</string>
|
||||
<string name="radio_favorites_description">Reproduce tus favoritos en un ciclo sin fin de felicidad.</string>
|
||||
<string name="radio_your_content_description">Selección de tus librerías</string>
|
||||
<string name="radio_your_content_title">Tu contenido</string>
|
||||
<string name="radio_user_radios">Radios del usuario</string>
|
||||
<string name="radio_instance_radios">Radios del servidor</string>
|
||||
<string name="radio_playback_error">No se puede reproducir esta emisora de radio</string>
|
||||
<string name="track_info_details_track_instance">Instancia de Funkwhale</string>
|
||||
<string name="track_info_details_track_bitrate">Ratio de bits</string>
|
||||
<string name="track_info_details_track_position">Posición en el álbum</string>
|
||||
<string name="track_info_details_track_duration">Duración</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_license">Licencia</string>
|
||||
<string name="track_info_details_track_title">Título</string>
|
||||
<string name="track_info_details_album">Álbum</string>
|
||||
<string name="track_info_details_artist">Artista</string>
|
||||
<string name="track_info_details_title">Detalles de la pista</string>
|
||||
<string name="track_info_details">Información</string>
|
||||
<string name="track_info_album">Ir al álbum</string>
|
||||
<string name="track_info_artist">Ir al artista</string>
|
||||
<string name="alt_track_info">Información de la pista</string>
|
||||
<string name="alt_more_options">Más opciones</string>
|
||||
<string name="alt_album_cover">Portada del álbum</string>
|
||||
<string name="alt_artist_art">Imagen del artista</string>
|
||||
<string name="alt_app_logo">Logo de la aplicación</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d álbum</item>
|
||||
<item quantity="other">%d álbumes</item>
|
||||
</plurals>
|
||||
<string name="error_playback">No se puede reproducir esta pista</string>
|
||||
<string name="control_next">Siguiente pista</string>
|
||||
<string name="control_previous">Pista anterior</string>
|
||||
<string name="control_toggle">Alternar reproducción</string>
|
||||
<string name="manage_add_to_favorites">Añadir a favoritos</string>
|
||||
<string name="playback_queue_clear">Borrar cola</string>
|
||||
<string name="playback_queue_download">Descargar</string>
|
||||
<string name="playback_queue_play_next">Reproducir justo después</string>
|
||||
<string name="playback_queue_add_item">Añadir a la cola</string>
|
||||
<string name="playback_queue_remove_item">Borrar</string>
|
||||
<string name="playback_queue_empty">Tu cola está vacía</string>
|
||||
<string name="playback_queue">Cola</string>
|
||||
<string name="playback_shuffle">Mezclar</string>
|
||||
<string name="playback_play">Reproducir</string>
|
||||
<string name="playback_media_controls_description">Control de reproducción del sonido</string>
|
||||
<string name="playback_media_controls">Control de reproducción</string>
|
||||
<string name="favorites">Favoritos</string>
|
||||
<string name="radios">Radios</string>
|
||||
<string name="playlists">Listas de reproducción</string>
|
||||
<string name="tracks">Pistas</string>
|
||||
<string name="albums">Álbumes</string>
|
||||
<string name="artists">Artistas</string>
|
||||
<string name="settings_logout">Cerrar sesión</string>
|
||||
<string name="settings_crash_report_copied">Informe del cierre inesperado copiado en el portapapeles</string>
|
||||
<string name="settings_crash_report_description">Sólo se recogerán los registros de Otter desde los 5 minutos antes del cierre inesperado</string>
|
||||
<string name="settings_crash_report_title">Copiar registros de error</string>
|
||||
<string name="settings_information_license_description">licencia MIT</string>
|
||||
<string name="settings_information_license_title">Licencia</string>
|
||||
<string name="settings_version_title">Versión</string>
|
||||
<string name="settings_information_repository_description">Otter por Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Repositorio</string>
|
||||
<string name="settings_information">Información</string>
|
||||
<string name="settings_experiments_restart_content">Cierre y reinicie la aplicación para utilizar la nueva configuración</string>
|
||||
<string name="settings_experiments_restart_title">Es necesario reiniciar la aplicación</string>
|
||||
<string name="settings_experiments_description">Utilízalo bajo tu propio riesgo. Puede congelar o bloquear la aplicación.</string>
|
||||
<string name="settings_experiments">Características experimentales</string>
|
||||
<string name="settings_night_mode_system_summary">El modo nocturno seguirá el sistema</string>
|
||||
<string name="settings_night_mode_system">Seguir el sistema</string>
|
||||
<string name="settings_night_mode_off_summary">El modo claro siempre estará activado</string>
|
||||
<string name="settings_night_mode_off">Siempre desactivado (modo claro)</string>
|
||||
<string name="settings_night_mode_on_summary">El modo oscuro siempre estará activado</string>
|
||||
<string name="settings_night_mode_on">Siempre activado (modo oscuro)</string>
|
||||
<string name="settings_night_mode">Modo oscuro</string>
|
||||
<string name="settings_other">Otros</string>
|
||||
<string name="settings_play_order_in_order_summary">Prefieres reproducir los álbumes en orden</string>
|
||||
<string name="settings_play_order_in_order">Reproducir álbumes en orden</string>
|
||||
<string name="settings_play_order_shuffle_summary">Prefieres mezclar las canciones de un álbum</string>
|
||||
<string name="settings_play_order_shuffle">Mezclar álbumes</string>
|
||||
<string name="settings_play_order">Orden de reproducción preferido</string>
|
||||
<string name="settings_media_cache_size_summary">Se usarán %d GB para guardar las canciones para reproduccir sin conexión</string>
|
||||
<string name="settings_media_cache_size">Tamaño de la caché para los medios</string>
|
||||
<string name="settings_media_quality_summary_size">La versión más pequeña será reproducida</string>
|
||||
<string name="settings_media_quality_summary_quality">La mejor versión disponible será reproducida</string>
|
||||
<string name="settings_media_quality_size">Tamaño más pequeño</string>
|
||||
<string name="settings_media_quality_quality">Mejor calidad</string>
|
||||
<string name="settings_media_quality">Calidad del sonido</string>
|
||||
<string name="settings_general">General</string>
|
||||
<string name="search_no_results">No se han encontrado resultados</string>
|
||||
<string name="search_welcome">Introduzca su búsqueda arriba y pulse enter para buscar en su colección</string>
|
||||
<string name="search_placeholder">Buscar artistas, álbumes y pistas</string>
|
||||
<string name="title_oss_licences">Licencias de código abierto</string>
|
||||
<string name="title_settings">Ajustes</string>
|
||||
<string name="title_downloads">Descargas</string>
|
||||
<string name="toolbar_search">Buscar</string>
|
||||
<string name="login_error_userinfo">No se ha podido recuperar la información del usuario</string>
|
||||
<string name="login_error_hostname_https">El servidor de Funkwhale debería estar asegurado con HTTPS</string>
|
||||
<string name="login_error_hostname">Introduzca primero una URL válida</string>
|
||||
<string name="login_logging_in">Iniciando sesión…</string>
|
||||
<string name="login_submit">Iniciar sesión</string>
|
||||
<string name="login_password">Contraseña</string>
|
||||
<string name="login_username">Nombre de usuario</string>
|
||||
<string name="login_anonymous">Autenticación anónima</string>
|
||||
<string name="login_cleartext">Permitir tráfico sin cifrar (HTTP)</string>
|
||||
<string name="login_hostname">Servidor</string>
|
||||
<string name="login_welcome">Por favor, introduzca los datos de una instancia de Funkwhale para acceder a su contenido</string>
|
||||
<string name="filters_followed">Contenido seguido</string>
|
||||
<string name="filters_my_music">Mi música</string>
|
||||
<string name="fiters_all">Toda la música</string>
|
||||
<string name="filters">Filtros</string>
|
||||
<string name="playlist_added_to">Añadida a la lista de reproducción “%s”</string>
|
||||
<string name="playlist_add_to_create">Crear lista de reproducción</string>
|
||||
<string name="playlist_add_to_new">Nueva lista de reproducción…</string>
|
||||
<string name="playlist_add_to">Añadir a la lista de reproducción</string>
|
||||
<string name="playback_queue_save">Guardar</string>
|
||||
</resources>
|
|
@ -8,7 +8,7 @@
|
|||
<string name="login_password">Mot de passe</string>
|
||||
<string name="login_submit">Se connecter</string>
|
||||
<string name="login_logging_in">Connexion</string>
|
||||
<string name="login_error_hostname">Cela ne semble pas être un nom d\hôte valide</string>
|
||||
<string name="login_error_hostname">Cela ne semble pas être un nom d\'hôte valide</string>
|
||||
<string name="login_error_hostname_https">Le nom d\'hôte Funkwhale devrait être sécurisé à travers HTTPS</string>
|
||||
<string name="login_error_userinfo">Nous n\'avons pas pu récupérer les informations à propos de votre utilisateur</string>
|
||||
<string name="toolbar_search">Rechercher</string>
|
||||
|
@ -26,6 +26,11 @@
|
|||
<string name="settings_media_quality_summary_size">Les pistes les plus légères seront utilisées</string>
|
||||
<string name="settings_media_cache_size">Taille du cache</string>
|
||||
<string name="settings_media_cache_size_summary">%d Go seront utilisés pour mettre en cache les pistes pour la lecture hors-ligne</string>
|
||||
<string name="settings_play_order">Ordre de lecture préféré</string>
|
||||
<string name="settings_play_order_shuffle">Lecture aléatoire</string>
|
||||
<string name="settings_play_order_shuffle_summary">Vous préférez écouter les albums aléatoirement</string>
|
||||
<string name="settings_play_order_in_order">Lecture dans l\'ordre</string>
|
||||
<string name="settings_play_order_in_order_summary">Vous préférez écouter les albums dans l\'ordre</string>
|
||||
<string name="settings_other">Autres</string>
|
||||
<string name="settings_night_mode">Mode nuit</string>
|
||||
<string name="settings_night_mode_on">Toujours activé (mode sombre)</string>
|
||||
|
@ -54,6 +59,7 @@
|
|||
<string name="playlists">Playlists</string>
|
||||
<string name="radios">Radios</string>
|
||||
<string name="favorites">Favoris</string>
|
||||
<string name="playback_play">Jouer</string>
|
||||
<string name="playback_media_controls">Contrôle de lecture</string>
|
||||
<string name="playback_media_controls_description">Contrôler la lecture musicale</string>
|
||||
<string name="playback_shuffle">Lecture aléatoire</string>
|
||||
|
@ -63,6 +69,8 @@
|
|||
<string name="playback_queue_add_item">Ajouter à la liste de lecture</string>
|
||||
<string name="playback_queue_play_next">Prochaine écoute</string>
|
||||
<string name="playback_queue_download">Télécharger</string>
|
||||
<string name="playback_queue_clear">Effacer</string>
|
||||
<string name="playback_queue_save">Enregistrer</string>
|
||||
<string name="manage_add_to_favorites">Ajouter aux favoris</string>
|
||||
<string name="control_toggle">Lecture / pause</string>
|
||||
<string name="control_previous">Piste précédente</string>
|
||||
|
@ -77,13 +85,15 @@
|
|||
<string name="alt_album_cover">Couverture de l\'album</string>
|
||||
<string name="alt_more_options">Plus d\'options</string>
|
||||
<string name="alt_track_info">Informations sur cette piste</string>
|
||||
<string name="track_info_artist">Voir l\'artist</string>
|
||||
<string name="track_info_artist">Voir l\'artiste</string>
|
||||
<string name="track_info_album">Voir l\'album</string>
|
||||
<string name="track_info_details">Informations</string>
|
||||
<string name="track_info_details_title">Détails de la piste</string>
|
||||
<string name="track_info_details_artist">Artiste</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_track_title">Nom de piste</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_license">Licence</string>
|
||||
<string name="track_info_details_track_duration">Durée</string>
|
||||
<string name="track_info_details_track_position">Position dans l\'album</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
|
@ -92,7 +102,7 @@
|
|||
<string name="radio_instance_radios">Radios de l\'instance</string>
|
||||
<string name="radio_user_radios">Radios des utilisateurs</string>
|
||||
<string name="radio_your_content_title">Votre contenu</string>
|
||||
<string name="radio_your_content_description">Une sélection de votre propre bibliothèque.</string>
|
||||
<string name="radio_your_content_description">Une sélection de vos propres bibliothèques</string>
|
||||
<string name="radio_favorites_description">Jouez vos morceaux favoris dans une boucle allègre infinie.</string>
|
||||
<string name="radio_random_title">Aléatoire</string>
|
||||
<string name="radio_random_description">Choix de pistes totalement aléatoires, vous découvrirez peut-être quelque chose ?</string>
|
||||
|
@ -104,7 +114,14 @@
|
|||
<item quantity="one">%1$d piste • %2$s</item>
|
||||
<item quantity="other">%1$d pistes • %2$s</item>
|
||||
</plurals>
|
||||
<string name="only_my_music">Ma musique seulement</string>
|
||||
<string name="playlist_add_to">Ajouter à une playlist</string>
|
||||
<string name="playlist_add_to_new">Nouvelle playlist…</string>
|
||||
<string name="playlist_add_to_create">Créer playlist</string>
|
||||
<string name="playlist_added_to">Ajouté à la playlist %s</string>
|
||||
<string name="filters">Filtres</string>
|
||||
<string name="fiters_all">Toute la musique</string>
|
||||
<string name="filters_my_music">Ma musique</string>
|
||||
<string name="filters_followed">Contenu suivi</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Téléchargement de %1$d piste</item>
|
||||
<item quantity="other">Téléchargement de %1$d pistes</item>
|
||||
|
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="settings_experiments">Características experimentais</string>
|
||||
<string name="settings_night_mode_system_summary">O modo noite seguirá ao sistema</string>
|
||||
<string name="settings_night_mode_system">Seguir ao sistema</string>
|
||||
<string name="settings_night_mode_off_summary">O modo claro estará activo sempre</string>
|
||||
<string name="settings_night_mode_off">Desactivado (modo claro)</string>
|
||||
<string name="settings_night_mode_on_summary">O modo escuro estará sempre activo</string>
|
||||
<string name="settings_night_mode_on">Activo sempre (modo escuro)</string>
|
||||
<string name="settings_night_mode">Modo escuro</string>
|
||||
<string name="settings_other">Outro</string>
|
||||
<string name="settings_play_order_in_order_summary">Prefires reproducir os álbumes en orde</string>
|
||||
<string name="settings_play_order_in_order">Reproducir álbumes en orde</string>
|
||||
<string name="settings_play_order_shuffle_summary">Prefires mesturar as pistas dos álbumes</string>
|
||||
<string name="settings_play_order_shuffle">Barallar álbumes</string>
|
||||
<string name="settings_play_order">Orde preferida de reprodución</string>
|
||||
<string name="settings_media_cache_size_summary">Utilizarase %d GB de almacenaxe para reprodución sen conexión</string>
|
||||
<string name="settings_media_cache_size">Tamaño da caché multimedia</string>
|
||||
<string name="settings_media_quality_summary_size">Reproducirase a pista co menor tamaño dispoñible</string>
|
||||
<string name="settings_media_quality_summary_quality">Reproducirase a mellor calidade dispoñible</string>
|
||||
<string name="settings_media_quality_size">O menor tamaño</string>
|
||||
<string name="settings_media_quality_quality">A mellor calidade</string>
|
||||
<string name="settings_media_quality">Calidade do multimedia</string>
|
||||
<string name="settings_general">Xeral</string>
|
||||
<string name="search_no_results">Non se atopan resultados</string>
|
||||
<string name="search_welcome">Escribe os termos a buscar e preme enter para buscar na túa colección</string>
|
||||
<string name="search_placeholder">Busca artistas, álbumes e cancións</string>
|
||||
<string name="title_oss_licences">Licenzas libres</string>
|
||||
<string name="title_settings">Axustes</string>
|
||||
<string name="title_downloads">Descargas</string>
|
||||
<string name="toolbar_search">Buscar</string>
|
||||
<string name="login_error_userinfo">Non se puido obter info da usuaria</string>
|
||||
<string name="login_error_hostname_https">O servidor Funkwhale debería ser seguro a través de HTTPS</string>
|
||||
<string name="login_error_hostname">Escribe un URL válido</string>
|
||||
<string name="login_logging_in">Accedendo…</string>
|
||||
<string name="login_submit">Acceder</string>
|
||||
<string name="login_password">Contrasinal</string>
|
||||
<string name="login_username">Nome de usuaria</string>
|
||||
<string name="login_anonymous">Autentificación anónima</string>
|
||||
<string name="login_cleartext">Permitir tráfico sen cifrar (HTTP)</string>
|
||||
<string name="login_hostname">Servidor</string>
|
||||
<string name="login_welcome">Escribe os datos da instancia Funkwhale para acceder ó seu contido</string>
|
||||
<string name="radios">Radios</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Descargando %1$d pista…</item>
|
||||
<item quantity="other">Descargando %1$d pistas…</item>
|
||||
</plurals>
|
||||
<string name="filters_followed">Contido seguido</string>
|
||||
<string name="filters_my_music">A miña música</string>
|
||||
<string name="fiters_all">Toda a música</string>
|
||||
<string name="filters">Filtros</string>
|
||||
<string name="playlist_added_to">Engadida á lista \"%s\"</string>
|
||||
<string name="playlist_add_to_create">Crear lista de reprodución</string>
|
||||
<string name="playlist_add_to_new">Nova lista…</string>
|
||||
<string name="playlist_add_to">Engadir á lista</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d pista • %2$s</item>
|
||||
<item quantity="other">%1$d pistas • %2$s</item>
|
||||
</plurals>
|
||||
<string name="logout_content">Queres saír da sesión en Funkwhale\?</string>
|
||||
<string name="logout_title">Pechar sesión</string>
|
||||
<string name="radio_less_listened_description">Escoita pistas que non adoitas escoitar.</string>
|
||||
<string name="radio_less_listened_title">Menos reproducido</string>
|
||||
<string name="radio_random_description">Escollas ao chou, igoal descubres algo novo\?</string>
|
||||
<string name="radio_random_title">Ó chou</string>
|
||||
<string name="radio_favorites_description">Reproduce os teus favoritos nun bucle de felicidade infinita.</string>
|
||||
<string name="radio_your_content_description">Escollas das túas propias bibliotecas</string>
|
||||
<string name="radio_your_content_title">O teu contido</string>
|
||||
<string name="radio_user_radios">Radios da usuaria</string>
|
||||
<string name="radio_instance_radios">Radios da instancia</string>
|
||||
<string name="radio_playback_error">Non se puido reproducir a radio</string>
|
||||
<string name="track_info_details_track_instance">Instancia Funkwhale</string>
|
||||
<string name="track_info_details_track_bitrate">Taxa de bits</string>
|
||||
<string name="track_info_details_track_position">Posición no álbume</string>
|
||||
<string name="track_info_details_track_duration">Duración</string>
|
||||
<string name="track_info_details_track_license">Licenza</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_title">Título da pista</string>
|
||||
<string name="track_info_details_album">Álbume</string>
|
||||
<string name="track_info_details_artist">Artista</string>
|
||||
<string name="track_info_details_title">Detalles da psita</string>
|
||||
<string name="track_info_details">Info</string>
|
||||
<string name="track_info_album">Ir ó álbume</string>
|
||||
<string name="track_info_artist">Ir á artista</string>
|
||||
<string name="alt_track_info">Info da pista</string>
|
||||
<string name="alt_more_options">Máis opcións</string>
|
||||
<string name="alt_album_cover">Cuberta do álbume</string>
|
||||
<string name="alt_artist_art">Arte da artista</string>
|
||||
<string name="alt_app_logo">Logo da app</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d álbume</item>
|
||||
<item quantity="other">%d álbumes</item>
|
||||
</plurals>
|
||||
<string name="error_playback">Non se puido reproducir a pista</string>
|
||||
<string name="control_next">Pista seguinte</string>
|
||||
<string name="control_previous">Pista anterior</string>
|
||||
<string name="control_toggle">Activar reprodución</string>
|
||||
<string name="manage_add_to_favorites">Engadir a favoritas</string>
|
||||
<string name="playback_queue_save">Gardar</string>
|
||||
<string name="playback_queue_clear">Baleirar</string>
|
||||
<string name="playback_queue_download">Descargar</string>
|
||||
<string name="playback_queue_play_next">Reproducir a continuación</string>
|
||||
<string name="playback_queue_add_item">Engadir á cola</string>
|
||||
<string name="playback_queue_remove_item">Eliminar</string>
|
||||
<string name="playback_queue_empty">A cola está baleira</string>
|
||||
<string name="playback_queue">Cola</string>
|
||||
<string name="playback_shuffle">Barallar</string>
|
||||
<string name="playback_play">Reproducir</string>
|
||||
<string name="playback_media_controls_description">Control da reprodución multimedia</string>
|
||||
<string name="playback_media_controls">Control da reprodución</string>
|
||||
<string name="favorites">Favoritas</string>
|
||||
<string name="playlists">Listaxes</string>
|
||||
<string name="tracks">Pistas</string>
|
||||
<string name="albums">Álbumes</string>
|
||||
<string name="artists">Artistas</string>
|
||||
<string name="settings_logout">Pechar sesión</string>
|
||||
<string name="settings_crash_report_copied">Copiouse ao portapapeis o último informe de fallos</string>
|
||||
<string name="settings_crash_report_description">Só se recollen os rexistros de Otter dos 5 minutos anteriores ao fallo</string>
|
||||
<string name="settings_crash_report_title">Copiar rexistro de fallos</string>
|
||||
<string name="settings_information_license_description">Licenza MIT</string>
|
||||
<string name="settings_information_license_title">Licenza</string>
|
||||
<string name="settings_version_title">Versión</string>
|
||||
<string name="settings_information_repository_description">Otter por Antonie POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Repositorio</string>
|
||||
<string name="settings_information">Info</string>
|
||||
<string name="settings_experiments_restart_content">Pecha e reinicia a app para que se aplique o cambio</string>
|
||||
<string name="settings_experiments_restart_title">Require reiniciar a app</string>
|
||||
<string name="settings_experiments_description">Usa baixo a túa responsabilidade. Podería facer fallar a app.</string>
|
||||
</resources>
|
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Preuzimanje pjesme %1$d</item>
|
||||
<item quantity="few">Preuzimanje pjesama %1$d</item>
|
||||
<item quantity="other">Preuzimanje pjesama %1$d</item>
|
||||
</plurals>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d pjesma • %2$s</item>
|
||||
<item quantity="few">%1$d pjesme • %2$s</item>
|
||||
<item quantity="other">%1$d pjesme • %2$s</item>
|
||||
</plurals>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="few">%d albuma</item>
|
||||
<item quantity="other">%d albuma</item>
|
||||
</plurals>
|
||||
<string name="filters_followed">Praćeni sadržaj</string>
|
||||
<string name="filters_my_music">Moja glazba</string>
|
||||
<string name="fiters_all">Sva glazba</string>
|
||||
<string name="filters">Filteri</string>
|
||||
<string name="playlist_added_to">Dodanu u listu %s</string>
|
||||
<string name="playlist_add_to_create">Stvorite listu</string>
|
||||
<string name="playlist_add_to_new">Nova lista…</string>
|
||||
<string name="playlist_add_to">Dodaj u listu</string>
|
||||
<string name="logout_content">Jeste li sigurni da se želite odjaviti iz ove Funkwhale instance\?</string>
|
||||
<string name="logout_title">Odjava</string>
|
||||
<string name="radio_less_listened_description">Slušajte trake koje inače ne slušate. Vrijeme je da ostvarite ravnotežu.</string>
|
||||
<string name="radio_less_listened_title">Manje slušano</string>
|
||||
<string name="radio_random_description">Potpuno nasumični odabiri, možda otkrijete nove stvari\?</string>
|
||||
<string name="radio_random_title">Nasumično</string>
|
||||
<string name="radio_favorites_description">Igrajte vaše najdraže pjesme u neprekidnom krugu sreće.</string>
|
||||
<string name="radio_your_content_description">Odabiri iz vaše biblioteke</string>
|
||||
<string name="radio_your_content_title">Tvoj sadržaj</string>
|
||||
<string name="radio_user_radios">Radiji korisnika</string>
|
||||
<string name="radio_instance_radios">Radiji instance</string>
|
||||
<string name="radio_playback_error">Pogreška pri pokušaju reprodukcije radija</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instanca</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_position">Pozicija albuma</string>
|
||||
<string name="track_info_details_track_duration">Trajanje</string>
|
||||
<string name="track_info_details_track_license">Licenca</string>
|
||||
<string name="track_info_details_track_copyright">Autorska prava</string>
|
||||
<string name="track_info_details_track_title">Naziv</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_artist">Umjetnik</string>
|
||||
<string name="track_info_details_title">Detalji</string>
|
||||
<string name="track_info_details">Informacije</string>
|
||||
<string name="track_info_album">Idi na album</string>
|
||||
<string name="track_info_artist">Idi na umjetnika</string>
|
||||
<string name="alt_track_info">Informacije o traci</string>
|
||||
<string name="alt_more_options">Više opcija</string>
|
||||
<string name="alt_album_cover">Naslovna albuma</string>
|
||||
<string name="alt_artist_art">Naslovna umjetnika</string>
|
||||
<string name="alt_app_logo">Logo aplikacije</string>
|
||||
<string name="error_playback">Ova traka se nije mogla reproducirati</string>
|
||||
<string name="control_next">Sljedeće</string>
|
||||
<string name="control_previous">Prijašnje</string>
|
||||
<string name="control_toggle">Igraj / Pauziraj</string>
|
||||
<string name="manage_add_to_favorites">Dodaj u favorite</string>
|
||||
<string name="playback_queue_save">Spremi</string>
|
||||
<string name="playback_queue_clear">Očisti</string>
|
||||
<string name="playback_queue_download">Preuzmi</string>
|
||||
<string name="playback_queue_play_next">Igraj sljedeće</string>
|
||||
<string name="playback_queue_add_item">Dodaj u poredak</string>
|
||||
<string name="playback_queue_remove_item">Ukloni</string>
|
||||
<string name="playback_queue_empty">Poredak je prazan</string>
|
||||
<string name="playback_queue">Poredak</string>
|
||||
<string name="playback_shuffle">Izmješaj</string>
|
||||
<string name="playback_play">Igraj</string>
|
||||
<string name="playback_media_controls_description">Kontrola reprodukcije medija</string>
|
||||
<string name="playback_media_controls">Kontrola medija</string>
|
||||
<string name="favorites">Favoriti</string>
|
||||
<string name="radios">Radiji</string>
|
||||
<string name="playlists">Liste</string>
|
||||
<string name="tracks">Pjesme</string>
|
||||
<string name="albums">Albumi</string>
|
||||
<string name="artists">Umjetnici</string>
|
||||
<string name="settings_logout">Odjava</string>
|
||||
<string name="settings_crash_report_copied">Zadnje izvješće o rušenju je kopirano na međuspremnik</string>
|
||||
<string name="settings_crash_report_description">Samo Otter-ova izvješća zadnjih 5 minuta prije pada će biti spremljena</string>
|
||||
<string name="settings_crash_report_title">Kopiraj izvješća rušenja</string>
|
||||
<string name="settings_information_license_description">MIT licenca</string>
|
||||
<string name="settings_information_license_title">Licenca</string>
|
||||
<string name="settings_version_title">Verzija</string>
|
||||
<string name="settings_information_repository_description">Otter od Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Softverski repozitorij</string>
|
||||
<string name="settings_information">Informacije</string>
|
||||
<string name="settings_experiments_restart_content">Molimo ugasite i ponovno pokrenite aplikaciju kako bi se promjene primjenile</string>
|
||||
<string name="settings_experiments_restart_title">Potrebno ponovno pokretanje</string>
|
||||
<string name="settings_experiments_description">Koristiti na svoju odgovornost, može zamrznuti ili srušiti aplikaciju</string>
|
||||
<string name="settings_experiments">Omogući eksperimentalne funkcije</string>
|
||||
<string name="settings_night_mode_system_summary">Tamni izgled će se prilagoditi postavkama sistema</string>
|
||||
<string name="settings_night_mode_system">Prilagodi postavkama sistema</string>
|
||||
<string name="settings_night_mode_off">Uvijek isključen (svjetli izgled)</string>
|
||||
<string name="settings_night_mode_off_summary">Svjetli izgled će uvijek biti isključen</string>
|
||||
<string name="settings_night_mode_on_summary">Tamni izgled će uvijek biti uključen</string>
|
||||
<string name="settings_night_mode_on">Uvijek uključen (tamni izgled)</string>
|
||||
<string name="settings_night_mode">Tamni izgled</string>
|
||||
<string name="settings_other">Ostalo</string>
|
||||
<string name="settings_play_order_in_order_summary">Preferirate igrati albume po redu</string>
|
||||
<string name="settings_play_order_in_order">Igraj albume po redu</string>
|
||||
<string name="settings_play_order_shuffle_summary">Preferirate mješanje albumnih traka</string>
|
||||
<string name="settings_play_order_shuffle">Izmješaj albume</string>
|
||||
<string name="settings_play_order">Preferirani poredak reprodukcije</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB će se koristiti za pohranjivanje datoteka u svrhu izvanmrežne reprodukcije</string>
|
||||
<string name="settings_media_cache_size">Veličina medijske predmemorije (cache)</string>
|
||||
<string name="settings_media_quality_summary_size">Igrat će se najmanja dostupna datoteka</string>
|
||||
<string name="settings_media_quality_summary_quality">Igrat će se najbolja dostupna verzija</string>
|
||||
<string name="settings_media_quality_size">Najmanja veličina</string>
|
||||
<string name="settings_media_quality_quality">Najbolja kvaliteta</string>
|
||||
<string name="settings_media_quality">Kvaliteta medija</string>
|
||||
<string name="settings_general">Opće</string>
|
||||
<string name="search_no_results">Nema rezultata za vaš upit</string>
|
||||
<string name="search_welcome">Iznad upišite svoj pojam/ve i stisnite Enter za pretraživanje kolekcije</string>
|
||||
<string name="search_placeholder">Pretraži umjetnike, albume i pjesme</string>
|
||||
<string name="title_oss_licences">Open source licence</string>
|
||||
<string name="title_settings">Postavke</string>
|
||||
<string name="title_downloads">Preuzimanja</string>
|
||||
<string name="toolbar_search">Traži</string>
|
||||
<string name="login_error_userinfo">Nismo mogli preuzeti informacije o vašem korisniku</string>
|
||||
<string name="login_error_hostname_https">Funkwhale operator bi trebao biti siguran sa HTTPS-om</string>
|
||||
<string name="login_error_hostname">Ovo se nije moglo prepoznati kao validan URL</string>
|
||||
<string name="login_logging_in">Prijavljivanje</string>
|
||||
<string name="login_submit">Prijavi se</string>
|
||||
<string name="login_password">Lozinka</string>
|
||||
<string name="login_username">Korisničko ime</string>
|
||||
<string name="login_anonymous">Anonimna autentikacija</string>
|
||||
<string name="login_cleartext">Dozvoli cleartext promet (HTTP)</string>
|
||||
<string name="login_hostname">Ime operatera</string>
|
||||
<string name="login_welcome">Molimo unesite detalje vaše Funkwhale \'instance\' kako bi pristupili njenom sadržaju</string>
|
||||
</resources>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="login_submit">Masuk</string>
|
||||
<string name="settings_other">Lainnya</string>
|
||||
<string name="settings_night_mode">Mode gelap</string>
|
||||
<string name="settings_night_mode_system">Ikuti sistem</string>
|
||||
<string name="settings_information">Informasi</string>
|
||||
<string name="settings_version_title">Versi</string>
|
||||
<string name="settings_information_license_title">Lisensi</string>
|
||||
<string name="settings_logout">Keluar</string>
|
||||
<string name="artists">Artis</string>
|
||||
<string name="albums">Album</string>
|
||||
<string name="login_hostname">Hostname</string>
|
||||
<string name="login_anonymous">Autentikasi anonim</string>
|
||||
<string name="login_username">Nama pengguna</string>
|
||||
<string name="login_password">Sandi</string>
|
||||
<string name="login_logging_in">Sedang masuk…</string>
|
||||
<string name="login_error_userinfo">Tidak dapat memuat informasi pengguna</string>
|
||||
<string name="toolbar_search">Telusuri</string>
|
||||
<string name="title_downloads">Unduhan</string>
|
||||
<string name="title_settings">Pengaturan</string>
|
||||
<string name="title_oss_licences">Lisensi libre</string>
|
||||
<string name="search_no_results">Tak ada hasil</string>
|
||||
<string name="settings_general">Umum</string>
|
||||
<string name="settings_media_quality">Kualitas media</string>
|
||||
<string name="login_error_hostname">Masukkan URL yang sah terlebih dahulu</string>
|
||||
<string name="search_welcome">Masukkan pencarian Anda di atas dan tekan enter untuk menelusuri koleksi Anda</string>
|
||||
<string name="settings_media_quality_quality">Kualitas terbaik</string>
|
||||
<string name="settings_media_quality_size">Ukuran terkecil</string>
|
||||
<string name="settings_media_quality_summary_quality">Versi terbaik yang akan diputar</string>
|
||||
<string name="settings_media_cache_size">Ukuran cache untuk media</string>
|
||||
<string name="settings_play_order_in_order">Putar album sesuai urutan</string>
|
||||
<string name="settings_play_order_in_order_summary">Anda memilih untuk memutar album sesuai urutan</string>
|
||||
<string name="settings_night_mode_on">Selalu aktif (mode gelap)</string>
|
||||
<string name="settings_night_mode_on_summary">Mode gelap akan selalu aktif</string>
|
||||
<string name="settings_night_mode_off">Selalu nonaktif (mode cerah)</string>
|
||||
<string name="settings_night_mode_off_summary">Mode cerah akan selalu aktif</string>
|
||||
<string name="settings_night_mode_system_summary">Mode gelap akan mengikuti sistem</string>
|
||||
<string name="settings_experiments">Fitur eksperimental</string>
|
||||
<string name="settings_experiments_restart_title">Aplikasi harus dimulai ulang</string>
|
||||
<string name="settings_experiments_restart_content">Tutup kemudian mulai kembali aplikasi untuk menggunakan pengaturan baru</string>
|
||||
<string name="settings_information_repository_title">Repositori</string>
|
||||
<string name="settings_information_repository_description">Otter oleh Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_license_description">Lisensi MIT</string>
|
||||
</resources>
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="other">%d album</item>
|
||||
</plurals>
|
||||
<string name="playback_shuffle">Shuffle</string>
|
||||
<string name="playback_play">Riproduci</string>
|
||||
<string name="alt_artist_art">Immagine dell\'artista</string>
|
||||
<string name="playback_queue_clear">Pulisci</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Scaricando %1$d brano</item>
|
||||
<item quantity="other">Scaricando %1$d brani</item>
|
||||
</plurals>
|
||||
<string name="filters_followed">Contenuto seguito</string>
|
||||
<string name="filters_my_music">La mia musica</string>
|
||||
<string name="fiters_all">Tutta la musica</string>
|
||||
<string name="filters">Filtri</string>
|
||||
<string name="playlist_added_to">Aggiunto alla playlist %s</string>
|
||||
<string name="playlist_add_to_create">Crea playlist</string>
|
||||
<string name="playlist_add_to_new">Nuova playlist…</string>
|
||||
<string name="playlist_add_to">Aggiungi alla playlist</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d brano • %2$s</item>
|
||||
<item quantity="other">%1$d brani • %2$s</item>
|
||||
</plurals>
|
||||
<string name="logout_content">Sei sicuro di voler uscire da questa istanza di Funkwhale\?</string>
|
||||
<string name="logout_title">Disconnetti</string>
|
||||
<string name="radio_less_listened_description">Ascolta i brani che di solito non fai. È ora di ristabilire un po\' di equilibrio.</string>
|
||||
<string name="radio_less_listened_title">Meno ascoltato</string>
|
||||
<string name="radio_random_description">Scelte totalmente casuali, forse scoprirai cose nuove\?</string>
|
||||
<string name="radio_random_title">Casuale</string>
|
||||
<string name="radio_favorites_description">Riproduci i tuoi brani preferiti in un ciclo di felicità senza fine.</string>
|
||||
<string name="radio_your_content_description">Scegli dalle tue librerie</string>
|
||||
<string name="radio_your_content_title">Il tuo contenuto</string>
|
||||
<string name="radio_user_radios">Radio dell\'utente</string>
|
||||
<string name="radio_instance_radios">Radio dell\'istanza</string>
|
||||
<string name="radio_playback_error">Si è verificato un errore durante il tentativo di riprodurre questa radio</string>
|
||||
<string name="track_info_details_track_instance">Istanza di Funkwhale</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_position">Positione dell\'album</string>
|
||||
<string name="track_info_details_track_duration">Durata</string>
|
||||
<string name="track_info_details_track_license">Licenza</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_title">Titolo del brano</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_artist">Artista</string>
|
||||
<string name="track_info_details_title">Dettagli del brano</string>
|
||||
<string name="track_info_details">Informazione</string>
|
||||
<string name="track_info_album">Vai all\'album</string>
|
||||
<string name="track_info_artist">Vai all\'artista</string>
|
||||
<string name="alt_track_info">Informazioni sul brano</string>
|
||||
<string name="alt_more_options">Più opzioni</string>
|
||||
<string name="alt_album_cover">Cover dell\'album</string>
|
||||
<string name="alt_app_logo">Logo dell\'applicazione</string>
|
||||
<string name="error_playback">Impossibile riprodurre questo brano</string>
|
||||
<string name="control_next">Traccia successiva</string>
|
||||
<string name="control_previous">Brano precedente</string>
|
||||
<string name="control_toggle">Attiva/disattiva la riproduzione</string>
|
||||
<string name="manage_add_to_favorites">Aggiungi ai preferiti</string>
|
||||
<string name="playback_queue_save">Salva</string>
|
||||
<string name="playback_queue_download">Scarica</string>
|
||||
<string name="playback_queue_play_next">Riproduci successivamente</string>
|
||||
<string name="playback_queue_add_item">Aggiungi alla coda</string>
|
||||
<string name="playback_queue_remove_item">Rimuovi</string>
|
||||
<string name="playback_queue_empty">La tua coda è vuota</string>
|
||||
<string name="playback_queue">Coda</string>
|
||||
<string name="playback_media_controls_description">Controlla la riproduzione multimediale</string>
|
||||
<string name="playback_media_controls">Controlli multimediali</string>
|
||||
<string name="favorites">Preferiti</string>
|
||||
<string name="radios">Radio</string>
|
||||
<string name="playlists">Playlist</string>
|
||||
<string name="tracks">Brani</string>
|
||||
<string name="albums">Album</string>
|
||||
<string name="artists">Artisti</string>
|
||||
<string name="settings_logout">Disconnetti</string>
|
||||
<string name="settings_crash_report_copied">L\'ultimo rapporto sull\'arresto anomalo è stato copiato negli appunti</string>
|
||||
<string name="settings_crash_report_description">Verranno raccolti solo i registri di Otter dagli ultimi 5 minuti fino all\'arresto</string>
|
||||
<string name="settings_crash_report_title">Copia i registri degli arresti anomali</string>
|
||||
<string name="settings_information_license_description">Licenza MIT</string>
|
||||
<string name="settings_information_license_title">Licenza</string>
|
||||
<string name="settings_version_title">Versione</string>
|
||||
<string name="settings_information_repository_description">Otter di Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Repository</string>
|
||||
<string name="settings_information">Informazione</string>
|
||||
<string name="settings_experiments_restart_content">Si prega di chiudere e riavviare l\'app affinché questa modifica abbia effetto</string>
|
||||
<string name="settings_experiments_restart_title">Riavvio richiesto</string>
|
||||
<string name="settings_experiments_description">Utilizzare a proprio rischio, potrebbe bloccare o mandare in crash l\'app</string>
|
||||
<string name="settings_experiments">Abilita funzionalità sperimentali</string>
|
||||
<string name="settings_night_mode_system_summary">La modalità notturna seguirà le impostazioni di sistema</string>
|
||||
<string name="settings_night_mode_system">Segui le impostazioni di sistema</string>
|
||||
<string name="settings_night_mode_off_summary">La modalità luce sarà sempre attiva</string>
|
||||
<string name="settings_night_mode_off">Sempre spento (modalità luce)</string>
|
||||
<string name="settings_night_mode_on_summary">La modalità oscura sarà sempre attiva</string>
|
||||
<string name="settings_night_mode_on">Sempre attivo (modalità oscura)</string>
|
||||
<string name="settings_night_mode">Modalità oscura</string>
|
||||
<string name="settings_other">Altro</string>
|
||||
<string name="settings_play_order_in_order_summary">Preferisci riprodurre gli album in ordine</string>
|
||||
<string name="settings_play_order_in_order">Riproduci gli album in ordine</string>
|
||||
<string name="settings_play_order_shuffle_summary">Preferisci mescolare le tracce degli album</string>
|
||||
<string name="settings_play_order_shuffle">Album casuali</string>
|
||||
<string name="settings_play_order">Ordine di riproduzione preferito</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB verranno utilizzati per memorizzare le tracce per la riproduzione offline</string>
|
||||
<string name="settings_media_cache_size">Dimensioni della cache multimediale</string>
|
||||
<string name="settings_media_quality_summary_size">Verrà riprodotta la traccia più piccola disponibile</string>
|
||||
<string name="settings_media_quality_summary_quality">Verrà riprodotta la migliore versione disponibile</string>
|
||||
<string name="settings_media_quality_size">Dimensioni minime</string>
|
||||
<string name="settings_media_quality_quality">Migliore qualità</string>
|
||||
<string name="settings_media_quality">Qualità dei media</string>
|
||||
<string name="settings_general">Generale</string>
|
||||
<string name="search_no_results">Nessun risultato trovato per la tua ricerca</string>
|
||||
<string name="search_welcome">Inserisci i termini di ricerca sopra e premi invio per cercare nella tua raccolta</string>
|
||||
<string name="search_placeholder">Cerca artisti, album e brani</string>
|
||||
<string name="title_oss_licences">Licenze open source</string>
|
||||
<string name="title_settings">Impostazioni</string>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="toolbar_search">Cerca</string>
|
||||
<string name="login_error_userinfo">Non siamo riusciti a recuperare le informazioni sul tuo utente</string>
|
||||
<string name="login_error_hostname_https">Il nome dell\'host di Funkwhale dovrebbe essere protetto tramite HTTPS</string>
|
||||
<string name="login_error_hostname">Questo non può essere interpretato come un URL valido</string>
|
||||
<string name="login_logging_in">Entrando</string>
|
||||
<string name="login_submit">Accedi</string>
|
||||
<string name="login_password">Password</string>
|
||||
<string name="login_username">Nome utente</string>
|
||||
<string name="login_anonymous">Autenticazione anonima</string>
|
||||
<string name="login_cleartext">Consenti traffico in chiaro (HTTP)</string>
|
||||
<string name="login_hostname">Nome dell\'host</string>
|
||||
<string name="login_welcome">Inserisci i dettagli della tua istanza di Funkwhale per accedere al suo contenuto</string>
|
||||
</resources>
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="settings_experiments">実験的な機能を有効にする</string>
|
||||
<string name="title_settings">設定</string>
|
||||
<string name="title_oss_licences">オープンソースライセンス</string>
|
||||
<string name="settings_general">一般設定</string>
|
||||
<string name="settings_media_cache_size">メディアのキャッシュサイズ</string>
|
||||
<string name="settings_media_cache_size_summary">オフライン再生向けの曲の保存のために、%d GBが使われます</string>
|
||||
<string name="settings_night_mode">ダークモード</string>
|
||||
<string name="settings_other">その他の設定</string>
|
||||
<string name="settings_experiments_restart_title">アプリの再起動が必要です</string>
|
||||
<string name="settings_experiments_restart_content">変更を適用するためにアプリを終了し、再起動して下さい</string>
|
||||
<string name="settings_information">情報</string>
|
||||
<string name="settings_information_repository_title">Gitリポジトリ</string>
|
||||
<string name="settings_information_repository_description">Otter by Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_version_title">バージョン</string>
|
||||
<string name="settings_information_license_title">ライセンス</string>
|
||||
<string name="settings_information_license_description">MIT License</string>
|
||||
<string name="settings_crash_report_title">クラッシュログのコピー</string>
|
||||
<string name="login_username">ユーザーネーム</string>
|
||||
<string name="login_password">パスワード</string>
|
||||
<string name="login_submit">ログイン</string>
|
||||
<string name="login_logging_in">ログインしています</string>
|
||||
<string name="toolbar_search">検索</string>
|
||||
<string name="title_downloads">ダウンロード一覧</string>
|
||||
<string name="search_placeholder">アーティスト、アルバム、曲を探す</string>
|
||||
<string name="settings_experiments_description">あなた自身のリスクで使用してください。アプリがフリーズしたり、クラッシュするかもしれません</string>
|
||||
<string name="settings_crash_report_copied">クリップボードに前回のクラッシュレポートがコピーされました</string>
|
||||
<string name="track_info_details">曲情報</string>
|
||||
<string name="track_info_details_title">この曲について</string>
|
||||
<string name="track_info_details_artist">アーティスト</string>
|
||||
<string name="track_info_album">アルバムのページへ</string>
|
||||
<string name="playback_queue_add_item">キューに追加する</string>
|
||||
<string name="playback_queue_play_next">次の曲を再生</string>
|
||||
<string name="playback_queue_clear">クリア</string>
|
||||
<string name="playback_queue_save">保存</string>
|
||||
<string name="manage_add_to_favorites">お気に入りに追加</string>
|
||||
<string name="control_previous">前の曲</string>
|
||||
<string name="control_next">次の曲</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="other">%dアルバム</item>
|
||||
</plurals>
|
||||
<string name="alt_album_cover">アルバムカバー</string>
|
||||
<string name="alt_artist_art">アーティストアート</string>
|
||||
<string name="alt_app_logo">アプリロゴ</string>
|
||||
<string name="alt_track_info">曲についての情報</string>
|
||||
<string name="radio_random_title">ランダム</string>
|
||||
<string name="logout_title">サインアウト</string>
|
||||
<string name="track_info_details_track_title">曲名</string>
|
||||
<string name="track_info_details_track_copyright">コピーライト</string>
|
||||
<string name="track_info_details_track_license">ライセンス</string>
|
||||
<string name="track_info_details_track_duration">再生時間</string>
|
||||
<string name="track_info_details_track_bitrate">ビットレート</string>
|
||||
<string name="track_info_details_track_instance">Funkwhaleインスタンス</string>
|
||||
<string name="radio_instance_radios">インスタンスのラジオ</string>
|
||||
<string name="radio_user_radios">ユーザーのラジオ</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="other">%1$d曲 • %2$s分</item>
|
||||
</plurals>
|
||||
<string name="playlist_add_to">プレイリストに追加</string>
|
||||
<string name="playlist_add_to_create">プレイリストを作成</string>
|
||||
<string name="logout_content">本当にこのFunkwhaleポッドからサインアウトしますか?</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="other">%1$d曲をダウンロードしています</item>
|
||||
</plurals>
|
||||
<string name="playback_queue_empty">キューに何もありません</string>
|
||||
<string name="settings_logout">サインアウト</string>
|
||||
<string name="artists">アーティスト</string>
|
||||
<string name="albums">アルバム</string>
|
||||
<string name="tracks">曲</string>
|
||||
<string name="playlists">プレイリスト</string>
|
||||
<string name="radios">ラジオ</string>
|
||||
<string name="favorites">お気に入り</string>
|
||||
<string name="settings_night_mode_on">常にオン(ダークモード)</string>
|
||||
<string name="settings_night_mode_on_summary">常にダークモードになっています</string>
|
||||
<string name="settings_night_mode_off">常にオフ(ライトモード)</string>
|
||||
<string name="settings_night_mode_off_summary">常にライトモードになっています</string>
|
||||
<string name="settings_night_mode_system">システムの設定に従う</string>
|
||||
<string name="settings_media_quality_quality">最高品質</string>
|
||||
<string name="settings_media_quality_size">最小サイズ</string>
|
||||
<string name="settings_media_quality">メディアのクオリティー</string>
|
||||
<string name="settings_media_quality_summary_quality">最高品質のメディアが再生されます</string>
|
||||
<string name="settings_media_quality_summary_size">サイズが小さな曲が再生されます</string>
|
||||
<string name="settings_play_order">好きな再生方法</string>
|
||||
<string name="settings_play_order_shuffle">アルバムの曲をシャッフル再生</string>
|
||||
<string name="settings_play_order_shuffle_summary">アルバムの曲をシャッフル再生します</string>
|
||||
<string name="settings_play_order_in_order_summary">アルバムの曲を順番に再生します</string>
|
||||
<string name="settings_play_order_in_order">アルバムの曲を上から順に再生</string>
|
||||
<string name="settings_night_mode_system_summary">システムの設定に従います</string>
|
||||
<string name="settings_crash_report_description">Otterのログは、クラッシュするまでの5分間だけ収集されます</string>
|
||||
<string name="playback_media_controls">メディアコントロール</string>
|
||||
<string name="playback_media_controls_description">メディアの再生について管理する</string>
|
||||
<string name="playback_play">再生</string>
|
||||
<string name="playback_shuffle">シャッフル</string>
|
||||
<string name="playback_queue_remove_item">除く</string>
|
||||
<string name="login_anonymous">匿名での認証</string>
|
||||
<string name="login_hostname">ホストネーム</string>
|
||||
<string name="login_error_hostname">有効なURLとして認識されませんでした</string>
|
||||
<string name="login_error_hostname_https">FunkwhaleのホストネームはHTTPSから始まる安全なものにすべきです</string>
|
||||
<string name="login_error_userinfo">あなたのユーザー情報を取得できませんでした</string>
|
||||
<string name="playback_queue">キュー</string>
|
||||
<string name="playback_queue_download">ダウンロード</string>
|
||||
<string name="track_info_details_album">アルバム</string>
|
||||
<string name="track_info_artist">アーティストのページへ</string>
|
||||
<string name="filters">フィルター</string>
|
||||
<string name="login_welcome">Funkwhaleポッドのコンテンツにアクセスするため、ポッドについての情報を入力してください</string>
|
||||
<string name="login_cleartext">平文での通信(HTTP)を許可する</string>
|
||||
<string name="error_playback">この曲は再生できませんでした</string>
|
||||
<string name="radio_playback_error">ラジオの再生を試みてる間にエラーがありました</string>
|
||||
</resources>
|
|
@ -0,0 +1,129 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="login_welcome">Skriv inn detaljene til en Funkwhale-instans for å få tilgang til dens innhold</string>
|
||||
<string name="login_anonymous">Anonym identitetsbekreftelse</string>
|
||||
<string name="login_username">Brukernavn</string>
|
||||
<string name="login_password">Passord</string>
|
||||
<string name="login_submit">Logg inn</string>
|
||||
<string name="login_logging_in">Logger inn …</string>
|
||||
<string name="login_error_hostname">Skriv inn en gyldig nettadresse først</string>
|
||||
<string name="login_error_hostname_https">Funkwhale-vertsnavnet burde sikres med HTTPS</string>
|
||||
<string name="login_error_userinfo">Kunne ikke hente brukerinfo</string>
|
||||
<string name="toolbar_search">Søk</string>
|
||||
<string name="title_downloads">Nedlastninger</string>
|
||||
<string name="title_settings">Innstillinger</string>
|
||||
<string name="title_oss_licences">Friprog-lisenser</string>
|
||||
<string name="search_welcome">Skriv inn søket din ovenfor og trykk enter for å søke i samlingen din</string>
|
||||
<string name="search_no_results">Resultatløst</string>
|
||||
<string name="settings_general">Generelt</string>
|
||||
<string name="settings_media_quality">Mediakvalitet</string>
|
||||
<string name="settings_media_quality_size">Minste størrelse</string>
|
||||
<string name="settings_media_quality_summary_quality">Beste tilgjengelige versjon vil bli avspilt</string>
|
||||
<string name="settings_media_quality_summary_size">Minste tilgjengelige versjon vil bli avspilt</string>
|
||||
<string name="settings_media_cache_size">Media-hurtiglagringsstørrelse</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB vil brukes til å lagre spor for frakoblet avspilling</string>
|
||||
<string name="search_placeholder">Søk etter artister, album og spor</string>
|
||||
<string name="settings_media_quality_quality">Beste kvalitet</string>
|
||||
<string name="error_playback">Kunne ikke spille av sporet</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Laster ned %1$d spor…</item>
|
||||
<item quantity="other">Laster ned %1$d spor…</item>
|
||||
</plurals>
|
||||
<string name="login_hostname">Vertsnavn</string>
|
||||
<string name="login_cleartext">Tillat klarteksttrafikk (HTTP)</string>
|
||||
<string name="settings_play_order">Foretrukket avspillingsrekkefølge</string>
|
||||
<string name="settings_play_order_in_order">Spill album i rekkefølge</string>
|
||||
<string name="settings_play_order_in_order_summary">Du foretrekker avspilling av album i rekkefølge</string>
|
||||
<string name="settings_other">Annet</string>
|
||||
<string name="settings_night_mode">Mørk drakt</string>
|
||||
<string name="settings_night_mode_on">Alltid på (mørk drakt)</string>
|
||||
<string name="settings_play_order_shuffle_summary">Du foretrekker omstokking av album</string>
|
||||
<string name="settings_play_order_shuffle">Omstokking av album</string>
|
||||
<string name="settings_night_mode_on_summary">Mørk drakt vil alltid iføres</string>
|
||||
<string name="settings_night_mode_off">Alltid av (lys drakt)</string>
|
||||
<string name="settings_night_mode_off_summary">Lys drakt vil alltid iføres</string>
|
||||
<string name="settings_experiments_restart_content">Avslutt og start programmet igjen for å bruke endringene</string>
|
||||
<string name="settings_experiments_restart_title">Programomstart kreves</string>
|
||||
<string name="settings_experiments_description">Bruk på egen risiko. Kan fryse eller krasje programmet.</string>
|
||||
<string name="settings_night_mode_system">Følg system</string>
|
||||
<string name="settings_experiments">Eksperimentelle funksjoner</string>
|
||||
<string name="settings_night_mode_system_summary">Nattmodus vil følge systemet</string>
|
||||
<string name="settings_information_repository_description">Otter av Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_version_title">Versjon</string>
|
||||
<string name="settings_information_license_title">Lisens</string>
|
||||
<string name="settings_information_license_description">MIT-lisens</string>
|
||||
<string name="settings_crash_report_title">Kopier krasjloggføring</string>
|
||||
<string name="settings_information_repository_title">Pakkebrønn</string>
|
||||
<string name="settings_information">Info</string>
|
||||
<string name="settings_crash_report_description">Kun Otter sine logger 5 minutter før krasjet vil samles inn</string>
|
||||
<string name="settings_crash_report_copied">Siste krasjrapport kopiert til utklippstavlen</string>
|
||||
<string name="settings_logout">Logg ut</string>
|
||||
<string name="artists">Artister</string>
|
||||
<string name="albums">Album</string>
|
||||
<string name="tracks">Spor</string>
|
||||
<string name="playlists">Spillelister</string>
|
||||
<string name="radios">Radiostasjoner</string>
|
||||
<string name="favorites">Favoritter</string>
|
||||
<string name="playback_media_controls">Mediakontroller</string>
|
||||
<string name="playback_media_controls_description">Kontroller mediaavspilling</string>
|
||||
<string name="playback_play">Spill</string>
|
||||
<string name="playback_shuffle">Omstokking</string>
|
||||
<string name="playback_queue_remove_item">Fjern</string>
|
||||
<string name="playback_queue_add_item">Legg til i avspillingskø</string>
|
||||
<string name="playback_queue">Avspillingskø</string>
|
||||
<string name="playback_queue_empty">Avspillingskøen din er tom</string>
|
||||
<string name="playback_queue_play_next">Spill av neste</string>
|
||||
<string name="playback_queue_download">Last ned</string>
|
||||
<string name="playback_queue_clear">Tøm</string>
|
||||
<string name="playback_queue_save">Lagre</string>
|
||||
<string name="manage_add_to_favorites">Legg til som favoritt</string>
|
||||
<string name="control_toggle">Veksle avspilling</string>
|
||||
<string name="control_previous">Forrige spor</string>
|
||||
<string name="control_next">Neste spor</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="other">%d album</item>
|
||||
</plurals>
|
||||
<string name="alt_artist_art">Artistkunst</string>
|
||||
<string name="alt_album_cover">Albumsomslag</string>
|
||||
<string name="alt_more_options">Flere innstillinger</string>
|
||||
<string name="track_info_artist">Gå til artist</string>
|
||||
<string name="track_info_album">Gå til album</string>
|
||||
<string name="track_info_details_title">Spordetaljer</string>
|
||||
<string name="track_info_details_artist">Artist</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_track_title">Sportittel</string>
|
||||
<string name="track_info_details_track_copyright">Opphavsrett</string>
|
||||
<string name="track_info_details_track_license">Lisens</string>
|
||||
<string name="track_info_details_track_duration">Varighet</string>
|
||||
<string name="track_info_details_track_position">Albumposisjon</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_instance">Funkwhate-instans</string>
|
||||
<string name="radio_instance_radios">Instanse-radiostasjoner</string>
|
||||
<string name="radio_user_radios">Bruker-radiostasjoner</string>
|
||||
<string name="radio_your_content_title">Ditt innhold</string>
|
||||
<string name="logout_title">Logg ut</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d spor • %2$s</item>
|
||||
<item quantity="other">%1$d spor • %2$s</item>
|
||||
</plurals>
|
||||
<string name="radio_your_content_description">Fra dine egne bibliotek</string>
|
||||
<string name="radio_random_title">Tilfeldig</string>
|
||||
<string name="logout_content">Logg ut av denne Funkwhale-instansen\?</string>
|
||||
<string name="radio_less_listened_description">Lytt til spor du vanligvis ikke spiller.</string>
|
||||
<string name="radio_less_listened_title">Sjeldnere spilt</string>
|
||||
<string name="radio_favorites_description">Spill dine favoritter i en uendelig gledesløkke.</string>
|
||||
<string name="radio_playback_error">Kunne ikke spille denne radiostasjonen</string>
|
||||
<string name="track_info_details">Info</string>
|
||||
<string name="alt_track_info">Sporinfo</string>
|
||||
<string name="alt_app_logo">Programlogo</string>
|
||||
<string name="radio_random_description">Helt tilfeldig plukket. Kanskje du vil oppdage noe nytt\?</string>
|
||||
<string name="playlist_add_to">Legg til i spilleliste</string>
|
||||
<string name="playlist_add_to_new">Ny spilleliste …</string>
|
||||
<string name="playlist_add_to_create">Opprett spilleliste</string>
|
||||
<string name="filters">Filtre</string>
|
||||
<string name="fiters_all">All musikk</string>
|
||||
<string name="filters_my_music">Min musikk</string>
|
||||
<string name="filters_followed">Fulgt innhold</string>
|
||||
<string name="playlist_added_to">Lag til i «%s»-spillelisten</string>
|
||||
</resources>
|
|
@ -1,9 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="surface">#121212</color>
|
||||
<color name="elevatedSurface">#191919</color>
|
||||
|
||||
<color name="colorPrimary">#283f4e</color>
|
||||
<color name="colorAccent">#f1b44f</color>
|
||||
<color name="colorAccent">#3282b8</color>
|
||||
|
||||
<color name="colorSelected">#525252</color>
|
||||
<color name="colorFavorite">#eba999</color>
|
||||
|
|
|
@ -39,9 +39,9 @@
|
|||
<string name="login_submit">Log in</string>
|
||||
<string name="login_password">Wachtwoord</string>
|
||||
<string name="login_username">Gebruikersnaam</string>
|
||||
<string name="login_anonymous">Anoniem aanmelden</string>
|
||||
<string name="login_anonymous">Anoniem inloggen</string>
|
||||
<string name="login_hostname">Servernaam</string>
|
||||
<string name="login_welcome">Voer de gegevens van je Funkwhale-server in om toegang te krijgen tot je media</string>
|
||||
<string name="login_welcome">Voer de gegevens van een Funkwhale-server in om toegang te krijgen tot de media</string>
|
||||
<string name="track_info_details_track_duration">Duur</string>
|
||||
<string name="track_info_details_track_title">Nummer-titel</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
|
@ -65,10 +65,10 @@
|
|||
<string name="control_toggle">Afspelen / Pauze</string>
|
||||
<string name="manage_add_to_favorites">Aan favorieten toevoegen</string>
|
||||
<string name="playback_queue_play_next">Volgende afspelen</string>
|
||||
<string name="playback_queue_add_item">Toevoegen aan wachtrij</string>
|
||||
<string name="playback_queue_add_item">Toevoegen aan afspeellijst</string>
|
||||
<string name="playback_queue_remove_item">Verwijder</string>
|
||||
<string name="playback_queue_empty">De wachtrij is leeg</string>
|
||||
<string name="playback_queue">Wachtrij</string>
|
||||
<string name="playback_queue_empty">De afspeellijst is leeg</string>
|
||||
<string name="playback_queue">Afspeellijst</string>
|
||||
<string name="playback_shuffle">Shuffle</string>
|
||||
<string name="favorites">Favorieten</string>
|
||||
<string name="radios">Radio\'s</string>
|
||||
|
@ -84,4 +84,5 @@
|
|||
<string name="settings_experiments_restart_content">Beëindig en herstart de app om nieuwe instelling toe te passen</string>
|
||||
<string name="settings_experiments_restart_title">Opnieuw opstarten vereist</string>
|
||||
<string name="settings_experiments_description">Gebruik op eigen risico; kan de app bevriezen of crashen</string>
|
||||
<string name="login_cleartext">Onversleutelde verbindingen toestaan (http)</string>
|
||||
</resources>
|
|
@ -0,0 +1,132 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Pobieranie %1$d nagrania</item>
|
||||
<item quantity="few">Pobieranie %1$d nagrań</item>
|
||||
<item quantity="many">Pobieranie %1$d nagrań</item>
|
||||
</plurals>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d nagranie • %2$s</item>
|
||||
<item quantity="few">%1$d nagrania • %2$s</item>
|
||||
<item quantity="many">%1$d nagrań • %2$s</item>
|
||||
</plurals>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="few">%d albumy</item>
|
||||
<item quantity="many">%d albumów</item>
|
||||
</plurals>
|
||||
<string name="filters_followed">Obserwowane</string>
|
||||
<string name="filters_my_music">Moje utwory</string>
|
||||
<string name="fiters_all">Wszystkie utwory</string>
|
||||
<string name="playlist_added_to">Dodano do grajlisty %s</string>
|
||||
<string name="playlist_add_to_create">Stwórz grajlistę</string>
|
||||
<string name="playlist_add_to_new">Nowa grajlista…</string>
|
||||
<string name="playlist_add_to">Dodaj do grajlisty</string>
|
||||
<string name="logout_content">Czy na pewno chcesz się wylogować z tego konta Funkwhale\?</string>
|
||||
<string name="radio_less_listened_description">Posłuchaj tego, na co zwykle nie masz czasu.</string>
|
||||
<string name="radio_less_listened_title">Rzadziej słuchane</string>
|
||||
<string name="radio_random_description">Może los przyniesie coś nowego\?</string>
|
||||
<string name="radio_random_title">Losowe</string>
|
||||
<string name="radio_favorites_description">Ulubione utwory na okrągło.</string>
|
||||
<string name="radio_your_content_description">Korzysta z Twoich własnych zasobów</string>
|
||||
<string name="radio_your_content_title">Twoje utowry</string>
|
||||
<string name="radio_user_radios">Dodane przez Ciebie</string>
|
||||
<string name="radio_instance_radios">Radio instancji</string>
|
||||
<string name="radio_playback_error">Próba odtworzenia radia skończyła się błędem</string>
|
||||
<string name="track_info_details_track_instance">Źródło</string>
|
||||
<string name="track_info_details_track_bitrate">Próbkowanie</string>
|
||||
<string name="track_info_details_track_position">Kolejność w albumie</string>
|
||||
<string name="track_info_details_title">Dane utworu</string>
|
||||
<string name="track_info_details">Informacje</string>
|
||||
<string name="alt_track_info">Informacje o utworze</string>
|
||||
<string name="alt_artist_art">Ikona artysty</string>
|
||||
<string name="playback_media_controls_description">Powiadomienie z przyciskami do pauzowania i przełączania utworów</string>
|
||||
<string name="playback_media_controls">Kontrola odtwarzania</string>
|
||||
<string name="login_error_hostname_https">Błąd szyfrowanego połączenia (HTTPS)</string>
|
||||
<string name="playlists">Grajlisty</string>
|
||||
<string name="settings_play_order_in_order_summary">Wolisz słuchać albumów w domyślnej kolejności</string>
|
||||
<string name="settings_play_order_shuffle_summary">Wolisz słuchać albumów w losowej kolejności</string>
|
||||
<string name="search_no_results">Niczego nie znaleziono</string>
|
||||
<string name="search_welcome">Wpisz wyszukiwaną nazwę i wciśnij enter</string>
|
||||
<string name="login_error_userinfo">Nie znaleziono takiego użytkownika</string>
|
||||
<string name="login_hostname">Adres WWW</string>
|
||||
<string name="login_welcome">Wypełnij dane swojej instancji (serwera) Funkwhale</string>
|
||||
<string name="filters">Filtry</string>
|
||||
<string name="logout_title">Wyloguj się</string>
|
||||
<string name="track_info_details_track_duration">Długość</string>
|
||||
<string name="track_info_details_track_license">Licencja</string>
|
||||
<string name="track_info_details_track_copyright">Prawa autorskie</string>
|
||||
<string name="track_info_details_track_title">Tytuł utworu</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_artist">Artysta</string>
|
||||
<string name="track_info_album">Przejdź do utworu</string>
|
||||
<string name="track_info_artist">Przejdź do artysty</string>
|
||||
<string name="alt_more_options">Ustawienia dodatkowe</string>
|
||||
<string name="alt_album_cover">Okładka albumu</string>
|
||||
<string name="playback_queue">Kolejka</string>
|
||||
<string name="playback_shuffle">Wymieszaj</string>
|
||||
<string name="playback_play">Odtwórz</string>
|
||||
<string name="radios">Radio</string>
|
||||
<string name="settings_crash_report_copied">Szczegóły ostatniej awarii zostały skopiowane do schowka</string>
|
||||
<string name="settings_crash_report_description">Zostaną skopiowane tylko działania tego programu z ostatnich 5 minut przed awarią</string>
|
||||
<string name="settings_crash_report_title">Skopiuj szczegóły awarii</string>
|
||||
<string name="settings_information_repository_description">Otter stworzony przez Antoine\'a POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Repozytorium</string>
|
||||
<string name="settings_information">Informacje</string>
|
||||
<string name="settings_experiments_restart_content">Do wprowadzenia zmian należy zrestartować program</string>
|
||||
<string name="settings_experiments_restart_title">Wymagany restart</string>
|
||||
<string name="settings_play_order_in_order">Odtwarzaj albumy po kolei</string>
|
||||
<string name="settings_play_order_shuffle">Losuj albumy</string>
|
||||
<string name="search_placeholder">Szukaj artystów, albumów i utworów</string>
|
||||
<string name="title_oss_licences">Licencje bibliotek</string>
|
||||
<string name="title_downloads">Pobrane</string>
|
||||
<string name="toolbar_search">Szukaj</string>
|
||||
<string name="alt_app_logo">Ikona programu</string>
|
||||
<string name="error_playback">Utwór nie mógł zostać odtworzony</string>
|
||||
<string name="control_next">Następny utwór</string>
|
||||
<string name="control_previous">Poprzedni utwór</string>
|
||||
<string name="control_toggle">Pauza</string>
|
||||
<string name="manage_add_to_favorites">Dodaj do ulubionych</string>
|
||||
<string name="playback_queue_save">Zapisz</string>
|
||||
<string name="playback_queue_clear">Wyczyść</string>
|
||||
<string name="playback_queue_download">Pobierz</string>
|
||||
<string name="playback_queue_play_next">Odtwórz następne</string>
|
||||
<string name="playback_queue_add_item">Dodaj do kolejki</string>
|
||||
<string name="playback_queue_remove_item">Usuń</string>
|
||||
<string name="playback_queue_empty">Kolejka jest pusta</string>
|
||||
<string name="favorites">Ulubione</string>
|
||||
<string name="tracks">Nagrania</string>
|
||||
<string name="albums">Albumy</string>
|
||||
<string name="artists">Artyści</string>
|
||||
<string name="settings_logout">Wyloguj się</string>
|
||||
<string name="settings_information_license_description">Licencja MIT</string>
|
||||
<string name="settings_information_license_title">Licencja</string>
|
||||
<string name="settings_version_title">Wersja</string>
|
||||
<string name="settings_experiments_description">Mogą powodować mniejszą stabilność programu</string>
|
||||
<string name="settings_experiments">Włącz funkcje eksperymentalne</string>
|
||||
<string name="settings_night_mode_system_summary">Jasność będzie zależna od ustawień telefonu</string>
|
||||
<string name="settings_night_mode_system">Wedle ustawień systemowych</string>
|
||||
<string name="settings_night_mode_off_summary">Używasz jasnego trybu</string>
|
||||
<string name="settings_night_mode_off">Wyłączony</string>
|
||||
<string name="settings_night_mode_on_summary">Używasz trybu nocnego</string>
|
||||
<string name="settings_night_mode_on">Włączony</string>
|
||||
<string name="settings_night_mode">Tryb nocny</string>
|
||||
<string name="settings_other">Inne</string>
|
||||
<string name="settings_play_order">Kolejka odtwarzania</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB zostanie przeznaczone na utwory dostępne offline</string>
|
||||
<string name="settings_media_cache_size">Wielkość pamięci podręcznej</string>
|
||||
<string name="settings_media_quality_summary_size">Zostanie ograniczony przesył danych</string>
|
||||
<string name="settings_media_quality_summary_quality">Usłyszysz dźwięk w najlepszej dostępnej jakości</string>
|
||||
<string name="settings_media_quality_size">Najmniejszy rozmiar</string>
|
||||
<string name="settings_media_quality_quality">Najlepsza jakość</string>
|
||||
<string name="settings_media_quality">Jakość dźwięku</string>
|
||||
<string name="settings_general">Ogólne</string>
|
||||
<string name="title_settings">Ustawienia</string>
|
||||
<string name="login_error_hostname">To nie jest prawidłowy adres URL</string>
|
||||
<string name="login_logging_in">Trwa logowanie</string>
|
||||
<string name="login_submit">Zaloguj się</string>
|
||||
<string name="login_password">Hasło</string>
|
||||
<string name="login_username">Nazwa użytkownika</string>
|
||||
<string name="login_anonymous">Dostęp anonimowy</string>
|
||||
<string name="login_cleartext">Zezwól na nieszyfrowany przesył (HTTP)</string>
|
||||
</resources>
|
|
@ -0,0 +1,134 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="login_error_hostname">Введите корректный URL</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Загрузка %1$d трека…</item>
|
||||
<item quantity="few">Загрузка %1$d треков…</item>
|
||||
<item quantity="many">Загрузка %1$d треков…</item>
|
||||
<item quantity="other">Загрузка %1$d треков…</item>
|
||||
</plurals>
|
||||
<string name="only_my_music">Только моя музыка</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d трек • %2$s</item>
|
||||
<item quantity="few">%1$d трека • %2$s</item>
|
||||
<item quantity="many">%1$d треков • %2$s</item>
|
||||
</plurals>
|
||||
<string name="logout_content">Выйти из этого инстанса Funkwhale\?</string>
|
||||
<string name="logout_title">Выйти</string>
|
||||
<string name="radio_less_listened_description">Проигрывает треки которые вы обычно не слушаете.</string>
|
||||
<string name="radio_less_listened_title">Малопрослушиваемые</string>
|
||||
<string name="radio_random_description">Полностью случайный выбор, может вы обнаружите для себя новые треки\?</string>
|
||||
<string name="radio_random_title">Случайный порядок</string>
|
||||
<string name="radio_favorites_description">Проигрывает ваши любимые мелодии по кругу и никогда не заканчивается.</string>
|
||||
<string name="radio_your_content_description">Подобрано из вашей библиотеки</string>
|
||||
<string name="radio_your_content_title">Ваш контент</string>
|
||||
<string name="radio_user_radios">Пользовательское радио</string>
|
||||
<string name="radio_instance_radios">Радио инстанса</string>
|
||||
<string name="radio_playback_error">Не получилось воспроизвести это радио</string>
|
||||
<string name="track_info_details_track_instance">Инстанс Funkwhale</string>
|
||||
<string name="track_info_details_track_bitrate">Битрейт</string>
|
||||
<string name="track_info_details_track_position">Позиция в альбоме</string>
|
||||
<string name="track_info_details_track_duration">Продолжительность</string>
|
||||
<string name="track_info_details_track_license">Лицензия</string>
|
||||
<string name="track_info_details_track_copyright">Авторские права</string>
|
||||
<string name="track_info_details_track_title">Название трека</string>
|
||||
<string name="track_info_details_album">Альбом</string>
|
||||
<string name="track_info_details_artist">Исполнитель</string>
|
||||
<string name="track_info_details_title">Информация о треке</string>
|
||||
<string name="track_info_details">Информация</string>
|
||||
<string name="track_info_album">Перейти к альбому</string>
|
||||
<string name="track_info_artist">Перейти к исполнителю</string>
|
||||
<string name="alt_track_info">Информация о треке</string>
|
||||
<string name="alt_more_options">Больше параметров</string>
|
||||
<string name="alt_album_cover">Обложка альбома</string>
|
||||
<string name="alt_artist_art">Изображение исполнителя</string>
|
||||
<string name="alt_app_logo">Иконка приложения</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d альбом</item>
|
||||
<item quantity="few">%d альбома</item>
|
||||
<item quantity="many">%d альбомов</item>
|
||||
</plurals>
|
||||
<string name="error_playback">Не получилось воспроизвести</string>
|
||||
<string name="control_next">Следующий трек</string>
|
||||
<string name="control_previous">Прошлый трек</string>
|
||||
<string name="control_toggle">Переключить воспроизведение</string>
|
||||
<string name="manage_add_to_favorites">Добавить в избранное</string>
|
||||
<string name="playback_queue_download">Загрузить</string>
|
||||
<string name="playback_queue_play_next">Играть следующим</string>
|
||||
<string name="playback_queue_add_item">Добавить в очередь воспроизведения</string>
|
||||
<string name="playback_queue_remove_item">Удалить из очереди воспроизведения</string>
|
||||
<string name="playback_queue_empty">Ваша очередь воспроизведения пуста</string>
|
||||
<string name="playback_queue">Очередь воспроизведения</string>
|
||||
<string name="playback_shuffle">Перемешать</string>
|
||||
<string name="playback_media_controls_description">Управление воспроизведением медиа</string>
|
||||
<string name="playback_media_controls">Управление медиа</string>
|
||||
<string name="favorites">Любимые</string>
|
||||
<string name="radios">Радио</string>
|
||||
<string name="playlists">Плейлисты</string>
|
||||
<string name="tracks">Треки</string>
|
||||
<string name="albums">Альбомы</string>
|
||||
<string name="artists">Исполнители</string>
|
||||
<string name="settings_logout">Выйти</string>
|
||||
<string name="settings_crash_report_copied">Последний отчёт о сбое скопирован в ваш буфер обмена</string>
|
||||
<string name="settings_crash_report_description">Будут собраны только логи Otter за последние 5 минут до сбоя</string>
|
||||
<string name="settings_crash_report_title">Скопировать журнал сбоев</string>
|
||||
<string name="settings_information_license_description">Лицензия MIT</string>
|
||||
<string name="settings_information_license_title">Лицензия</string>
|
||||
<string name="settings_version_title">Версия</string>
|
||||
<string name="settings_information_repository_description">Otter создан Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">Репозиторий</string>
|
||||
<string name="settings_information">Информация</string>
|
||||
<string name="settings_experiments_restart_content">Закройте и запустите приложение, чтобы изменения применились</string>
|
||||
<string name="settings_experiments_restart_title">Требуется перезагрузка приложения</string>
|
||||
<string name="settings_experiments_description">Используйте на свой страх и риск, приложение может подтормаживать или сбоить.</string>
|
||||
<string name="settings_experiments">Экспериментальные возможности</string>
|
||||
<string name="settings_night_mode_system_summary">Тёмный режим будет следовать настройкам системы</string>
|
||||
<string name="settings_night_mode_system">Следовать настройкам системы</string>
|
||||
<string name="settings_night_mode_off_summary">Светлый режим всегда будет включен</string>
|
||||
<string name="settings_night_mode_off">Всегда выключен (светлый режим)</string>
|
||||
<string name="settings_night_mode_on_summary">Темный режим всегда будет включен</string>
|
||||
<string name="settings_night_mode_on">Всегда включён (тёмный режим)</string>
|
||||
<string name="settings_night_mode">Тёмный режим</string>
|
||||
<string name="settings_other">Другие</string>
|
||||
<string name="settings_media_cache_size_summary">%d ГБ будет использовано для сохранения треков для оффлайн воспроизведения</string>
|
||||
<string name="settings_media_cache_size">Размер медиакеша</string>
|
||||
<string name="settings_media_quality_summary_size">Будет проиграна наихудшая версия</string>
|
||||
<string name="settings_media_quality_summary_quality">Будет проиграна наилучшая версия</string>
|
||||
<string name="settings_media_quality_size">Наихудшее качество</string>
|
||||
<string name="settings_media_quality_quality">Лучшее качество</string>
|
||||
<string name="settings_media_quality">Качество медиа</string>
|
||||
<string name="settings_general">Основные</string>
|
||||
<string name="search_no_results">По вашему запросу ничего не найдено</string>
|
||||
<string name="search_welcome">Введите ваш поисковый запрос и нажмите Enter для поиска в вашей коллекции</string>
|
||||
<string name="search_placeholder">Поиск исполнителей, альбомов и треков</string>
|
||||
<string name="title_oss_licences">Лицензии открытого исходного кода</string>
|
||||
<string name="title_settings">Настройки</string>
|
||||
<string name="title_downloads">Загрузки</string>
|
||||
<string name="toolbar_search">Поиск</string>
|
||||
<string name="login_error_userinfo">Мы не смогли получить информацию о вашем аккаунте</string>
|
||||
<string name="login_error_hostname_https">Имя хоста Funkwhale должно быть защищено с помощью HTTPS</string>
|
||||
<string name="login_logging_in">Происходит вход…</string>
|
||||
<string name="login_submit">Войти</string>
|
||||
<string name="login_password">Пароль</string>
|
||||
<string name="login_username">Имя пользователя</string>
|
||||
<string name="login_anonymous">Анонимная аутентификация</string>
|
||||
<string name="login_cleartext">Разрешить незашифрованный тарфик (HTTP)</string>
|
||||
<string name="login_hostname">Доменное имя</string>
|
||||
<string name="login_welcome">Введите данные вашего инстанса Funkwhale для доступа к контенту</string>
|
||||
<string name="playlist_add_to_new">Новый плейлист…</string>
|
||||
<string name="filters_followed">Подписки на контент</string>
|
||||
<string name="filters_my_music">Моя музыка</string>
|
||||
<string name="fiters_all">Вся музыка</string>
|
||||
<string name="filters">Фильтры</string>
|
||||
<string name="playlist_added_to">Добавлено в плейлист “%s“</string>
|
||||
<string name="playlist_add_to_create">Создать плейлист</string>
|
||||
<string name="playlist_add_to">Добавить в плейлист</string>
|
||||
<string name="playback_queue_save">Сохранить</string>
|
||||
<string name="playback_queue_clear">Очистить</string>
|
||||
<string name="playback_play">Играть</string>
|
||||
<string name="settings_play_order_in_order_summary">Предпочитаемый порядок проигрывания альбомов</string>
|
||||
<string name="settings_play_order_in_order">Проигрывать альбомы в порядке</string>
|
||||
<string name="settings_play_order_shuffle_summary">Перемешивать трэки в альбомах</string>
|
||||
<string name="settings_play_order_shuffle">Перемешать альбомы</string>
|
||||
<string name="settings_play_order">Предпочтительный порядок проигрывания</string>
|
||||
</resources>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="filters">පෙරහන්</string>
|
||||
<string name="title_oss_licences">විවෘත මූලාශ්ර බලපත්ර</string>
|
||||
<string name="title_settings">සැකසුම්</string>
|
||||
<string name="title_downloads">බාගැනීම්</string>
|
||||
<string name="toolbar_search">සොයන්න</string>
|
||||
<string name="login_logging_in">පිවිසෙමින්</string>
|
||||
<string name="login_submit">පිවිසෙන්න</string>
|
||||
<string name="login_password">මුර පදය</string>
|
||||
<string name="login_username">පරිශීලක නාමය</string>
|
||||
</resources>
|
|
@ -1,21 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">下载 %1$d 曲目</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other">下载 %1$d 曲目…</item>
|
||||
</plurals>
|
||||
<string name="only_my_music">只有我的音乐</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d 曲目 • %2$s</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="logout_content">是否确实要退出此 Funkwhale 实例?</string>
|
||||
<string name="logout_content">退出此 Funkwhale 实例?</string>
|
||||
<string name="logout_title">登出</string>
|
||||
<string name="radio_less_listened_description">听一些你通常不听的曲目。是时候恢复一些平衡了。</string>
|
||||
<string name="radio_less_listened_title">少听</string>
|
||||
<string name="radio_less_listened_description">听一些你通常不听的曲目。</string>
|
||||
<string name="radio_less_listened_title">听的最少的</string>
|
||||
<string name="radio_random_description">完全随机挑选,也许你会发现新的东西?</string>
|
||||
<string name="radio_random_title">随机</string>
|
||||
<string name="radio_playback_error">尝试播放此收音机时出错</string>
|
||||
<string name="radio_playback_error">无法播放此电台</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale 实例</string>
|
||||
<string name="track_info_details_track_bitrate">比特率</string>
|
||||
<string name="track_info_details_track_position">专辑位置</string>
|
||||
|
@ -27,14 +26,14 @@
|
|||
<string name="track_info_details">信息</string>
|
||||
<string name="track_info_album">进入专辑</string>
|
||||
<string name="track_info_artist">转到艺术家</string>
|
||||
<string name="alt_track_info">有关音乐的信息</string>
|
||||
<string name="alt_track_info">音乐信息</string>
|
||||
<string name="alt_more_options">更多操作</string>
|
||||
<string name="alt_album_cover">专辑封面</string>
|
||||
<string name="alt_artist_art">艺术家</string>
|
||||
<string name="alt_app_logo">应用程序徽标</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d专辑</item>
|
||||
<item quantity="other"></item>
|
||||
<item quantity="other"/>
|
||||
</plurals>
|
||||
<string name="error_playback">无法播放此曲目</string>
|
||||
<string name="control_next">下一曲</string>
|
||||
|
@ -58,7 +57,7 @@
|
|||
<string name="artists">艺术家</string>
|
||||
<string name="settings_logout">登出</string>
|
||||
<string name="settings_crash_report_copied">上次崩溃报告已复制到剪贴板</string>
|
||||
<string name="settings_crash_report_description">仅从崩溃前的最后 5 分钟开始,仅包含 Otter 的日志</string>
|
||||
<string name="settings_crash_report_description">仅收集从崩溃前的最后 5 分钟开始的 Otter日志</string>
|
||||
<string name="settings_crash_report_title">复制崩溃日志</string>
|
||||
<string name="settings_information_license_description">MIT许可证</string>
|
||||
<string name="settings_information_license_title">许可证</string>
|
||||
|
@ -66,10 +65,10 @@
|
|||
<string name="settings_information_repository_description">Otter来自Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_information_repository_title">代码库</string>
|
||||
<string name="settings_information">信息</string>
|
||||
<string name="settings_experiments_restart_content">请终止并重新启动应用程序,以便此更改生效</string>
|
||||
<string name="settings_experiments_restart_title">需要重新启动</string>
|
||||
<string name="settings_experiments_description">自行承担使用风险,可能会冻结或崩溃应用</string>
|
||||
<string name="settings_experiments">启用实验功能</string>
|
||||
<string name="settings_experiments_restart_content">请关闭并重新启动应用程序,以应用新设置</string>
|
||||
<string name="settings_experiments_restart_title">应用需要重新启动</string>
|
||||
<string name="settings_experiments_description">自行承担使用风险,可能会冻结或崩溃应用。</string>
|
||||
<string name="settings_experiments">实验功能</string>
|
||||
<string name="settings_night_mode_system_summary">夜间模式将遵循系统设置</string>
|
||||
<string name="settings_night_mode_system">遵循系统设置</string>
|
||||
<string name="settings_night_mode_off_summary">明亮模式将始终打开</string>
|
||||
|
@ -87,19 +86,44 @@
|
|||
<string name="settings_media_quality">媒体质量</string>
|
||||
<string name="settings_general">常规</string>
|
||||
<string name="search_no_results">未找到查询结果</string>
|
||||
<string name="search_welcome">在上面输入您的搜索词,然后按Enter搜索您的收藏</string>
|
||||
<string name="search_welcome">在上面输入您的搜索词,然后按 Enter 搜索您的收藏</string>
|
||||
<string name="search_placeholder">搜索艺术家、专辑、曲目</string>
|
||||
<string name="title_oss_licences">开放源码许可证</string>
|
||||
<string name="title_oss_licences">自由授权条款</string>
|
||||
<string name="title_settings">设置</string>
|
||||
<string name="title_downloads">下载</string>
|
||||
<string name="toolbar_search">搜索</string>
|
||||
<string name="login_error_hostname_https">应通过HTTPS保护Funkwhale主机名</string>
|
||||
<string name="login_error_hostname">这不能理解为有效的 URL</string>
|
||||
<string name="login_logging_in">登录中</string>
|
||||
<string name="login_error_hostname">请先输入一个有效的 URL</string>
|
||||
<string name="login_logging_in">登录中…</string>
|
||||
<string name="login_submit">登录</string>
|
||||
<string name="login_password">密码</string>
|
||||
<string name="login_username">用户名</string>
|
||||
<string name="login_anonymous">匿名身份验证</string>
|
||||
<string name="login_hostname">主机名称</string>
|
||||
<string name="login_welcome">请输入您的Funkwhale实例的详细信息以访问其内容</string>
|
||||
<string name="radio_favorites_description">在永无止境的幸福循环中播放你最喜欢的曲子。</string>
|
||||
<string name="radio_your_content_description">从您自己的图书馆中挑选</string>
|
||||
<string name="radio_your_content_title">您的内容</string>
|
||||
<string name="radio_user_radios">用户电台</string>
|
||||
<string name="radio_instance_radios">实例电台</string>
|
||||
<string name="track_info_details_track_license">许可证</string>
|
||||
<string name="track_info_details_track_copyright">版权所有</string>
|
||||
<string name="login_error_userinfo">我们无法获取用户信息</string>
|
||||
<string name="login_cleartext">允许明文流量(HTTP)</string>
|
||||
<string name="filters_followed">跟随内容</string>
|
||||
<string name="filters_my_music">我的音乐</string>
|
||||
<string name="fiters_all">所有的音乐</string>
|
||||
<string name="filters">过滤</string>
|
||||
<string name="playback_queue_clear">清除</string>
|
||||
<string name="playback_play">播放</string>
|
||||
<string name="settings_play_order_in_order_summary">你喜欢按顺序播放专辑</string>
|
||||
<string name="settings_play_order_in_order">按顺序播放专辑</string>
|
||||
<string name="settings_play_order_shuffle_summary">你喜欢随机播放专辑曲目吗</string>
|
||||
<string name="settings_play_order_shuffle">专辑重新排序</string>
|
||||
<string name="settings_play_order">首选播放顺序</string>
|
||||
<string name="playlist_added_to">添加到播放列表 %s</string>
|
||||
<string name="playlist_add_to_create">创建播放列表</string>
|
||||
<string name="playlist_add_to_new">新播放列表…</string>
|
||||
<string name="playlist_add_to">加入播放列表</string>
|
||||
<string name="playback_queue_save">保存</string>
|
||||
</resources>
|
|
@ -10,6 +10,16 @@
|
|||
<item>size</item>
|
||||
</array>
|
||||
|
||||
<array name="play_orders">
|
||||
<item>@string/settings_play_order_shuffle</item>
|
||||
<item>@string/settings_play_order_in_order</item>
|
||||
</array>
|
||||
|
||||
<array name="play_orders_values">
|
||||
<item>shuffle</item>
|
||||
<item>in_order</item>
|
||||
</array>
|
||||
|
||||
<array name="night_mode">
|
||||
<item>@string/settings_night_mode_on</item>
|
||||
<item>@string/settings_night_mode_off</item>
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="surface">@android:color/background_light</color>
|
||||
<color name="elevatedSurface">@android:color/background_light</color>
|
||||
|
||||
<color name="colorPrimary">#327eae</color>
|
||||
<color name="colorPrimaryDark">#3d3e40</color>
|
||||
|
|
|
@ -1,113 +1,130 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="app_name" translatable="false">Otter</string>
|
||||
<string name="login_welcome">Please enter the details of your Funkwhale instance to access its content</string>
|
||||
<string name="login_hostname">Host name</string>
|
||||
<string name="login_cleartext">Allow cleartext traffic (HTTP)</string>
|
||||
<string name="login_anonymous">Anonymous authentication</string>
|
||||
<string name="login_username">Username</string>
|
||||
<string name="login_password">Password</string>
|
||||
<string name="login_submit">Log in</string>
|
||||
<string name="login_logging_in">Logging in</string>
|
||||
<string name="login_error_hostname">This could not be understood as a valid URL</string>
|
||||
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
|
||||
<string name="login_error_userinfo">We could not retrieve information about your user</string>
|
||||
<string name="toolbar_search">Search</string>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="title_oss_licences">Open source licences</string>
|
||||
<string name="search_placeholder">Search artists, albums and tracks</string>
|
||||
<string name="search_welcome">Enter your search terms above and hit enter to search your collection</string>
|
||||
<string name="search_no_results">No results were found for your query</string>
|
||||
<string name="settings_general">General</string>
|
||||
<string name="settings_media_quality">Media quality</string>
|
||||
<string name="settings_media_quality_quality">Best quality</string>
|
||||
<string name="settings_media_quality_size">Smallest size</string>
|
||||
<string name="settings_media_quality_summary_quality">Best available version will be played</string>
|
||||
<string name="settings_media_quality_summary_size">Smallest available track will be played</string>
|
||||
<string name="settings_media_cache_size">Media cache size</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB will be used to store tracks for offline playback</string>
|
||||
<string name="settings_other">Other</string>
|
||||
<string name="settings_night_mode">Dark mode</string>
|
||||
<string name="settings_night_mode_on">Always on (dark mode)</string>
|
||||
<string name="settings_night_mode_on_summary">Dark mode will always be on</string>
|
||||
<string name="settings_night_mode_off">Always off (light mode)</string>
|
||||
<string name="settings_night_mode_off_summary">Light mode will always be on</string>
|
||||
<string name="settings_night_mode_system">Follow system settings</string>
|
||||
<string name="settings_night_mode_system_summary">Night mode will follow system settings</string>
|
||||
<string name="settings_experiments">Enable experimental features</string>
|
||||
<string name="settings_experiments_description">Use at your own risks, may freeze or crash the app</string>
|
||||
<string name="settings_experiments_restart_title">Restart required</string>
|
||||
<string name="settings_experiments_restart_content">Please kill and restart the app in order for this change to take effect</string>
|
||||
<string name="settings_information">Information</string>
|
||||
<string name="settings_information_repository_title">Repository</string>
|
||||
<string name="settings_information_repository_description">Otter by Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_version_title">Version</string>
|
||||
<string name="settings_information_license_title">License</string>
|
||||
<string name="settings_information_license_description">MIT license</string>
|
||||
<string name="settings_crash_report_title">Copy crash logs</string>
|
||||
<string name="settings_crash_report_description">Only Otter\'s logs from the last 5 minutes up until the crash will be collected</string>
|
||||
<string name="settings_crash_report_copied">Last crash report was copied to your clipboard</string>
|
||||
<string name="settings_logout">Sign out</string>
|
||||
<string name="artists">Artists</string>
|
||||
<string name="albums">Albums</string>
|
||||
<string name="tracks">Tracks</string>
|
||||
<string name="playlists">Playlists</string>
|
||||
<string name="radios">Radios</string>
|
||||
<string name="favorites">Favorites</string>
|
||||
<string name="playback_media_controls">Media controls</string>
|
||||
<string name="playback_media_controls_description">Control media playback</string>
|
||||
<string name="playback_shuffle">Shuffle</string>
|
||||
<string name="playback_queue">Queue</string>
|
||||
<string name="playback_queue_empty">Your queue is empty</string>
|
||||
<string name="playback_queue_remove_item">Remove</string>
|
||||
<string name="playback_queue_add_item">Add to queue</string>
|
||||
<string name="playback_queue_play_next">Play next</string>
|
||||
<string name="playback_queue_download">Download</string>
|
||||
<string name="manage_add_to_favorites">Add to favorites</string>
|
||||
<string name="control_toggle">Toggle playback</string>
|
||||
<string name="control_previous">Previous track</string>
|
||||
<string name="control_next">Next track</string>
|
||||
<string name="error_playback">This track could not be played</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="other">%d albums</item>
|
||||
</plurals>
|
||||
<string name="alt_app_logo">Application logo</string>
|
||||
<string name="alt_artist_art">Artist art</string>
|
||||
<string name="alt_album_cover">Album cover</string>
|
||||
<string name="alt_more_options">More options</string>
|
||||
<string name="alt_track_info">Information about track</string>
|
||||
<string name="track_info_artist">Go to artist</string>
|
||||
<string name="track_info_album">Go to album</string>
|
||||
<string name="track_info_details">Information</string>
|
||||
<string name="track_info_details_title">Track details</string>
|
||||
<string name="track_info_details_artist">Artist</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_track_title">Track title</string>
|
||||
<string name="track_info_details_track_duration">Duration</string>
|
||||
<string name="track_info_details_track_position">Album position</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instance</string>
|
||||
<string name="radio_playback_error">There was an error while trying to play this radio</string>
|
||||
<string name="radio_instance_radios">Instance radios</string>
|
||||
<string name="radio_user_radios">User radios</string>
|
||||
<string name="radio_your_content_title">Your content</string>
|
||||
<string name="radio_your_content_description">Picks from your own libraries</string>
|
||||
<string name="radio_favorites_description"> Play your favorites tunes in a never-ending happiness loop.</string>
|
||||
<string name="radio_random_title">Random</string>
|
||||
<string name="radio_random_description">Totally random picks, maybe you\'ll discover new things?</string>
|
||||
<string name="radio_less_listened_title">Less listened</string>
|
||||
<string name="radio_less_listened_description">Listen to tracks you usually don\'t. It\'s time to restore some balance.</string>
|
||||
<string name="logout_title">Sign out</string>
|
||||
<string name="logout_content">Are you sure you want to sign out of this Funkwhale instance\?</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d track • %2$s"</item>
|
||||
<item quantity="other">%1$d tracks • %2$s"</item>
|
||||
</plurals>
|
||||
<string name="only_my_music">Only my music</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Downloading %1$d track</item>
|
||||
<item quantity="other">Downloading %1$d tracks</item>
|
||||
</plurals>
|
||||
</resources>
|
||||
<string name="app_name" translatable="false">Otter</string>
|
||||
<string name="login_welcome">Please enter details for a Funkwhale instance to access its content</string>
|
||||
<string name="login_hostname">Hostname</string>
|
||||
<string name="login_cleartext">Allow cleartext traffic (HTTP)</string>
|
||||
<string name="login_anonymous">Anonymous authentication</string>
|
||||
<string name="login_username">Username</string>
|
||||
<string name="login_password">Password</string>
|
||||
<string name="login_submit">Log in</string>
|
||||
<string name="login_logging_in">Logging in…</string>
|
||||
<string name="login_error_hostname">Enter a valid URL first</string>
|
||||
<string name="login_error_hostname_https">The Funkwhale hostname should be secure through HTTPS</string>
|
||||
<string name="login_error_userinfo">Could not fetch user info</string>
|
||||
<string name="toolbar_search">Search</string>
|
||||
<string name="title_downloads">Downloads</string>
|
||||
<string name="title_settings">Settings</string>
|
||||
<string name="title_oss_licences">Libre licences</string>
|
||||
<string name="search_placeholder">Search artists, albums and tracks</string>
|
||||
<string name="search_welcome">Enter your search above and hit enter to search in your collection</string>
|
||||
<string name="search_no_results">No results</string>
|
||||
<string name="settings_general">General</string>
|
||||
<string name="settings_media_quality">Media quality</string>
|
||||
<string name="settings_media_quality_quality">Best quality</string>
|
||||
<string name="settings_media_quality_size">Smallest size</string>
|
||||
<string name="settings_media_quality_summary_quality">Best available version will be played</string>
|
||||
<string name="settings_media_quality_summary_size">Smallest available track will be played</string>
|
||||
<string name="settings_media_cache_size">Cache size for media</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB will be used to store tracks for offline playback</string>
|
||||
<string name="settings_play_order">Preferred playback order</string>
|
||||
<string name="settings_play_order_shuffle">Shuffle albums</string>
|
||||
<string name="settings_play_order_shuffle_summary">You prefer shuffling album tracks</string>
|
||||
<string name="settings_play_order_in_order">Play albums in order</string>
|
||||
<string name="settings_play_order_in_order_summary">You prefer playing albums in order</string>
|
||||
<string name="settings_other">Other</string>
|
||||
<string name="settings_night_mode">Dark mode</string>
|
||||
<string name="settings_night_mode_on">Always on (dark mode)</string>
|
||||
<string name="settings_night_mode_on_summary">Dark mode will always be on</string>
|
||||
<string name="settings_night_mode_off">Always off (light mode)</string>
|
||||
<string name="settings_night_mode_off_summary">Light mode will always be on</string>
|
||||
<string name="settings_night_mode_system">Follow system</string>
|
||||
<string name="settings_night_mode_system_summary">Night mode will follow the system</string>
|
||||
<string name="settings_experiments">Experimental features</string>
|
||||
<string name="settings_experiments_description">Use at your own risks. It may freeze or crash the app.</string>
|
||||
<string name="settings_experiments_restart_title">App restart required</string>
|
||||
<string name="settings_experiments_restart_content">Close and restart the app to use the new settings</string>
|
||||
<string name="settings_information">Info</string>
|
||||
<string name="settings_information_repository_title">Repository</string>
|
||||
<string name="settings_information_repository_description">Otter by Antoine POPINEAU (apognu)</string>
|
||||
<string name="settings_version_title">Version</string>
|
||||
<string name="settings_information_license_title">License</string>
|
||||
<string name="settings_information_license_description">MIT license</string>
|
||||
<string name="settings_crash_report_title">Copy crash logs</string>
|
||||
<string name="settings_crash_report_description">Only Otter logs from 5 minutes before crash will be collected</string>
|
||||
<string name="settings_crash_report_copied">Last crash report copied to your clipboard</string>
|
||||
<string name="settings_logout">Sign out</string>
|
||||
<string name="artists">Artists</string>
|
||||
<string name="albums">Albums</string>
|
||||
<string name="tracks">Tracks</string>
|
||||
<string name="playlists">Playlists</string>
|
||||
<string name="radios">Radios</string>
|
||||
<string name="favorites">Favorites</string>
|
||||
<string name="playback_media_controls">Media controls</string>
|
||||
<string name="playback_media_controls_description">Control media playback</string>
|
||||
<string name="playback_play">Play</string>
|
||||
<string name="playback_shuffle">Shuffle</string>
|
||||
<string name="playback_queue">Queue</string>
|
||||
<string name="playback_queue_empty">Your queue is empty</string>
|
||||
<string name="playback_queue_remove_item">Remove</string>
|
||||
<string name="playback_queue_add_item">Add to queue</string>
|
||||
<string name="playback_queue_play_next">Play next</string>
|
||||
<string name="playback_queue_download">Download</string>
|
||||
<string name="playback_queue_clear">Clear</string>
|
||||
<string name="playback_queue_save">Save</string>
|
||||
<string name="manage_add_to_favorites">Add to favorites</string>
|
||||
<string name="control_toggle">Toggle playback</string>
|
||||
<string name="control_previous">Previous track</string>
|
||||
<string name="control_next">Next track</string>
|
||||
<string name="error_playback">Could not play this track</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="other">%d albums</item>
|
||||
</plurals>
|
||||
<string name="alt_app_logo">App logo</string>
|
||||
<string name="alt_artist_art">Artist art</string>
|
||||
<string name="alt_album_cover">Album cover</string>
|
||||
<string name="alt_more_options">More options</string>
|
||||
<string name="alt_track_info">Track info</string>
|
||||
<string name="track_info_artist">Go to artist</string>
|
||||
<string name="track_info_album">Go to album</string>
|
||||
<string name="track_info_details">Info</string>
|
||||
<string name="track_info_details_title">Track details</string>
|
||||
<string name="track_info_details_artist">Artist</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_track_title">Track title</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_license">License</string>
|
||||
<string name="track_info_details_track_duration">Duration</string>
|
||||
<string name="track_info_details_track_position">Album position</string>
|
||||
<string name="track_info_details_track_bitrate">Bitrate</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instance</string>
|
||||
<string name="radio_playback_error">Could not play this radio station</string>
|
||||
<string name="radio_instance_radios">Instance radios</string>
|
||||
<string name="radio_user_radios">User radios</string>
|
||||
<string name="radio_your_content_title">Your content</string>
|
||||
<string name="radio_your_content_description">Picks from your own libraries</string>
|
||||
<string name="radio_favorites_description">Play your favorites in a never-ending happiness loop.</string>
|
||||
<string name="radio_random_title">Random</string>
|
||||
<string name="radio_random_description">Totally random picks. Maybe you\'ll discover new things\?</string>
|
||||
<string name="radio_less_listened_title">Less played</string>
|
||||
<string name="radio_less_listened_description">Listen to tracks you usually don\'t.</string>
|
||||
<string name="logout_title">Sign out</string>
|
||||
<string name="logout_content">Sign out of this Funkwhale instance\?</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d track • %2$s"</item>
|
||||
<item quantity="other">%1$d tracks • %2$s"</item>
|
||||
</plurals>
|
||||
<string name="playlist_add_to">Add to playlist</string>
|
||||
<string name="playlist_add_to_new">New playlist…</string>
|
||||
<string name="playlist_add_to_create">Create playlist</string>
|
||||
<string name="playlist_added_to">Added to “%s” playlist</string>
|
||||
<string name="filters">Filters</string>
|
||||
<string name="fiters_all">All music</string>
|
||||
<string name="filters_my_music">My music</string>
|
||||
<string name="filters_followed">Followed content</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Downloading %1$d track…</item>
|
||||
<item quantity="other">Downloading %1$d tracks…</item>
|
||||
</plurals>
|
||||
</resources>
|
|
@ -69,7 +69,13 @@
|
|||
<item name="android:drawableTint" tools:targetApi="m">@android:color/white</item>
|
||||
<item name="android:tint">@android:color/white</item>
|
||||
|
||||
<item name="android:popupTheme">@style/ThemeOverlay.AppCompat.DayNight</item>
|
||||
<item name="actionBarPopupTheme">@style/AppTheme.PopupMenu</item>
|
||||
<item name="popupTheme">@style/AppTheme.PopupMenu</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Toolbar.Primary">
|
||||
<item name="android:drawableTint" tools:targetApi="m">@color/blackWhileLight</item>
|
||||
<item name="android:tint">@color/blackWhileLight</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.FloatingBottomSheet" parent="Theme.MaterialComponents.DayNight.BottomSheetDialog">
|
||||
|
@ -99,4 +105,14 @@
|
|||
<item name="android:textColor">@android:color/white</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.IconButton" parent="Widget.MaterialComponents.Button.OutlinedButton">
|
||||
<item name="iconPadding">0dp</item>
|
||||
<item name="android:insetBottom">0dp</item>
|
||||
<item name="android:paddingLeft">12dp</item>
|
||||
<item name="android:paddingRight">12dp</item>
|
||||
<item name="android:minWidth">43dp</item>
|
||||
<item name="android:minHeight">43dp</item>
|
||||
<item name="android:background">@android:color/transparent</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config xmlns:tools="http://schemas.android.com/tools">
|
||||
<base-config>
|
||||
<trust-anchors>
|
||||
|
||||
<certificates src="system" />
|
||||
|
||||
<certificates
|
||||
src="user"
|
||||
tools:ignore="AcceptsUserCertificates" />
|
||||
|
||||
</trust-anchors>
|
||||
</base-config>
|
||||
</network-security-config>
|
|
@ -22,6 +22,14 @@
|
|||
app:showSeekBarValue="true"
|
||||
app:updatesContinuously="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="shuffle"
|
||||
android:entries="@array/play_orders"
|
||||
android:entryValues="@array/play_orders_values"
|
||||
android:icon="@drawable/play"
|
||||
android:key="play_order"
|
||||
android:title="@string/settings_play_order" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/settings_other">
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
Various UI and performance improvements, some bug fixes and the following new features:
|
||||
|
||||
* Initial support for Funkwhale 1.0
|
||||
* Always more translations (thanks to all contributors)
|
||||
* Keep downloads when disconnected
|
||||
* Support for cleartext connections and user-configured CAs
|
||||
* Changed item ordering to be more friendly
|
||||
* Enhanced metadata broadcast to work across devices
|
|
@ -0,0 +1,8 @@
|
|||
Diverses améliorations des performances et de l'UI, quelques corrections de bugs et ces nouvelles fonctionnalités :
|
||||
|
||||
* Support initial de Funkwhale 1.0
|
||||
* Ajout et amélioration des traductions (merci à tous les contributeurs)
|
||||
* Conservation des téléchargements lors des déconnexions
|
||||
* Support des connexions HTTP et utilisant des CA utilisateurs
|
||||
* Changement de l'ordre d'affichage des éléments
|
||||
* Amélioration de la diffusion des métadonnées de lecture pour supporter plus d'appareils
|
Binary file not shown.
Loading…
Reference in New Issue