Compare commits
66 Commits
Author | SHA1 | Date |
---|---|---|
Umut Solmaz | 2d705609df | |
josé m | 5de5adfda0 | |
Umut Solmaz | 93123f2df6 | |
Bruno-Van-den-Bosch | 52a45bc5df | |
Renovate Bot | bea9c5b75a | |
Renovate Bot | 3ebf11a6f6 | |
Hugh Daschbach | 5ee798abfb | |
Hugh Daschbach | 6f24535b79 | |
Hugh Daschbach | 467556d75c | |
Renovate Bot | 10035aa5fe | |
Georg Krause | 8b9a1201af | |
Hugh Daschbach | 2088e06a68 | |
Renovate Bot | 22a72d9e83 | |
Bruno-Van-den-Bosch | 2bdf904804 | |
mittwerk | 042d6b4d6e | |
mittwerk | 67aa47a4cb | |
Hugh Daschbach | 01c676acd8 | |
Hugh Daschbach | b27e4c85ee | |
Renovate Bot | c061c64c3d | |
Georg Krause | 554bc0ca5c | |
Georg Krause | ef54aad835 | |
Aitor | d23456d334 | |
Georg Krause | fef2d5b05f | |
josé m | 64f947aa23 | |
Thomas | 9666cccd5b | |
Hugh Daschbach | 36f1c7ba66 | |
Georg Krause | 1978fc4fb4 | |
Hugh Daschbach | c9056a2dbe | |
Hugh Daschbach | c1eb9d6b2a | |
Hugh Daschbach | b9ade47988 | |
Hugh Daschbach | 2133d4a4fb | |
Hugh Daschbach | feb86fe9c0 | |
Hugh Daschbach | f65e29af39 | |
Hugh Daschbach | 4dba9e29dd | |
Hugh Daschbach | 629ce2b309 | |
Renovate Bot | 080ba21c35 | |
Christophe Henry | 31908b6175 | |
Christophe Henry | 1a050c2d73 | |
Christophe Henry | 056e3a4d66 | |
Christophe Henry | b924a0c655 | |
Christophe Henry | 822adcac4a | |
Christophe Henry | fbbd90111d | |
Christophe Henry | 45773aac8d | |
josé m | 6472a3743e | |
Thomas | ada0b09a66 | |
Hugh Daschbach | 9c3d965a7e | |
Dylan Gageot | 5c5d86a728 | |
Dylan Gageot | 1288e050fd | |
Dylan Gageot | 8e09dccb9f | |
Dylan Gageot | 45ad4bdb8e | |
Dylan Gageot | 27e751df35 | |
Dylan Gageot | 33938e3705 | |
Renovate Bot | 1d5578febf | |
Georg krause | e1be5b1303 | |
Georg krause | 53bff969cd | |
Georg krause | cd82472c27 | |
Georg krause | 89c5718ac8 | |
Georg krause | de1cd69646 | |
Georg krause | 8da3cc78be | |
Georg krause | cc004dafdf | |
Georg krause | 22899ed2aa | |
Georg krause | de8343a973 | |
Georg krause | aab9e28e00 | |
Renovate Bot | f8838bae64 | |
Georg krause | 0075c10442 | |
Matyáš Caras | 103cac4145 |
|
@ -11,6 +11,7 @@ stages:
|
|||
- test
|
||||
- visualize
|
||||
- build
|
||||
- test-after-build
|
||||
- deploy
|
||||
|
||||
cache: &global_cache
|
||||
|
@ -38,7 +39,7 @@ cache: &global_cache
|
|||
before_script:
|
||||
- git fetch --unshallow --tags
|
||||
after_script:
|
||||
- export versionCode=`$ANDROID_HOME/build-tools/30.0.2/aapt dump badging $apk_file | grep versionCode | awk '{print $3}' | sed s/versionCode=//g | sed s/\'//g`
|
||||
- export versionCode=`$ANDROID_HOME/build-tools/30.0.3/aapt dump badging $apk_file | grep versionCode | awk '{print $3}' | sed s/versionCode=//g | sed s/\'//g`
|
||||
- apt update && apt install gettext-base
|
||||
- cat $metadata_template | envsubst > $metadata_file
|
||||
extends: .gradle-default
|
||||
|
@ -69,6 +70,14 @@ test:
|
|||
<<: *global_cache
|
||||
# override the policy
|
||||
policy: pull-push
|
||||
|
||||
test_nonfree_code:
|
||||
stage: test-after-build
|
||||
image: registry.funkwhale.audio/funkwhale/ci/android-fdroidserver
|
||||
script:
|
||||
- fdroid scanner -v app/build/outputs/apk/*/app-*.apk |& tee output.txt
|
||||
- cat output.txt
|
||||
- (! grep "CRITICAL" output.txt)
|
||||
|
||||
coverage:
|
||||
stage: visualize
|
||||
|
|
25
CHANGELOG
25
CHANGELOG
|
@ -1,3 +1,28 @@
|
|||
0.3.0 (2023-12-12)
|
||||
Features:
|
||||
|
||||
- Add option to limit bandwidth usage by streaming transcoded music
|
||||
- Improve player bottom sheet, in particular fling support
|
||||
|
||||
|
||||
Enhancements:
|
||||
|
||||
- Refactor CoverArt.withContext().
|
||||
|
||||
|
||||
Bugfixes:
|
||||
|
||||
- Fix buffering progress bar display
|
||||
- Fix landscape view induced MainActivity leak.
|
||||
- Fix Too Many Receivers exception
|
||||
|
||||
|
||||
0.2.1 (2023-04-18)
|
||||
Bugfixes:
|
||||
|
||||
- Removed navigation-dynamic-features-fragment, which has proprietary dependencies and isn't needed
|
||||
|
||||
|
||||
0.2.0 (2023-04-05)
|
||||
Features:
|
||||
|
||||
|
|
10
README.md
10
README.md
|
@ -30,13 +30,9 @@ Funkwhale for Android™ will try to behave as you would expect a mobile music p
|
|||
|
||||
## Screenshots
|
||||
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/4.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/5.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/6.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/7.png" width="200" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/1.png" width="33%" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/2.png" width="33%" />
|
||||
<img src="https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/raw/develop/app/src/main/play/listings/en-US/graphics/phone-screenshots/3.png" width="33%" />
|
||||
|
||||
## Translation
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@ plugins {
|
|||
id("kotlin-android")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("kotlin-parcelize")
|
||||
id("kotlin-kapt")
|
||||
|
||||
id("org.jlleitschuh.gradle.ktlint") version "11.2.0"
|
||||
id("com.gladed.androidgitversion") version "0.4.14"
|
||||
id("com.github.triplet.play") version "3.7.0"
|
||||
id("com.github.triplet.play") version "3.8.1"
|
||||
id("de.mobilej.unmock")
|
||||
id("com.github.ben-manes.versions")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
|
@ -35,8 +36,8 @@ androidGitVersion {
|
|||
|
||||
android {
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
namespace = "audio.funkwhale.ffa"
|
||||
|
@ -46,7 +47,7 @@ android {
|
|||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_1_8.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
buildFeatures {
|
||||
|
@ -176,19 +177,21 @@ dependencies {
|
|||
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
|
||||
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
|
||||
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.0")
|
||||
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||
implementation("androidx.recyclerview:recyclerview:1.2.1")
|
||||
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
|
||||
implementation("com.google.android.material:material:1.8.0")
|
||||
implementation("com.android.support.constraint:constraint-layout:2.0.4")
|
||||
implementation("com.google.android.material:material:1.9.0") {
|
||||
exclude("androidx.constraintlayout")
|
||||
}
|
||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||
|
||||
implementation("com.google.android.exoplayer:exoplayer-core:2.18.1")
|
||||
implementation("com.google.android.exoplayer:exoplayer-ui:2.18.1")
|
||||
implementation("com.google.android.exoplayer:extension-mediasession:2.18.1")
|
||||
|
||||
implementation("io.insert-koin:koin-core:3.3.2")
|
||||
implementation("io.insert-koin:koin-android:3.3.2")
|
||||
testImplementation("io.insert-koin:koin-test:3.3.2")
|
||||
implementation("io.insert-koin:koin-core:3.5.3")
|
||||
implementation("io.insert-koin:koin-android:3.5.3")
|
||||
testImplementation("io.insert-koin:koin-test:3.5.3")
|
||||
|
||||
implementation("com.github.PaulWoitaschek.ExoPlayer-Extensions:extension-opus:789a4f83169cff5c7a91655bb828fde2cfde671a") {
|
||||
isTransitive = false
|
||||
|
@ -209,13 +212,13 @@ dependencies {
|
|||
|
||||
implementation("androidx.navigation:navigation-fragment-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-ui-ktx:$navVersion")
|
||||
implementation("androidx.navigation:navigation-dynamic-features-fragment:$navVersion")
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
testImplementation("io.mockk:mockk:1.13.4")
|
||||
testImplementation("androidx.test:core:1.5.0")
|
||||
testImplementation("io.strikt:strikt-core:0.34.1")
|
||||
testImplementation("org.robolectric:robolectric:4.9.2")
|
||||
debugImplementation("io.sentry:sentry-android:6.17.0")
|
||||
|
||||
androidTestImplementation("io.mockk:mockk-android:1.13.4")
|
||||
androidTestImplementation("androidx.navigation:navigation-testing:$navVersion")
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
|
@ -22,7 +23,6 @@
|
|||
android:name=".activities.SplashActivity"
|
||||
android:launchMode="singleInstance"
|
||||
android:noHistory="true"
|
||||
android:screenOrientation="portrait"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
|
@ -41,8 +41,7 @@
|
|||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
android:name=".activities.MainActivity" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.DownloadsActivity"
|
||||
|
@ -56,6 +55,11 @@
|
|||
android:name=".activities.LicencesActivity"
|
||||
android:screenOrientation="portrait" />
|
||||
|
||||
<activity
|
||||
android:name="net.openid.appauth.AuthorizationManagementActivity"
|
||||
android:launchMode="@integer/launch_mode_for_app_auth"
|
||||
tools:replace="android:launchMode" />
|
||||
|
||||
<service
|
||||
android:name=".playback.PlayerService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
|
@ -85,6 +89,7 @@
|
|||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<meta-data android:name="io.sentry.dsn" android:value="https://4e377f47d01242baae2d9d8bd689c3ef@am.funkwhale.audio/4" />
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -1,9 +1,11 @@
|
|||
package audio.funkwhale.ffa.activities
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
|
@ -64,6 +66,13 @@ class LoginActivity : AppCompatActivity() {
|
|||
override fun onResume() {
|
||||
super.onResume()
|
||||
with(binding) {
|
||||
val preferences = getPreferences(Context.MODE_PRIVATE)
|
||||
val hn = preferences?.getString("hostname", "")
|
||||
if (hn != null && !hn.isEmpty()) {
|
||||
hostname.text = Editable.Factory.getInstance().newEditable(hn)
|
||||
}
|
||||
cleartext.setChecked(preferences?.getBoolean("cleartext", false) ?: false)
|
||||
anonymous.setChecked(preferences?.getBoolean("anonymous", false) ?: false)
|
||||
login.setOnClickListener {
|
||||
var hostname = hostname.text.toString().trim().trim('/')
|
||||
|
||||
|
@ -96,6 +105,12 @@ class LoginActivity : AppCompatActivity() {
|
|||
|
||||
hostnameField.error = message
|
||||
}
|
||||
if (hostnameField.error == null) {
|
||||
val preferences = getPreferences(Context.MODE_PRIVATE)
|
||||
preferences?.edit()?.putString("hostname", hostname)?.commit()
|
||||
preferences?.edit()?.putBoolean("cleartext", cleartext.isChecked)?.commit()
|
||||
preferences?.edit()?.putBoolean("anonymous", anonymous.isChecked)?.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,25 @@
|
|||
package audio.funkwhale.ffa.activities
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.animation.ObjectAnimator
|
||||
import android.animation.ValueAnimator
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Fragment
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Gravity
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.view.animation.AccelerateDecelerateInterpolator
|
||||
import android.widget.SeekBar
|
||||
import androidx.activity.addCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.fragment.NavHostFragment
|
||||
|
@ -33,57 +28,50 @@ import audio.funkwhale.ffa.R
|
|||
import audio.funkwhale.ffa.databinding.ActivityMainBinding
|
||||
import audio.funkwhale.ffa.fragments.AddToPlaylistDialog
|
||||
import audio.funkwhale.ffa.fragments.BrowseFragmentDirections
|
||||
import audio.funkwhale.ffa.fragments.LandscapeQueueFragment
|
||||
import audio.funkwhale.ffa.fragments.NowPlayingFragment
|
||||
import audio.funkwhale.ffa.fragments.QueueFragment
|
||||
import audio.funkwhale.ffa.fragments.TrackInfoDetailsFragment
|
||||
import audio.funkwhale.ffa.model.Track
|
||||
import audio.funkwhale.ffa.playback.MediaControlsManager
|
||||
import audio.funkwhale.ffa.playback.PinService
|
||||
import audio.funkwhale.ffa.playback.PlayerService
|
||||
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
||||
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
||||
import audio.funkwhale.ffa.repositories.Repository
|
||||
import audio.funkwhale.ffa.utils.AppContext
|
||||
import audio.funkwhale.ffa.utils.Command
|
||||
import audio.funkwhale.ffa.utils.CommandBus
|
||||
import audio.funkwhale.ffa.utils.CoverArt
|
||||
import audio.funkwhale.ffa.utils.Event
|
||||
import audio.funkwhale.ffa.utils.EventBus
|
||||
import audio.funkwhale.ffa.utils.FFACache
|
||||
import audio.funkwhale.ffa.utils.OAuth
|
||||
import audio.funkwhale.ffa.utils.ProgressBus
|
||||
import audio.funkwhale.ffa.utils.Request
|
||||
import audio.funkwhale.ffa.utils.RequestBus
|
||||
import audio.funkwhale.ffa.utils.Response
|
||||
import audio.funkwhale.ffa.utils.Settings
|
||||
import audio.funkwhale.ffa.utils.Userinfo
|
||||
import audio.funkwhale.ffa.utils.authorize
|
||||
import audio.funkwhale.ffa.utils.log
|
||||
import audio.funkwhale.ffa.utils.logError
|
||||
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
||||
import audio.funkwhale.ffa.utils.mustNormalizeUrl
|
||||
import audio.funkwhale.ffa.utils.onApi
|
||||
import audio.funkwhale.ffa.utils.toast
|
||||
import audio.funkwhale.ffa.utils.untilNetwork
|
||||
import audio.funkwhale.ffa.views.DisableableFrameLayout
|
||||
import audio.funkwhale.ffa.utils.wait
|
||||
import com.github.kittinunf.fuel.Fuel
|
||||
import com.github.kittinunf.fuel.coroutines.awaitStringResponse
|
||||
import com.google.android.exoplayer2.Player
|
||||
import com.google.android.exoplayer2.offline.DownloadService
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.gson.Gson
|
||||
import com.preference.PowerPreference
|
||||
import jp.wasabeef.picasso.transformations.RoundedCornersTransformation
|
||||
import kotlinx.coroutines.Dispatchers.Default
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koin.java.KoinJavaComponent.inject
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
enum class ResultCode(val code: Int) {
|
||||
LOGOUT(1001)
|
||||
}
|
||||
|
||||
private val favoriteRepository = FavoritesRepository(this)
|
||||
private val favoritedRepository = FavoritedRepository(this)
|
||||
private val favoritedRepository by lazy {
|
||||
FavoritedRepository(applicationContext)
|
||||
}
|
||||
private var menu: Menu? = null
|
||||
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
|
@ -96,16 +84,37 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
AppContext.init(this)
|
||||
AppContext.init(applicationContext)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
|
||||
(supportFragmentManager.findFragmentById(R.id.now_playing) as NowPlayingFragment).apply {
|
||||
onDetailsMenuItemClicked { binding.nowPlayingBottomSheet.close() }
|
||||
binding.nowPlayingBottomSheet.addBottomSheetCallback(
|
||||
object : BottomSheetBehavior.BottomSheetCallback() {
|
||||
override fun onStateChanged(bottomSheet: View, newState: Int) {
|
||||
// Add padding to the main fragment so that player control don't overlap
|
||||
// artists and albums
|
||||
addSiblingFragmentPadding()
|
||||
}
|
||||
|
||||
override fun onSlide(bottomSheet: View, slideOffset: Float) {
|
||||
// Animate the cover and other elements of the bottom sheet
|
||||
onBottomSheetDrag(slideOffset)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
addSiblingFragmentPadding()
|
||||
|
||||
setContentView(binding.root)
|
||||
|
||||
setSupportActionBar(binding.appbar)
|
||||
supportActionBar?.setDisplayShowTitleEnabled(false)
|
||||
|
||||
onBackPressedDispatcher.addCallback(this) {
|
||||
if (binding.nowPlaying.isOpened()) {
|
||||
binding.nowPlaying.close()
|
||||
if (binding.nowPlayingBottomSheet.isOpen) {
|
||||
binding.nowPlayingBottomSheet.close()
|
||||
} else {
|
||||
navigation.navigateUp()
|
||||
}
|
||||
|
@ -115,72 +124,33 @@ class MainActivity : AppCompatActivity() {
|
|||
MediaControlsManager.NOTIFICATION_ACTION_OPEN_QUEUE.toString() -> launchDialog(QueueFragment())
|
||||
}
|
||||
|
||||
watchEventBus()
|
||||
lifecycleScope.launch {
|
||||
RequestBus.send(Request.GetQueue).wait<Response.Queue>()?.let {
|
||||
if (it.queue.isNotEmpty() && binding.nowPlayingBottomSheet.isHidden) {
|
||||
binding.nowPlayingBottomSheet.show()
|
||||
} else if (it.queue.isEmpty()) {
|
||||
binding.nowPlayingBottomSheet.hide()
|
||||
}
|
||||
}
|
||||
// Watch the event bus only after to prevent concurrency in displaying the bottom sheet
|
||||
watchEventBus()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
findViewById<DisableableFrameLayout?>(R.id.container)?.apply {
|
||||
setShouldRegisterTouch {
|
||||
if (binding.nowPlaying.isOpened()) {
|
||||
binding.nowPlaying.close()
|
||||
false
|
||||
} else {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.nowPlaying.getFragment<NowPlayingFragment>().apply {
|
||||
favoritedRepository.update(applicationContext, lifecycleScope)
|
||||
|
||||
favoritedRepository.update(this, lifecycleScope)
|
||||
startService(Intent(applicationContext, PlayerService::class.java))
|
||||
DownloadService.start(applicationContext, PinService::class.java)
|
||||
|
||||
startService(Intent(this, PlayerService::class.java))
|
||||
DownloadService.start(this, PinService::class.java)
|
||||
CommandBus.send(Command.RefreshService)
|
||||
|
||||
CommandBus.send(Command.RefreshService)
|
||||
|
||||
lifecycleScope.launch(IO) {
|
||||
Userinfo.get(this@MainActivity, oAuth)
|
||||
}
|
||||
|
||||
with(binding) {
|
||||
|
||||
nowPlayingContainer?.nowPlayingToggle?.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
}
|
||||
|
||||
nowPlayingContainer?.nowPlayingNext?.setOnClickListener {
|
||||
CommandBus.send(Command.NextTrack)
|
||||
}
|
||||
|
||||
nowPlayingContainer?.nowPlayingDetailsPrevious?.setOnClickListener {
|
||||
CommandBus.send(Command.PreviousTrack)
|
||||
}
|
||||
|
||||
nowPlayingContainer?.nowPlayingDetailsNext?.setOnClickListener {
|
||||
CommandBus.send(Command.NextTrack)
|
||||
}
|
||||
|
||||
nowPlayingContainer?.nowPlayingDetailsToggle?.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.setOnSeekBarChangeListener(
|
||||
object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(view: SeekBar?) {}
|
||||
|
||||
override fun onStartTrackingTouch(view: SeekBar?) {}
|
||||
|
||||
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
CommandBus.send(Command.Seek(progress))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
landscapeQueue?.let {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(R.id.landscape_queue, LandscapeQueueFragment()).commit()
|
||||
lifecycleScope.launch(IO) {
|
||||
Userinfo.get(applicationContext, oAuth)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,7 +193,7 @@ class MainActivity : AppCompatActivity() {
|
|||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> {
|
||||
binding.nowPlaying.close()
|
||||
binding.nowPlayingBottomSheet.close()
|
||||
navigation.popBackStack(R.id.browseFragment, false)
|
||||
}
|
||||
|
||||
|
@ -284,6 +254,7 @@ class MainActivity : AppCompatActivity() {
|
|||
return false
|
||||
}
|
||||
}
|
||||
|
||||
R.id.nav_downloads -> startActivity(Intent(this, DownloadsActivity::class.java))
|
||||
R.id.settings -> resultLauncher.launch(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
@ -291,6 +262,20 @@ class MainActivity : AppCompatActivity() {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun addSiblingFragmentPadding() {
|
||||
val anim = if (binding.nowPlayingBottomSheet.isHidden) {
|
||||
ValueAnimator.ofInt(binding.nowPlayingBottomSheet.peekHeight, 0)
|
||||
} else {
|
||||
ValueAnimator.ofInt(0, binding.nowPlayingBottomSheet.peekHeight)
|
||||
}
|
||||
|
||||
anim.duration = 200
|
||||
anim.addUpdateListener {
|
||||
binding.navHostFragmentWrapper.setPadding(0, 0, 0, it.animatedValue as Int)
|
||||
}
|
||||
anim.start()
|
||||
}
|
||||
|
||||
private fun launchDialog(fragment: DialogFragment) =
|
||||
fragment.show(supportFragmentManager.beginTransaction(), "")
|
||||
|
||||
|
@ -298,321 +283,57 @@ class MainActivity : AppCompatActivity() {
|
|||
private fun watchEventBus() {
|
||||
lifecycleScope.launch(Main) {
|
||||
EventBus.get().collect { event ->
|
||||
if (event is Event.LogOut) {
|
||||
FFA.get().deleteAllData(this@MainActivity)
|
||||
startActivity(
|
||||
Intent(this@MainActivity, LoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
}
|
||||
)
|
||||
|
||||
finish()
|
||||
} else if (event is Event.PlaybackError) {
|
||||
toast(event.message)
|
||||
} else if (event is Event.Buffering) {
|
||||
when (event.value) {
|
||||
true -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.VISIBLE
|
||||
false -> binding.nowPlayingContainer?.nowPlayingBuffering?.visibility = View.GONE
|
||||
}
|
||||
} else if (event is Event.PlaybackStopped) {
|
||||
if (binding.nowPlaying.visibility == View.VISIBLE) {
|
||||
(binding.navHostFragment.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
it.bottomMargin = it.bottomMargin / 2
|
||||
}
|
||||
|
||||
binding.landscapeQueue?.let { landscape_queue ->
|
||||
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
it.bottomMargin = it.bottomMargin / 2
|
||||
when (event) {
|
||||
is Event.LogOut -> logout()
|
||||
is Event.PlaybackError -> toast(event.message)
|
||||
is Event.PlaybackStopped -> binding.nowPlayingBottomSheet.hide()
|
||||
is Event.TrackFinished -> incrementListenCount(event.track)
|
||||
is Event.QueueChanged -> {
|
||||
if (binding.nowPlayingBottomSheet.isHidden) binding.nowPlayingBottomSheet.show()
|
||||
findViewById<View>(R.id.nav_queue)?.let { view ->
|
||||
ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let {
|
||||
it.duration = 500
|
||||
it.interpolator = AccelerateDecelerateInterpolator()
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.nowPlaying.animate()
|
||||
.alpha(0.0f)
|
||||
.setDuration(400)
|
||||
.setListener(object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationEnd(animator: Animator) {
|
||||
binding.nowPlaying.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
.start()
|
||||
}
|
||||
} else if (event is Event.TrackFinished) {
|
||||
incrementListenCount(event.track)
|
||||
} else if (event is Event.StateChanged) {
|
||||
when (event.playing) {
|
||||
true -> {
|
||||
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
|
||||
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
|
||||
AppCompatResources.getDrawable(this@MainActivity, R.drawable.pause)
|
||||
}
|
||||
|
||||
false -> {
|
||||
binding.nowPlayingContainer?.nowPlayingToggle?.icon =
|
||||
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsToggle?.icon =
|
||||
AppCompatResources.getDrawable(this@MainActivity, R.drawable.play)
|
||||
}
|
||||
}
|
||||
} else if (event is Event.QueueChanged) {
|
||||
findViewById<View>(R.id.nav_queue)?.let { view ->
|
||||
ObjectAnimator.ofFloat(view, View.ROTATION, 0f, 360f).let {
|
||||
it.duration = 500
|
||||
it.interpolator = AccelerateDecelerateInterpolator()
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
CommandBus.get().collect { command ->
|
||||
if (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())
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
} else if (command is Command.RefreshTrack) {
|
||||
refreshCurrentTrack(command.track)
|
||||
} else if (command is Command.AddToPlaylist) {
|
||||
if (lifecycle.currentState.isAtLeast(Lifecycle.State.RESUMED)) {
|
||||
AddToPlaylistDialog.show(
|
||||
layoutInflater,
|
||||
this@MainActivity,
|
||||
lifecycleScope,
|
||||
command.tracks
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
ProgressBus.get().collect { (current, duration, percent) ->
|
||||
binding.nowPlayingContainer?.nowPlayingProgress?.progress = percent
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsProgress?.progress = percent
|
||||
|
||||
val currentMins = (current / 1000) / 60
|
||||
val currentSecs = (current / 1000) % 60
|
||||
|
||||
val durationMins = duration / 60
|
||||
val durationSecs = duration % 60
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsProgressCurrent?.text =
|
||||
"%02d:%02d".format(currentMins, currentSecs)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsProgressDuration?.text =
|
||||
"%02d:%02d".format(durationMins, durationSecs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
track?.let {
|
||||
if (binding.nowPlaying.visibility == View.GONE) {
|
||||
binding.nowPlaying.visibility = View.VISIBLE
|
||||
binding.nowPlaying.alpha = 0f
|
||||
|
||||
binding.nowPlaying.animate()
|
||||
.alpha(1.0f)
|
||||
.setDuration(400)
|
||||
.setListener(null)
|
||||
.start()
|
||||
|
||||
(binding.navHostFragment.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
it.bottomMargin = it.bottomMargin * 2
|
||||
}
|
||||
|
||||
binding.landscapeQueue?.let { landscape_queue ->
|
||||
(landscape_queue.layoutParams as? ViewGroup.MarginLayoutParams)?.let {
|
||||
it.bottomMargin = it.bottomMargin * 2
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingTitle?.text = track.title
|
||||
binding.nowPlayingContainer?.nowPlayingAlbum?.text = track.artist.name
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsTitle?.text = track.title
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsArtist?.text = track.artist.name
|
||||
|
||||
val lic = this.layoutInflater.context
|
||||
|
||||
CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.into(binding.nowPlayingContainer?.nowPlayingCover)
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsCover?.let { nowPlayingDetailsCover ->
|
||||
CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(nowPlayingDetailsCover)
|
||||
}
|
||||
|
||||
if (binding.nowPlayingContainer?.nowPlayingCover == null) {
|
||||
lifecycleScope.launch(Default) {
|
||||
val width = DisplayMetrics().apply {
|
||||
windowManager.defaultDisplay.getMetrics(this)
|
||||
}.widthPixels
|
||||
|
||||
val backgroundCover = CoverArt.withContext(lic, maybeNormalizeUrl(track.cover()))
|
||||
.get()
|
||||
.run { Bitmap.createScaledBitmap(this, width, width, false).toDrawable(resources) }
|
||||
.apply {
|
||||
alpha = 20
|
||||
gravity = Gravity.CENTER
|
||||
}
|
||||
|
||||
withContext(Main) {
|
||||
binding.nowPlayingContainer?.nowPlayingDetails?.background = backgroundCover
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.let { now_playing_details_repeat ->
|
||||
changeRepeatMode(FFACache.getLine(this@MainActivity, "repeat")?.toInt() ?: 0)
|
||||
|
||||
now_playing_details_repeat.setOnClickListener {
|
||||
val current = FFACache.getLine(this@MainActivity, "repeat")?.toInt() ?: 0
|
||||
|
||||
changeRepeatMode((current + 1) % 3)
|
||||
}
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsInfo?.let { nowPlayingDetailsInfo ->
|
||||
nowPlayingDetailsInfo.setOnClickListener {
|
||||
PopupMenu(
|
||||
CommandBus.get().flowWithLifecycle(
|
||||
this@MainActivity.lifecycle, Lifecycle.State.RESUMED
|
||||
).collect { command ->
|
||||
when (command) {
|
||||
is Command.StartService -> startService(command.command)
|
||||
is Command.RefreshTrack -> refreshTrack(command.track)
|
||||
is Command.AddToPlaylist -> AddToPlaylistDialog.show(
|
||||
layoutInflater,
|
||||
this@MainActivity,
|
||||
nowPlayingDetailsInfo,
|
||||
Gravity.START,
|
||||
R.attr.actionOverflowMenuStyle,
|
||||
0
|
||||
).apply {
|
||||
inflate(R.menu.track_info)
|
||||
lifecycleScope,
|
||||
command.tracks
|
||||
)
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
when (it.itemId) {
|
||||
R.id.track_info_artist -> BrowseFragmentDirections.browseToAlbums(
|
||||
track.artist,
|
||||
track.album?.cover()
|
||||
)
|
||||
R.id.track_info_album -> track.album?.let(BrowseFragmentDirections::browseToTracks)
|
||||
R.id.track_info_details -> TrackInfoDetailsFragment.new(track)
|
||||
.show(supportFragmentManager, "dialog")
|
||||
}
|
||||
|
||||
binding.nowPlaying.close()
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsFavorite?.let { now_playing_details_favorite ->
|
||||
favoritedRepository.fetch().untilNetwork(lifecycleScope, IO) { favorites, _, _, _ ->
|
||||
lifecycleScope.launch(Main) {
|
||||
track.favorite = favorites.contains(track.id)
|
||||
|
||||
when (track.favorite) {
|
||||
true -> now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
false -> now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
now_playing_details_favorite.setOnClickListener {
|
||||
when (track.favorite) {
|
||||
true -> {
|
||||
favoriteRepository.deleteFavorite(track.id)
|
||||
now_playing_details_favorite.setColorFilter(getColor(R.color.controlForeground))
|
||||
}
|
||||
|
||||
false -> {
|
||||
favoriteRepository.addFavorite(track.id)
|
||||
now_playing_details_favorite.setColorFilter(getColor(R.color.colorFavorite))
|
||||
}
|
||||
}
|
||||
|
||||
track.favorite = !track.favorite
|
||||
|
||||
favoriteRepository.fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsAddToPlaylist?.setOnClickListener {
|
||||
CommandBus.send(Command.AddToPlaylist(listOf(track)))
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun changeRepeatMode(index: Int) {
|
||||
when (index) {
|
||||
// From no repeat to repeat all
|
||||
0 -> {
|
||||
FFACache.set(this@MainActivity, "repeat", "0")
|
||||
private fun startService(command: Command) {
|
||||
val intent = Intent(this@MainActivity, PlayerService::class.java).apply {
|
||||
putExtra(PlayerService.INITIAL_COMMAND_KEY, command.toString())
|
||||
}
|
||||
ContextCompat.startForegroundService(this, intent)
|
||||
}
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
R.color.controlForeground
|
||||
)
|
||||
)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 0.2f
|
||||
|
||||
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_OFF))
|
||||
}
|
||||
|
||||
// From repeat all to repeat one
|
||||
1 -> {
|
||||
FFACache.set(this@MainActivity, "repeat", "1")
|
||||
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
R.color.controlForeground
|
||||
)
|
||||
)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
|
||||
|
||||
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ALL))
|
||||
}
|
||||
|
||||
// From repeat one to no repeat
|
||||
2 -> {
|
||||
FFACache.set(this@MainActivity, "repeat", "2")
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setImageResource(R.drawable.repeat_one)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.setColorFilter(
|
||||
ContextCompat.getColor(
|
||||
this,
|
||||
R.color.controlForeground
|
||||
)
|
||||
)
|
||||
binding.nowPlayingContainer?.nowPlayingDetailsRepeat?.alpha = 1.0f
|
||||
|
||||
CommandBus.send(Command.SetRepeatMode(Player.REPEAT_MODE_ONE))
|
||||
}
|
||||
private fun refreshTrack(track: Track?) {
|
||||
if (track != null) {
|
||||
binding.nowPlayingBottomSheet.show()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -623,7 +344,7 @@ class MainActivity : AppCompatActivity() {
|
|||
try {
|
||||
Fuel
|
||||
.post(mustNormalizeUrl("/api/v1/history/listenings/"))
|
||||
.authorize(this@MainActivity, oAuth)
|
||||
.authorize(applicationContext, oAuth)
|
||||
.header("Content-Type", "application/json")
|
||||
.body(Gson().toJson(mapOf("track" to track.id)))
|
||||
.awaitStringResponse()
|
||||
|
@ -633,4 +354,15 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun logout() {
|
||||
FFA.get().deleteAllData(this@MainActivity)
|
||||
startActivity(
|
||||
Intent(this@MainActivity, LoginActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_NO_HISTORY
|
||||
}
|
||||
)
|
||||
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,6 +114,14 @@ class SettingsFragment :
|
|||
}
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<ListPreference>("bandwidth_limitation")?.let {
|
||||
it.summary = when (it.value) {
|
||||
"unlimited" -> activity.getString(R.string.settings_bandwidth_limitation_summary_unlimited)
|
||||
"limited" -> activity.getString(R.string.settings_bandwidth_limitation_summary_limited)
|
||||
else -> activity.getString(R.string.settings_bandwidth_limitation_summary_unlimited)
|
||||
}
|
||||
}
|
||||
|
||||
preferenceManager.findPreference<ListPreference>("play_order")?.let {
|
||||
it.summary = when (it.value) {
|
||||
"shuffle" -> activity.getString(R.string.settings_play_order_shuffle_summary)
|
||||
|
|
|
@ -43,7 +43,7 @@ class AlbumsAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val album = data[position]
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, album.cover())
|
||||
CoverArt.requestCreator(album.cover())
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
|
|
|
@ -39,9 +39,8 @@ class AlbumsGridAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val album = data[position]
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(album.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(album.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(holder.cover)
|
||||
|
||||
|
|
|
@ -62,7 +62,7 @@ class ArtistsAdapter(
|
|||
val artist = active[position]
|
||||
|
||||
artist.cover()?.let { coverUrl ->
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(coverUrl))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(coverUrl))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.art)
|
||||
|
|
|
@ -9,29 +9,16 @@ import audio.funkwhale.ffa.fragments.FavoritesFragment
|
|||
import audio.funkwhale.ffa.fragments.PlaylistsFragment
|
||||
import audio.funkwhale.ffa.fragments.RadiosFragment
|
||||
|
||||
class BrowseTabsAdapter(val context: Fragment) :
|
||||
FragmentStateAdapter(context) {
|
||||
var tabs = mutableListOf<Fragment>()
|
||||
|
||||
class BrowseTabsAdapter(val context: Fragment) : FragmentStateAdapter(context) {
|
||||
override fun getItemCount() = 5
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
tabs.getOrNull(position)?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val fragment = when (position) {
|
||||
0 -> ArtistsFragment()
|
||||
1 -> AlbumsGridFragment()
|
||||
2 -> PlaylistsFragment()
|
||||
3 -> RadiosFragment()
|
||||
4 -> FavoritesFragment()
|
||||
else -> ArtistsFragment()
|
||||
}
|
||||
|
||||
tabs.add(position, fragment)
|
||||
|
||||
return fragment
|
||||
override fun createFragment(position: Int): Fragment = when (position) {
|
||||
0 -> ArtistsFragment()
|
||||
1 -> AlbumsGridFragment()
|
||||
2 -> PlaylistsFragment()
|
||||
3 -> RadiosFragment()
|
||||
4 -> FavoritesFragment()
|
||||
else -> ArtistsFragment()
|
||||
}
|
||||
|
||||
fun tabText(position: Int): String {
|
||||
|
|
|
@ -69,9 +69,8 @@ class FavoritesAdapter(
|
|||
val favorite = data[position]
|
||||
val track = favorite.track
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(track.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(track.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(holder.cover)
|
||||
|
||||
|
|
|
@ -71,9 +71,8 @@ class PlaylistTracksAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val playlistTrack = data[position]
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(playlistTrack.track.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(playlistTrack.track.cover()))
|
||||
.fit()
|
||||
.placeholder(R.drawable.cover)
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(holder.cover)
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ class PlaylistsAdapter(
|
|||
else -> RoundedCornersTransformation.CornerType.TOP_LEFT
|
||||
}
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, url)
|
||||
CoverArt.requestCreator(url)
|
||||
.transform(RoundedCornersTransformation(32, 0, corner))
|
||||
.into(imageView)
|
||||
}
|
||||
|
|
|
@ -189,7 +189,7 @@ class SearchAdapter(
|
|||
else -> tracks[position]
|
||||
}
|
||||
|
||||
CoverArt.withContext(fragment.layoutInflater.context, maybeNormalizeUrl(item.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(item.cover()))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(16, 0))
|
||||
.into(rowTrackViewHolder?.cover)
|
||||
|
|
|
@ -70,7 +70,7 @@ class TracksAdapter(
|
|||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val track = data[position]
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(track.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(track.cover()))
|
||||
.fit()
|
||||
.transform(RoundedCornersTransformation(8, 0))
|
||||
.into(holder.cover)
|
||||
|
|
|
@ -79,7 +79,7 @@ class AlbumsFragment : FFAFragment<Album, AlbumsAdapter>() {
|
|||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
binding.cover.let { cover ->
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(artistArt))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(artistArt))
|
||||
.noFade()
|
||||
.fit()
|
||||
.centerCrop()
|
||||
|
|
|
@ -0,0 +1,244 @@
|
|||
package audio.funkwhale.ffa.fragments
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.navigation.fragment.findNavController
|
||||
import audio.funkwhale.ffa.MainNavDirections
|
||||
import audio.funkwhale.ffa.R
|
||||
import audio.funkwhale.ffa.databinding.FragmentNowPlayingBinding
|
||||
import audio.funkwhale.ffa.model.Track
|
||||
import audio.funkwhale.ffa.repositories.FavoritedRepository
|
||||
import audio.funkwhale.ffa.repositories.FavoritesRepository
|
||||
import audio.funkwhale.ffa.repositories.Repository
|
||||
import audio.funkwhale.ffa.utils.Command
|
||||
import audio.funkwhale.ffa.utils.CommandBus
|
||||
import audio.funkwhale.ffa.utils.CoverArt
|
||||
import audio.funkwhale.ffa.utils.Event
|
||||
import audio.funkwhale.ffa.utils.EventBus
|
||||
import audio.funkwhale.ffa.utils.FFACache
|
||||
import audio.funkwhale.ffa.utils.ProgressBus
|
||||
import audio.funkwhale.ffa.utils.maybeNormalizeUrl
|
||||
import audio.funkwhale.ffa.utils.toIntOrElse
|
||||
import audio.funkwhale.ffa.utils.untilNetwork
|
||||
import audio.funkwhale.ffa.viewmodel.NowPlayingViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Float.max
|
||||
|
||||
class NowPlayingFragment: Fragment(R.layout.fragment_now_playing) {
|
||||
private val binding by lazy { FragmentNowPlayingBinding.bind(requireView()) }
|
||||
private val viewModel by viewModels<NowPlayingViewModel>()
|
||||
private val favoriteRepository by lazy { FavoritesRepository(requireContext()) }
|
||||
private val favoritedRepository by lazy { FavoritedRepository(requireContext()) }
|
||||
|
||||
private var onDetailsMenuItemClickedCb: () -> Unit = {}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel.currentTrack.distinctUntilChanged().observe(viewLifecycleOwner, ::onTrackChange)
|
||||
|
||||
with(binding.controls) {
|
||||
currentTrackTitle = viewModel.currentTrackTitle
|
||||
currentTrackArtist = viewModel.currentTrackArtist
|
||||
isCurrentTrackFavorite = viewModel.isCurrentTrackFavorite
|
||||
repeatModeResource = viewModel.repeatModeResource
|
||||
repeatModeAlpha = viewModel.repeatModeAlpha
|
||||
currentProgressText = viewModel.currentProgressText
|
||||
currentDurationText = viewModel.currentDurationText
|
||||
isPlaying = viewModel.isPlaying
|
||||
progress = viewModel.progress
|
||||
|
||||
nowPlayingDetailsPrevious.setOnClickListener {
|
||||
CommandBus.send(Command.PreviousTrack)
|
||||
}
|
||||
|
||||
nowPlayingDetailsNext.setOnClickListener {
|
||||
CommandBus.send(Command.NextTrack)
|
||||
}
|
||||
|
||||
nowPlayingDetailsToggle.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
}
|
||||
|
||||
nowPlayingDetailsRepeat.setOnClickListener { toggleRepeatMode() }
|
||||
nowPlayingDetailsProgress.setOnSeekBarChangeListener(OnSeekBarChanged())
|
||||
nowPlayingDetailsFavorite.setOnClickListener { onFavorite() }
|
||||
nowPlayingDetailsAddToPlaylist.setOnClickListener { onAddToPlaylist() }
|
||||
}
|
||||
|
||||
binding.nowPlayingDetailsInfo.setOnClickListener { openInfoMenu() }
|
||||
|
||||
with(binding.header) {
|
||||
lifecycleOwner = viewLifecycleOwner
|
||||
isBuffering = viewModel.isBuffering
|
||||
isPlaying = viewModel.isPlaying
|
||||
progress = viewModel.progress
|
||||
currentTrackTitle = viewModel.currentTrackTitle
|
||||
currentTrackArtist = viewModel.currentTrackArtist
|
||||
|
||||
|
||||
nowPlayingNext.setOnClickListener {
|
||||
CommandBus.send(Command.NextTrack)
|
||||
}
|
||||
|
||||
nowPlayingToggle.setOnClickListener {
|
||||
CommandBus.send(Command.ToggleState)
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
CommandBus.get().collect { onCommand(it) }
|
||||
}
|
||||
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
ProgressBus.get().collect { onProgress(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun onBottomSheetDrag(value: Float) {
|
||||
binding.nowPlayingRoot.progress = max(value, 0f)
|
||||
}
|
||||
|
||||
fun onDetailsMenuItemClicked(cb: () -> Unit) {
|
||||
onDetailsMenuItemClickedCb = cb
|
||||
}
|
||||
|
||||
|
||||
private fun toggleRepeatMode() {
|
||||
val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0)
|
||||
val iteratedRepeatMode = (cachedRepeatMode + 1) % 3
|
||||
FFACache.set(requireContext(), "repeat", "$iteratedRepeatMode")
|
||||
CommandBus.send(Command.SetRepeatMode(iteratedRepeatMode))
|
||||
}
|
||||
|
||||
private fun onAddToPlaylist() {
|
||||
val currentTrack = viewModel.currentTrack.value ?: return
|
||||
CommandBus.send(Command.AddToPlaylist(listOf(currentTrack)))
|
||||
}
|
||||
|
||||
private fun onCommand(command: Command) = when (command) {
|
||||
is Command.RefreshTrack -> refreshCurrentTrack(command.track)
|
||||
is Command.SetRepeatMode -> viewModel.repeatMode.postValue(command.mode)
|
||||
else -> {}
|
||||
}
|
||||
|
||||
private fun onFavorite() {
|
||||
val currentTrack = viewModel.currentTrack.value ?: return
|
||||
|
||||
if (currentTrack.favorite) favoriteRepository.deleteFavorite(currentTrack.id)
|
||||
else favoriteRepository.addFavorite(currentTrack.id)
|
||||
|
||||
currentTrack.favorite = !currentTrack.favorite
|
||||
// Trigger UI refresh
|
||||
viewModel.currentTrack.postValue(viewModel.currentTrack.value)
|
||||
|
||||
favoritedRepository.fetch(Repository.Origin.Network.origin)
|
||||
}
|
||||
|
||||
private fun onProgress(state: Triple<Int, Int, Int>) {
|
||||
val (current, duration, percent) = state
|
||||
|
||||
val currentMins = (current / 1000) / 60
|
||||
val currentSecs = (current / 1000) % 60
|
||||
|
||||
val durationMins = duration / 60
|
||||
val durationSecs = duration % 60
|
||||
|
||||
viewModel.progress.postValue(percent)
|
||||
viewModel.currentProgressText.postValue("%02d:%02d".format(currentMins, currentSecs))
|
||||
viewModel.currentDurationText.postValue("%02d:%02d".format(durationMins, durationSecs))
|
||||
}
|
||||
|
||||
private fun onTrackChange(track: Track?) {
|
||||
if (track == null) {
|
||||
binding.header.nowPlayingCover.setImageResource(R.drawable.cover)
|
||||
return
|
||||
}
|
||||
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(track.album?.cover()))
|
||||
.into(binding.header.nowPlayingCover)
|
||||
}
|
||||
|
||||
private fun openInfoMenu() {
|
||||
val currentTrack = viewModel.currentTrack.value ?: return
|
||||
|
||||
PopupMenu(
|
||||
requireContext(),
|
||||
binding.nowPlayingDetailsInfo,
|
||||
Gravity.START,
|
||||
R.attr.actionOverflowMenuStyle,
|
||||
0
|
||||
).apply {
|
||||
inflate(R.menu.track_info)
|
||||
|
||||
setOnMenuItemClickListener {
|
||||
onDetailsMenuItemClickedCb()
|
||||
|
||||
when (it.itemId) {
|
||||
R.id.track_info_artist -> findNavController().navigate(
|
||||
MainNavDirections.globalBrowseToAlbums(
|
||||
currentTrack.artist,
|
||||
currentTrack.album?.cover()
|
||||
)
|
||||
)
|
||||
R.id.track_info_album -> currentTrack.album?.let { album ->
|
||||
findNavController().navigate(MainNavDirections.globalBrowseTracks(album))
|
||||
}
|
||||
R.id.track_info_details -> TrackInfoDetailsFragment.new(currentTrack).show(
|
||||
requireActivity().supportFragmentManager, "dialog"
|
||||
)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshCurrentTrack(track: Track?) {
|
||||
viewModel.currentTrack.postValue(track)
|
||||
|
||||
val cachedRepeatMode = FFACache.getLine(requireContext(), "repeat").toIntOrElse(0)
|
||||
viewModel.repeatMode.postValue(cachedRepeatMode % 3)
|
||||
|
||||
// At this point, a non-null track is required
|
||||
|
||||
if (track == null) return
|
||||
|
||||
favoritedRepository.fetch().untilNetwork(lifecycleScope, Dispatchers.IO) { favorites, _, _, _ ->
|
||||
lifecycleScope.launch(Dispatchers.Main) {
|
||||
track.favorite = favorites.contains(track.id)
|
||||
// Trigger UI refresh
|
||||
viewModel.currentTrack.postValue(viewModel.currentTrack.value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
inner class OnSeekBarChanged : OnSeekBarChangeListener {
|
||||
override fun onStopTrackingTouch(view: SeekBar?) {}
|
||||
|
||||
override fun onStartTrackingTouch(view: SeekBar?) {}
|
||||
|
||||
override fun onProgressChanged(view: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
CommandBus.send(Command.Seek(progress))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -167,7 +167,7 @@ class PlaylistTracksFragment : FFAFragment<PlaylistTrack, PlaylistTracksAdapter>
|
|||
}
|
||||
|
||||
lifecycleScope.launch(Main) {
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(url))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(url))
|
||||
.fit()
|
||||
.centerCrop()
|
||||
.transform(RoundedCornersTransformation(16, 0, corner))
|
||||
|
|
|
@ -118,7 +118,7 @@ class TracksFragment : FFAFragment<Track, TracksAdapter>() {
|
|||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
|
||||
CoverArt.withContext(layoutInflater.context, maybeNormalizeUrl(args.album.cover()))
|
||||
CoverArt.requestCreator(maybeNormalizeUrl(args.album.cover()))
|
||||
.noFade()
|
||||
.fit()
|
||||
.centerCrop()
|
||||
|
|
|
@ -67,11 +67,20 @@ data class Track(
|
|||
fun bestUpload(): Upload? {
|
||||
if (uploads.isEmpty()) return null
|
||||
|
||||
return when (PowerPreference.getDefaultFile().getString("media_cache_quality")) {
|
||||
var bestUpload = when (PowerPreference.getDefaultFile().getString("media_cache_quality")) {
|
||||
"quality" -> uploads.maxByOrNull { it.bitrate } ?: uploads[0]
|
||||
"size" -> uploads.minByOrNull { it.bitrate } ?: uploads[0]
|
||||
else -> uploads.maxByOrNull { it.bitrate } ?: uploads[0]
|
||||
}
|
||||
|
||||
return when (PowerPreference.getDefaultFile().getString("bandwidth_limitation")) {
|
||||
"unlimited" -> bestUpload
|
||||
"limited" -> {
|
||||
var listenUrl = bestUpload.listen_url
|
||||
Upload(listenUrl.plus("&to=mp3&max_bitrate=320"), uploads[0].duration, 320_000)
|
||||
}
|
||||
else -> bestUpload
|
||||
}
|
||||
}
|
||||
|
||||
override fun cover(): String? {
|
||||
|
|
|
@ -68,7 +68,7 @@ class MediaControlsManager(
|
|||
.run {
|
||||
coverUrl?.let {
|
||||
try {
|
||||
setLargeIcon(CoverArt.withContext(context, coverUrl).get())
|
||||
setLargeIcon(CoverArt.requestCreator(coverUrl).get())
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import android.media.MediaMetadata
|
|||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.support.v4.media.MediaMetadataCompat
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.media.session.MediaButtonReceiver
|
||||
|
@ -384,7 +385,7 @@ class PlayerService : Service() {
|
|||
runBlocking(IO) {
|
||||
this@apply.putBitmap(
|
||||
MediaMetadataCompat.METADATA_KEY_ALBUM_ART,
|
||||
CoverArt.withContext(this@PlayerService.applicationContext, coverUrl).get()
|
||||
CoverArt.requestCreator(coverUrl).get()
|
||||
)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
|
@ -468,8 +469,11 @@ class PlayerService : Service() {
|
|||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
EventBus.send(Event.Buffering(playbackState == Player.STATE_BUFFERING))
|
||||
|
||||
when (playbackState) {
|
||||
Player.STATE_BUFFERING -> {
|
||||
EventBus.send(Event.Buffering(true))
|
||||
}
|
||||
Player.STATE_ENDED -> {
|
||||
setPlaybackState(false)
|
||||
|
||||
|
@ -488,6 +492,10 @@ class PlayerService : Service() {
|
|||
mediaControlsManager.remove()
|
||||
}
|
||||
}
|
||||
|
||||
Player.STATE_READY -> {
|
||||
EventBus.send(Event.Buffering(false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package audio.funkwhale.ffa.utils
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.BroadcastReceiver
|
||||
|
@ -23,7 +22,7 @@ object AppContext {
|
|||
const val PAGE_SIZE = 50
|
||||
const val TRANSITION_DURATION = 300L
|
||||
|
||||
fun init(context: Activity) {
|
||||
fun init(context: Context) {
|
||||
setupNotificationChannels(context)
|
||||
|
||||
// CastContext.getSharedInstance(context)
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
package audio.funkwhale.ffa.utils
|
||||
|
||||
import androidx.customview.widget.Openable
|
||||
|
||||
interface BottomSheetIneractable: Openable {
|
||||
val isHidden: Boolean
|
||||
fun show()
|
||||
fun hide()
|
||||
fun toggle()
|
||||
}
|
|
@ -98,7 +98,8 @@ object CommandBus {
|
|||
}
|
||||
|
||||
object RequestBus {
|
||||
private var _requests = MutableSharedFlow<Request>()
|
||||
// `replay` allows send requests before the PlayerService starts listening
|
||||
private var _requests = MutableSharedFlow<Request>(replay = 100)
|
||||
var requests = _requests.asSharedFlow()
|
||||
fun send(request: Request): Channel<Response> {
|
||||
return Channel<Response>().also {
|
||||
|
|
|
@ -2,8 +2,11 @@ package audio.funkwhale.ffa.utils
|
|||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.transition.CircularPropagation
|
||||
import android.util.Log
|
||||
import androidx.swiperefreshlayout.widget.CircularProgressDrawable
|
||||
import audio.funkwhale.ffa.BuildConfig
|
||||
import audio.funkwhale.ffa.FFA
|
||||
import audio.funkwhale.ffa.R
|
||||
import com.squareup.picasso.Downloader
|
||||
import com.squareup.picasso.NetworkPolicy
|
||||
|
@ -71,6 +74,19 @@ open class CoverArt private constructor() {
|
|||
// Cache with some useful concurrency semantics. See its docs for details.
|
||||
val fileCache = Bottleneck<File>()
|
||||
|
||||
private val picasso = with (FFA.get()) {
|
||||
Picasso.Builder(this)
|
||||
.addRequestHandler(CoverNetworkRequestHandler(this))
|
||||
// Be careful with this. There's at least one place in Picasso where it
|
||||
// doesn't null-check when logging, so it'll throw errors in places you
|
||||
// wouldn't get them with logging turned off. /sigh
|
||||
.loggingEnabled(false) // (BuildConfig.DEBUG)
|
||||
// Occasionally, we may get transient HTTP issues, or bogus files.
|
||||
// Listen for Picasso errors and invalidate those files
|
||||
.listener(invalidateIn(this))
|
||||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* We don't need to hang onto the Context, just the Path it gets us.
|
||||
*/
|
||||
|
@ -202,17 +218,6 @@ open class CoverArt private constructor() {
|
|||
/**
|
||||
* Low-level Picasso wiring.
|
||||
*/
|
||||
private fun buildPicasso(context: Context) = Picasso.Builder(context)
|
||||
// The bulk of the work happens here
|
||||
.addRequestHandler(CoverNetworkRequestHandler(context))
|
||||
// Be careful with this. There's at least one place in Picasso where it
|
||||
// doesn't null-check when logging, so it'll throw errors in places you
|
||||
// wouldn't get them with logging turned off. /sigh
|
||||
.loggingEnabled(false) // (BuildConfig.DEBUG)
|
||||
// Occasionally, we may get transient HTTP issues, or bogus files.
|
||||
// Listen for Picasso errors and invalidate those files
|
||||
.listener(invalidateIn(context))
|
||||
.build()
|
||||
|
||||
/**
|
||||
* We don't want to cache the HTTP part of the flow, because:
|
||||
|
@ -251,10 +256,11 @@ open class CoverArt private constructor() {
|
|||
/**
|
||||
* The primary entrypoint for the codebase.
|
||||
*/
|
||||
fun withContext(context: Context, url: String?): RequestCreator {
|
||||
return buildPicasso(context)
|
||||
.load(url)
|
||||
.placeholder(R.drawable.cover)
|
||||
fun requestCreator(url: String?): RequestCreator {
|
||||
val request = picasso.load(url)
|
||||
if(url == null) request.placeholder(R.drawable.cover)
|
||||
else request.placeholder(CircularProgressDrawable(FFA.get()))
|
||||
return request.error(R.drawable.cover)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -53,9 +53,6 @@ fun Request.authorize(context: Context, oAuth: OAuth): Request {
|
|||
this@authorize.apply {
|
||||
if (!Settings.isAnonymous()) {
|
||||
oAuth.state().let { state ->
|
||||
state.accessTokenExpirationTime?.let {
|
||||
Log.i("Request.authorize()", "Accesstoken expiration: ${Date(it).format()}")
|
||||
}
|
||||
val old = state.accessToken
|
||||
val auth = ClientSecretPost(oAuth.state().clientSecret)
|
||||
val done = CompletableDeferred<Boolean>()
|
||||
|
@ -64,10 +61,10 @@ fun Request.authorize(context: Context, oAuth: OAuth): Request {
|
|||
state.performActionWithFreshTokens(tokenService, auth) { token, _, e ->
|
||||
if (e != null) {
|
||||
Log.e("Request.authorize()", "performActionWithFreshToken failed: $e")
|
||||
Log.e("Request.authorize()", Log.getStackTraceString(e))
|
||||
}
|
||||
if (token == old) {
|
||||
Log.i("Request.authorize()", "Accesstoken not renewed")
|
||||
if (e.type != 2 || e.code != 2002) {
|
||||
Log.e("Request.authorize()", Log.getStackTraceString(e))
|
||||
EventBus.send(Event.LogOut)
|
||||
}
|
||||
}
|
||||
if (token != old && token != null) {
|
||||
state.save()
|
||||
|
@ -149,3 +146,5 @@ inline fun <T, U, V, W, R> LiveData<T>.mergeWith(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public fun String?.toIntOrElse(default: Int): Int = this?.toIntOrNull(radix = 10) ?: default
|
||||
|
|
|
@ -83,7 +83,7 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
|
|||
refreshAccessToken(state, context)
|
||||
} else {
|
||||
state.isAuthorized
|
||||
}.also { it.logInfo("tryRefreshAccessToken()") }
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -103,6 +103,7 @@ class OAuth(private val authorizationServiceFactory: AuthorizationServiceFactory
|
|||
if (e != null) {
|
||||
Log.e("OAuth", "performTokenRequest failed: $e")
|
||||
Log.e("OAuth", Log.getStackTraceString(e))
|
||||
EventBus.send(Event.LogOut)
|
||||
} else {
|
||||
state.apply {
|
||||
Log.i("OAuth", "applying new authState")
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
package audio.funkwhale.ffa.utils
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.Log
|
||||
import android.widget.ImageButton
|
||||
import androidx.annotation.ColorRes
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
import androidx.databinding.BindingAdapter
|
||||
|
||||
|
||||
@BindingAdapter("srcCompat")
|
||||
fun setImageViewResource(imageView: AppCompatImageView, resource: Any?) = when (resource) {
|
||||
is Bitmap -> imageView.setImageBitmap(resource)
|
||||
is Int -> imageView.setImageResource(resource)
|
||||
is Drawable -> imageView.setImageDrawable(resource)
|
||||
else -> imageView.setImageDrawable(ColorDrawable(Color.TRANSPARENT))
|
||||
}
|
||||
|
||||
@BindingAdapter("tint")
|
||||
fun setTint(imageView: ImageButton, @ColorRes resource: Int) = resource.let {
|
||||
imageView.setColorFilter(resource)
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
package audio.funkwhale.ffa.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.asLiveData
|
||||
import androidx.lifecycle.distinctUntilChanged
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.map
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import audio.funkwhale.ffa.FFA
|
||||
import audio.funkwhale.ffa.R
|
||||
import audio.funkwhale.ffa.model.Track
|
||||
import audio.funkwhale.ffa.utils.Event
|
||||
import audio.funkwhale.ffa.utils.EventBus
|
||||
import com.google.android.exoplayer2.Player
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.flow.emitAll
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class NowPlayingViewModel(app: Application) : AndroidViewModel(app) {
|
||||
val isBuffering = EventBus.get()
|
||||
.filter { it is Event.Buffering }
|
||||
.map { (it as Event.Buffering).value }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
.distinctUntilChanged()
|
||||
|
||||
val isPlaying = EventBus.get()
|
||||
.filter { it is Event.StateChanged }
|
||||
.map { (it as Event.StateChanged).playing }
|
||||
.stateIn(viewModelScope, SharingStarted.Lazily, false)
|
||||
.asLiveData(viewModelScope.coroutineContext)
|
||||
.distinctUntilChanged()
|
||||
|
||||
val repeatMode = MutableLiveData(0)
|
||||
val progress = MutableLiveData(0)
|
||||
val currentTrack = MutableLiveData<Track?>(null)
|
||||
val currentProgressText = MutableLiveData("")
|
||||
val currentDurationText = MutableLiveData("")
|
||||
|
||||
// Calling distinctUntilChanged() prevents triggering an event when the track hasn't changed
|
||||
val currentTrackTitle = currentTrack.distinctUntilChanged().map { it?.title ?: "" }
|
||||
val currentTrackArtist = currentTrack.distinctUntilChanged().map { it?.artist?.name ?: "" }
|
||||
|
||||
// Not calling distinctUntilChanged() here as we need to process every event
|
||||
val isCurrentTrackFavorite = currentTrack.map {
|
||||
it?.favorite ?: false
|
||||
}
|
||||
|
||||
val repeatModeResource = repeatMode.distinctUntilChanged().map {
|
||||
when (it) {
|
||||
Player.REPEAT_MODE_ONE -> AppCompatResources.getDrawable(context, R.drawable.repeat_one)
|
||||
else -> AppCompatResources.getDrawable(context, R.drawable.repeat)
|
||||
}
|
||||
}
|
||||
|
||||
val repeatModeAlpha = repeatMode.distinctUntilChanged().map {
|
||||
when (it) {
|
||||
Player.REPEAT_MODE_OFF -> 0.2f
|
||||
else -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
private val context: Context
|
||||
get() = getApplication<FFA>().applicationContext
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package audio.funkwhale.ffa.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.FrameLayout
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
import androidx.core.content.res.use
|
||||
import audio.funkwhale.ffa.R
|
||||
import audio.funkwhale.ffa.utils.BottomSheetIneractable
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
|
||||
|
||||
class NowPlayingBottomSheet @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
|
||||
) : CardView(context, attrs, defStyleAttr), BottomSheetIneractable {
|
||||
private val behavior = BottomSheetBehavior<NowPlayingBottomSheet>()
|
||||
private val targetHeaderId: Int
|
||||
|
||||
val peekHeight get() = behavior.peekHeight
|
||||
|
||||
init {
|
||||
targetHeaderId = context.theme.obtainStyledAttributes(
|
||||
attrs, R.styleable.NowPlaying, defStyleAttr, 0
|
||||
).use {
|
||||
it.getResourceId(R.styleable.NowPlaying_target_header, NO_ID)
|
||||
}
|
||||
|
||||
// Put default peek height to actionBarSize so it is not 0
|
||||
val tv = TypedValue()
|
||||
if (context.theme.resolveAttribute(android.R.attr.actionBarSize, tv, true)) {
|
||||
behavior.peekHeight = TypedValue.complexToDimensionPixelSize(
|
||||
tv.data, resources.displayMetrics
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setLayoutParams(params: ViewGroup.LayoutParams?) {
|
||||
super.setLayoutParams(params)
|
||||
(params as CoordinatorLayout.LayoutParams).behavior = behavior
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
findViewById<View>(targetHeaderId)?.apply {
|
||||
behavior.setPeekHeight(this.height, false)
|
||||
this.setOnClickListener { this@NowPlayingBottomSheet.toggle() }
|
||||
} ?: hide()
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean = true
|
||||
|
||||
fun addBottomSheetCallback(callback: BottomSheetCallback) {
|
||||
behavior.addBottomSheetCallback(callback)
|
||||
}
|
||||
|
||||
// Bottom sheet interactions
|
||||
override val isHidden: Boolean get() = behavior.state == BottomSheetBehavior.STATE_HIDDEN
|
||||
|
||||
override fun isOpen(): Boolean = behavior.state == BottomSheetBehavior.STATE_EXPANDED
|
||||
|
||||
override fun open() {
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
behavior.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
|
||||
override fun show() {
|
||||
behavior.isHideable = false
|
||||
if (behavior.state == BottomSheetBehavior.STATE_HIDDEN) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
override fun hide() {
|
||||
behavior.isHideable = true
|
||||
behavior.state = BottomSheetBehavior.STATE_HIDDEN
|
||||
}
|
||||
|
||||
override fun toggle() {
|
||||
if (isHidden) return
|
||||
if (isOpen) close() else open()
|
||||
}
|
||||
}
|
|
@ -1,255 +0,0 @@
|
|||
package audio.funkwhale.ffa.views
|
||||
|
||||
import android.animation.ValueAnimator
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.util.TypedValue
|
||||
import android.view.GestureDetector
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import audio.funkwhale.ffa.R
|
||||
import audio.funkwhale.ffa.databinding.PartialNowPlayingBinding
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.min
|
||||
|
||||
class NowPlayingView : MaterialCardView {
|
||||
val activity: Context
|
||||
var gestureDetector: GestureDetector? = null
|
||||
var gestureDetectorCallback: OnGestureDetection? = null
|
||||
|
||||
private val binding =
|
||||
PartialNowPlayingBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
constructor(context: Context) : super(context) {
|
||||
activity = context
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
|
||||
activity = context
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?, style: Int) : super(context, attrs, style) {
|
||||
activity = context
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
binding.nowPlayingRoot.measure(
|
||||
widthMeasureSpec,
|
||||
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onVisibilityChanged(changedView: View, visibility: Int) {
|
||||
super.onVisibilityChanged(changedView, visibility)
|
||||
|
||||
if (visibility == View.VISIBLE && gestureDetector == null) {
|
||||
viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
gestureDetectorCallback = OnGestureDetection()
|
||||
gestureDetector = GestureDetector(context, gestureDetectorCallback!!)
|
||||
|
||||
setOnTouchListener { _, motionEvent ->
|
||||
val ret = gestureDetector?.onTouchEvent(motionEvent) ?: false
|
||||
|
||||
if (motionEvent.actionMasked == MotionEvent.ACTION_UP) {
|
||||
if (gestureDetectorCallback?.isScrolling == true) {
|
||||
gestureDetectorCallback?.onUp()
|
||||
}
|
||||
}
|
||||
performClick()
|
||||
ret
|
||||
}
|
||||
|
||||
viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fun isOpened(): Boolean = gestureDetectorCallback?.isOpened() ?: false
|
||||
|
||||
fun close() {
|
||||
gestureDetectorCallback?.close()
|
||||
}
|
||||
|
||||
inner class OnGestureDetection : GestureDetector.SimpleOnGestureListener() {
|
||||
private var maxHeight = 0
|
||||
private var minHeight = 0
|
||||
private var maxMargin = 0
|
||||
|
||||
private var initialTouchY = 0f
|
||||
private var lastTouchY = 0f
|
||||
|
||||
var isScrolling = false
|
||||
private var flingAnimator: ValueAnimator? = null
|
||||
|
||||
init {
|
||||
(layoutParams as? MarginLayoutParams)?.let {
|
||||
maxMargin = it.marginStart
|
||||
}
|
||||
|
||||
minHeight = TypedValue().let {
|
||||
activity.theme.resolveAttribute(R.attr.actionBarSize, it, true)
|
||||
|
||||
TypedValue.complexToDimensionPixelSize(it.data, resources.displayMetrics)
|
||||
}
|
||||
|
||||
maxHeight = binding.nowPlayingDetails.measuredHeight + (2 * maxMargin)
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent): Boolean {
|
||||
initialTouchY = e.rawY
|
||||
lastTouchY = e.rawY
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun onUp(): Boolean {
|
||||
isScrolling = false
|
||||
|
||||
layoutParams.let {
|
||||
val offsetToMax = maxHeight - height
|
||||
val offsetToMin = height - minHeight
|
||||
|
||||
flingAnimator =
|
||||
if (offsetToMin < offsetToMax) ValueAnimator.ofInt(it.height, minHeight)
|
||||
else ValueAnimator.ofInt(it.height, maxHeight)
|
||||
|
||||
animateFling(500)
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
firstMotionEvent: MotionEvent,
|
||||
secondMotionEvent: MotionEvent,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
isScrolling = false
|
||||
|
||||
layoutParams.let {
|
||||
val diff =
|
||||
if (velocityY < 0) maxHeight - it.height
|
||||
else it.height - minHeight
|
||||
|
||||
flingAnimator =
|
||||
if (velocityY < 0) ValueAnimator.ofInt(it.height, maxHeight)
|
||||
else ValueAnimator.ofInt(it.height, minHeight)
|
||||
|
||||
animateFling(min(abs((diff.toFloat() / velocityY * 1000).toLong()), 600))
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
firstMotionEvent: MotionEvent,
|
||||
secondMotionEvent: MotionEvent,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
isScrolling = true
|
||||
|
||||
layoutParams.let {
|
||||
val newHeight = it.height + lastTouchY - secondMotionEvent.rawY
|
||||
val progress = (newHeight - minHeight) / (maxHeight - minHeight)
|
||||
val newMargin = maxMargin - (maxMargin * progress)
|
||||
|
||||
(layoutParams as? MarginLayoutParams)?.let { params ->
|
||||
params.marginStart = newMargin.toInt()
|
||||
params.marginEnd = newMargin.toInt()
|
||||
params.bottomMargin = newMargin.toInt()
|
||||
}
|
||||
|
||||
layoutParams = layoutParams.apply {
|
||||
when {
|
||||
newHeight <= minHeight -> {
|
||||
height = minHeight
|
||||
return true
|
||||
}
|
||||
newHeight >= maxHeight -> {
|
||||
height = maxHeight
|
||||
return true
|
||||
}
|
||||
else -> height = newHeight.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
binding.summary.alpha = 1f - progress
|
||||
|
||||
binding.summary.layoutParams = binding.summary.layoutParams.apply {
|
||||
height = (minHeight * (1f - progress)).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
lastTouchY = secondMotionEvent.rawY
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent): Boolean {
|
||||
layoutParams.let {
|
||||
if (height != minHeight) return true
|
||||
|
||||
flingAnimator = ValueAnimator.ofInt(it.height, maxHeight)
|
||||
|
||||
animateFling(300)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun isOpened(): Boolean = layoutParams.height == maxHeight
|
||||
|
||||
fun close(): Boolean {
|
||||
layoutParams.let {
|
||||
if (it.height == minHeight) return true
|
||||
|
||||
flingAnimator = ValueAnimator.ofInt(it.height, minHeight)
|
||||
|
||||
animateFling(300)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private fun animateFling(dur: Long) {
|
||||
flingAnimator?.apply {
|
||||
duration = dur
|
||||
interpolator = DecelerateInterpolator()
|
||||
|
||||
addUpdateListener { valueAnimator ->
|
||||
layoutParams = layoutParams.apply {
|
||||
val newHeight = valueAnimator.animatedValue as Int
|
||||
val progress = (newHeight.toFloat() - minHeight) / (maxHeight - minHeight)
|
||||
val newMargin = maxMargin - (maxMargin * progress)
|
||||
|
||||
(layoutParams as? MarginLayoutParams)?.let {
|
||||
it.marginStart = newMargin.toInt()
|
||||
it.marginEnd = newMargin.toInt()
|
||||
it.bottomMargin = newMargin.toInt()
|
||||
}
|
||||
|
||||
height = newHeight
|
||||
|
||||
binding.summary.alpha = 1f - progress
|
||||
|
||||
binding.summary.layoutParams = binding.summary.layoutParams.apply {
|
||||
height = (minHeight * (1f - progress)).toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
package audio.funkwhale.ffa.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
class SquareImageView : AppCompatImageView {
|
||||
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 onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
setMeasuredDimension(measuredWidth, measuredWidth)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package audio.funkwhale.ffa.views
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.AppCompatImageButton
|
||||
import androidx.appcompat.widget.AppCompatImageView
|
||||
|
||||
open class SquareView : View {
|
||||
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 onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val dimension = if(measuredWidth == 0 && measuredHeight > 0) measuredHeight else measuredWidth
|
||||
|
||||
setMeasuredDimension(dimension, dimension)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open class SquareImageView : AppCompatImageView {
|
||||
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 onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
|
||||
val dimension = if(measuredWidth == 0 && measuredHeight > 0) measuredHeight else measuredWidth
|
||||
|
||||
setMeasuredDimension(dimension, dimension)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
<vector android:height="24dp" android:viewportHeight="48"
|
||||
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="m4.05,44 l40,-40v40ZM34.3,41h6.75L41.05,11.2l-6.75,6.75Z"/>
|
||||
</vector>
|
|
@ -0,0 +1,6 @@
|
|||
<vector android:height="24dp" android:viewportHeight="48"
|
||||
android:viewportWidth="48" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M44.051,4L4.051,44L17.098,44L17.098,32.91L34.301,32.91L34.301,17.949L41.051,11.199L41.051,32.91L44.051,32.91L44.051,4z"/>
|
||||
<path android:fillColor="#FF000000"
|
||||
android:pathData="M17.873,33.639L17.873,47.316L47.16,47.316L47.16,33.639L17.873,33.639zM20.283,35.08L21.391,35.08L21.391,38.76C21.897,37.995 22.615,37.613 23.549,37.613C24.473,37.613 25.203,37.942 25.736,38.6C26.27,39.257 26.537,40.15 26.537,41.279C26.537,42.435 26.26,43.364 25.709,44.066C25.158,44.76 24.421,45.105 23.496,45.105C22.545,45.105 21.808,44.706 21.283,43.906L21.283,44.799L20.283,44.799L20.283,35.08zM35.256,35.893L36.363,35.893L36.363,37.813L37.51,37.813L37.51,38.719L36.363,38.719L36.363,43.506C36.363,43.755 36.402,43.923 36.482,44.012C36.571,44.092 36.737,44.133 36.977,44.133C37.199,44.133 37.376,44.116 37.51,44.08L37.51,45.012C37.163,45.074 36.861,45.105 36.604,45.105C36.168,45.105 35.835,45.008 35.604,44.813C35.372,44.626 35.256,44.356 35.256,44L35.256,38.719L34.311,38.719L34.311,37.813L35.256,37.813L35.256,35.893zM30.537,37.613C32.608,37.613 33.643,38.969 33.643,41.68L28.496,41.68C28.514,42.409 28.701,42.99 29.057,43.426C29.421,43.861 29.918,44.08 30.549,44.08C31.455,44.08 32.066,43.613 32.377,42.68L33.496,42.68C33.354,43.444 33.021,44.04 32.496,44.467C31.972,44.893 31.31,45.105 30.51,45.105C29.532,45.105 28.758,44.777 28.189,44.119C27.621,43.452 27.336,42.545 27.336,41.398C27.336,40.252 27.625,39.337 28.203,38.652C28.79,37.959 29.568,37.613 30.537,37.613zM41.283,37.613C42.145,37.613 42.798,37.777 43.242,38.105C43.687,38.425 43.91,38.897 43.91,39.52L43.91,43.625C43.91,43.989 44.11,44.172 44.51,44.172C44.59,44.172 44.67,44.164 44.75,44.146L44.75,44.986C44.439,45.066 44.186,45.105 43.99,45.105C43.635,45.105 43.362,45.022 43.176,44.854C42.998,44.694 42.888,44.436 42.844,44.08C42.097,44.765 41.304,45.105 40.469,45.105C39.767,45.105 39.207,44.92 38.789,44.547C38.38,44.174 38.176,43.67 38.176,43.039C38.176,42.835 38.195,42.647 38.23,42.479C38.275,42.31 38.319,42.164 38.363,42.039C38.417,41.906 38.504,41.786 38.629,41.68C38.753,41.564 38.856,41.47 38.936,41.398C39.024,41.327 39.168,41.261 39.363,41.199C39.568,41.128 39.723,41.079 39.83,41.053C39.937,41.017 40.124,40.978 40.391,40.934C40.657,40.889 40.852,40.858 40.977,40.84C41.101,40.822 41.323,40.791 41.643,40.746C42.078,40.693 42.38,40.608 42.549,40.492C42.718,40.377 42.803,40.204 42.803,39.973L42.803,39.68C42.803,39.342 42.666,39.084 42.391,38.906C42.124,38.728 41.74,38.639 41.242,38.639C40.727,38.639 40.337,38.741 40.07,38.945C39.804,39.141 39.648,39.452 39.604,39.879L38.482,39.879C38.536,38.368 39.47,37.613 41.283,37.613zM30.523,38.639C29.963,38.639 29.501,38.835 29.137,39.227C28.772,39.609 28.568,40.125 28.523,40.773L32.457,40.773C32.457,40.169 32.275,39.661 31.91,39.252C31.546,38.843 31.083,38.639 30.523,38.639zM23.336,38.652C22.749,38.652 22.279,38.901 21.924,39.398C21.568,39.887 21.391,40.542 21.391,41.359C21.391,42.177 21.568,42.834 21.924,43.332C22.279,43.821 22.749,44.066 23.336,44.066C23.94,44.066 24.429,43.821 24.803,43.332C25.185,42.834 25.377,42.19 25.377,41.398C25.377,40.563 25.19,39.896 24.816,39.398C24.452,38.901 23.958,38.652 23.336,38.652zM42.803,41.346C42.581,41.452 42.242,41.542 41.789,41.613C41.345,41.684 40.958,41.745 40.629,41.799C40.3,41.852 40.003,41.981 39.736,42.186C39.47,42.381 39.336,42.656 39.336,43.012C39.336,43.367 39.457,43.644 39.697,43.84C39.937,44.035 40.273,44.133 40.709,44.133C41.322,44.133 41.826,43.972 42.217,43.652C42.608,43.323 42.803,42.973 42.803,42.6L42.803,41.346z" android:strokeWidth="0.935758"/>
|
||||
</vector>
|
|
@ -1,64 +1,67 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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">
|
||||
|
||||
<LinearLayout
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:baselineAligned="false"
|
||||
android:orientation="horizontal">
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/appbar">
|
||||
<LinearLayout
|
||||
android:id="@+id/nav_host_fragment_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginBottom="?attr/actionBarSize"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:navGraph="@navigation/main_nav"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
tools:layout="@layout/fragment_artists" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/landscape_queue"
|
||||
android:layout_width="0dp"
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/landscape_queue"
|
||||
android:name="audio.funkwhale.ffa.fragments.LandscapeQueueFragment"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_weight="1"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:layout="@layout/partial_queue" />
|
||||
</LinearLayout>
|
||||
|
||||
<audio.funkwhale.ffa.views.NowPlayingBottomSheet
|
||||
android:id="@+id/now_playing_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="?attr/actionBarSize"
|
||||
android:layout_weight="1"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
android:background="@color/elevatedSurface"
|
||||
app:cardElevation="8dp"
|
||||
app:target_header="@id/constraint_layout_placeholder">
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/now_playing"
|
||||
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:layout="@layout/fragment_now_playing" />
|
||||
</audio.funkwhale.ffa.views.NowPlayingBottomSheet>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<audio.funkwhale.ffa.views.NowPlayingView
|
||||
android:id="@+id/now_playing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="8dp"
|
||||
android:alpha="0"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="8dp"
|
||||
app:cardElevation="12dp"
|
||||
app:layout_dodgeInsetEdges="bottom"
|
||||
tools:alpha="1"
|
||||
tools:visibility="visible">
|
||||
|
||||
<include layout="@layout/partial_now_playing" />
|
||||
|
||||
</audio.funkwhale.ffa.views.NowPlayingView>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:theme="@style/AppTheme.AppBar"
|
||||
app:backgroundTint="@color/colorPrimaryDark"
|
||||
app:layout_insetEdge="bottom"
|
||||
app:navigationIcon="@drawable/funkwhaleshape"
|
||||
tools:menu="@menu/toolbar" />
|
||||
tools:menu="@menu/toolbar"
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
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">
|
||||
|
||||
<data>
|
||||
<import type="androidx.lifecycle.LiveData" />
|
||||
<import type="android.view.View" />
|
||||
<import type="android.graphics.drawable.Drawable" />
|
||||
<variable name="isBuffering" type="LiveData<Boolean>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<Integer>" />
|
||||
<variable name="currentTrackTitle" type="LiveData<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<String>" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutDescription="@xml/fragment_now_playing_scene">
|
||||
|
||||
<include android:id="@+id/header" layout="@layout/partial_now_playing_header" />
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareView
|
||||
android:id="@+id/detail_image_placeholder"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_info"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_margin="8dp"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/alt_track_info"
|
||||
android:src="@drawable/more"
|
||||
app:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
|
||||
app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
|
||||
app:tint="@color/controlForeground"
|
||||
/>
|
||||
|
||||
<include
|
||||
android:id="@+id/controls"
|
||||
layout="@layout/partial_now_playing_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/detail_image_placeholder"
|
||||
android:alpha="0"
|
||||
android:background="@color/elevatedSurface"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
</layout>
|
|
@ -1,251 +0,0 @@
|
|||
<?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:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/elevatedSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/now_playing_progress"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-6dp"
|
||||
android:layout_marginBottom="-6dp"
|
||||
android:progress="40"
|
||||
android:progressTint="@color/colorPrimary" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareImageView
|
||||
android:id="@+id/now_playing_cover"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/now_playing_buffering"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="@color/controlForeground"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="2"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/itemTitle"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="Muse" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:icon="@drawable/play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/now_playing_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="32dp"
|
||||
android:paddingTop="16dp"
|
||||
android:paddingEnd="32dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<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"
|
||||
app:tint="@color/controlForeground" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/now_playing_details_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:max="100"
|
||||
android:progressBackgroundTint="#cacaca"
|
||||
android:progressTint="@color/controlForeground"
|
||||
android:thumbOffset="3dp"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:thumbTint="@color/controlForeground" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_duration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAlignment="textEnd" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_previous"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/control_previous"
|
||||
android:src="@drawable/previous" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_details_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
app:cornerRadius="64dp"
|
||||
app:icon="@drawable/play"
|
||||
app:iconSize="32dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_repeat"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/repeat" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,50 +1,58 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout 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:background="@color/surface">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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">
|
||||
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@id/appbar">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:id="@+id/nav_host_fragment_wrapper">
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="?attr/actionBarSize"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:navGraph="@navigation/main_nav"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
|
||||
tools:layout="@layout/fragment_artists"
|
||||
/>
|
||||
</FrameLayout>
|
||||
|
||||
<audio.funkwhale.ffa.views.NowPlayingView
|
||||
<audio.funkwhale.ffa.views.NowPlayingBottomSheet
|
||||
android:id="@+id/now_playing_bottom_sheet"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:backgroundTint="@color/elevatedSurface"
|
||||
app:cardElevation="16dp"
|
||||
app:target_header="@id/constraint_layout_placeholder">
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
android:id="@+id/now_playing"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_margin="8dp"
|
||||
android:alpha="0"
|
||||
android:visibility="gone"
|
||||
app:cardCornerRadius="3dp"
|
||||
app:cardElevation="12dp"
|
||||
app:layout_dodgeInsetEdges="bottom"
|
||||
tools:alpha="1"
|
||||
tools:visibility="visible">
|
||||
android:layout_height="match_parent"
|
||||
android:name="audio.funkwhale.ffa.fragments.NowPlayingFragment"
|
||||
tools:layout="@layout/fragment_now_playing"
|
||||
/>
|
||||
</audio.funkwhale.ffa.views.NowPlayingBottomSheet>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/now_playing_container"
|
||||
layout="@layout/partial_now_playing" />
|
||||
|
||||
</audio.funkwhale.ffa.views.NowPlayingView>
|
||||
|
||||
<com.google.android.material.bottomappbar.BottomAppBar
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:theme="@style/AppTheme.AppBar"
|
||||
app:backgroundTint="@color/elevatedSurface"
|
||||
app:layout_insetEdge="bottom"
|
||||
app:navigationIcon="@drawable/funkwhaleshape"
|
||||
tools:menu="@menu/toolbar" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
android:theme="@style/AppTheme.AppBar"
|
||||
android:background="@color/elevatedSurface"
|
||||
app:navigationIcon="@drawable/funkwhaleshape"
|
||||
tools:menu="@menu/toolbar"
|
||||
/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<data>
|
||||
<import type="androidx.lifecycle.LiveData" />
|
||||
<import type="android.view.View" />
|
||||
<import type="android.graphics.drawable.Drawable" />
|
||||
<variable name="isBuffering" type="LiveData<Boolean>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<Integer>" />
|
||||
<variable name="currentTrackTitle" type="LiveData<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<String>" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.motion.widget.MotionLayout
|
||||
android:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layoutDescription="@xml/fragment_now_playing_scene">
|
||||
|
||||
<include
|
||||
android:id="@+id/header"
|
||||
layout="@layout/partial_now_playing_header"
|
||||
/>
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareView
|
||||
android:id="@+id/detail_image_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_info"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
|
||||
app:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
|
||||
style="@style/IconButton"
|
||||
android:background="@drawable/circle"
|
||||
android:contentDescription="@string/alt_track_info"
|
||||
android:src="@drawable/more"
|
||||
app:tint="@color/controlForeground"
|
||||
/>
|
||||
|
||||
<include
|
||||
android:id="@+id/controls"
|
||||
layout="@layout/partial_now_playing_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/detail_image_placeholder"
|
||||
/>
|
||||
</androidx.constraintlayout.motion.widget.MotionLayout>
|
||||
</layout>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge
|
||||
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">
|
||||
|
||||
</merge>
|
|
@ -1,283 +0,0 @@
|
|||
<?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:id="@+id/now_playing_root"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="@color/elevatedSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/summary"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/now_playing_progress"
|
||||
style="@android:style/Widget.Material.ProgressBar.Horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="-6dp"
|
||||
android:layout_marginBottom="-6dp"
|
||||
android:progress="40"
|
||||
android:progressTint="@color/colorPrimaryDark" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:layout_marginEnd="16dp">
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareImageView
|
||||
android:id="@+id/now_playing_cover"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/now_playing_buffering"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:indeterminate="true"
|
||||
android:indeterminateTint="@color/controlForeground"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:layout_weight="2"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_title"
|
||||
style="@style/AppTheme.ItemTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
tools:text="Supermassive Black Hole" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_album"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
tools:text="Muse" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:icon="@drawable/play" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/now_playing_details"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:padding="8dp">
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareImageView
|
||||
android:id="@+id/now_playing_details_cover"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:adjustViewBounds="true"
|
||||
android:src="@drawable/funkwhaleshape"
|
||||
tools:src="@tools:sample/avatars" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_info"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
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"
|
||||
app:tint="@color/controlForeground" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/now_playing_details_controls"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:orientation="vertical"
|
||||
android:paddingTop="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:max="100"
|
||||
android:paddingStart="0dp"
|
||||
android:paddingEnd="0dp"
|
||||
android:progressBackgroundTint="#cacaca"
|
||||
android:progressTint="@color/controlForeground"
|
||||
android:thumbOffset="3dp"
|
||||
android:thumbTint="@color/controlForeground" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_current"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_duration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textAlignment="textEnd" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="64dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_previous"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:contentDescription="@string/control_previous"
|
||||
android:src="@drawable/previous" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_details_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
app:cornerRadius="64dp"
|
||||
app:icon="@drawable/play"
|
||||
app:iconSize="32dp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_repeat"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="28dp"
|
||||
android:layout_height="28dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/repeat" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,161 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
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">
|
||||
<data>
|
||||
<import type="androidx.lifecycle.LiveData" />
|
||||
<import type="android.graphics.drawable.Drawable" />
|
||||
<variable name="currentTrackTitle" type="LiveData<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<String>" />
|
||||
<variable name="isCurrentTrackFavorite" type="LiveData<Boolean>" />
|
||||
<variable name="repeatModeResource" type="LiveData<Drawable>" />
|
||||
<variable name="repeatModeAlpha" type="LiveData<Float>" />
|
||||
<variable name="currentProgressText" type="LiveData<String>" />
|
||||
<variable name="currentDurationText" type="LiveData<String>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<Integer>" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:alpha="0">
|
||||
<TextView
|
||||
android:id="@+id/current_playing_details_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:text="@{currentTrackTitle}"
|
||||
android:textColor="@color/itemTitle"
|
||||
android:textSize="18sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_add_to_playlist"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" tools:text="Supermassive Black Hole" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/current_playing_details_artist"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:text="@{currentTrackArtist}"
|
||||
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_add_to_playlist"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/current_playing_details_title"
|
||||
tools:text="Muse" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_add_to_playlist"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/playlist_add_to"
|
||||
android:src="@drawable/add_to_playlist"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/current_playing_details_artist"
|
||||
|
||||
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_favorite"
|
||||
app:layout_constraintTop_toTopOf="@+id/current_playing_details_title" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_favorite"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/control_add_to_favorites"
|
||||
android:src="@drawable/favorite"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/current_playing_details_artist"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/current_playing_details_title"
|
||||
app:tint="@{isCurrentTrackFavorite ? @color/colorFavorite : @color/controlForeground, default=@color/controlForeground}" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_current"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text='@{currentProgressText, default="5:04"}'
|
||||
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_progress"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/now_playing_details_progress" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/now_playing_details_progress"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:max="100"
|
||||
android:progress="@{progress, default=40}"
|
||||
android:progressBackgroundTint="#cacaca"
|
||||
android:progressTint="@color/controlForeground"
|
||||
android:thumbOffset="3dp"
|
||||
android:thumbTint="@color/controlForeground"
|
||||
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_progress_duration"
|
||||
app:layout_constraintStart_toEndOf="@+id/now_playing_details_progress_current"
|
||||
app:layout_constraintTop_toBottomOf="@+id/current_playing_details_artist" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/now_playing_details_progress_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text='@{currentDurationText, default="5:04"}'
|
||||
android:textAlignment="textEnd"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@+id/now_playing_details_progress" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_previous"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/control_previous"
|
||||
android:src="@drawable/previous"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
|
||||
app:layout_constraintEnd_toStartOf="@+id/now_playing_details_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_details_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="64dp"
|
||||
android:layout_height="64dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cornerRadius="64dp"
|
||||
app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}"
|
||||
app:iconSize="32dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_next"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
|
||||
app:layout_constraintStart_toEndOf="@+id/now_playing_details_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_details_repeat"
|
||||
style="@style/IconButton"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_margin="8dp"
|
||||
android:alpha="@{repeatModeAlpha, default=1}"
|
||||
android:contentDescription="@string/control_repeat_mode"
|
||||
android:src="@{repeatModeResource, default=@drawable/repeat}"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/now_playing_details_toggle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/now_playing_details_progress"
|
||||
app:tint="@color/controlForeground" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</layout>
|
|
@ -0,0 +1,117 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<data>
|
||||
<import type="androidx.lifecycle.LiveData" />
|
||||
<import type="android.view.View" />
|
||||
<import type="android.graphics.drawable.Drawable" />
|
||||
<variable name="isBuffering" type="LiveData<Boolean>" />
|
||||
<variable name="isPlaying" type="LiveData<Boolean>" />
|
||||
<variable name="progress" type="LiveData<Integer>" />
|
||||
<variable name="currentTrackTitle" type="LiveData<String>" />
|
||||
<variable name="currentTrackArtist" type="LiveData<String>" />
|
||||
</data>
|
||||
|
||||
<merge>
|
||||
<!-- Placeholder for setting constraints and interacting -->
|
||||
<View
|
||||
android:id="@+id/constraint_layout_placeholder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<com.google.android.material.progressindicator.LinearProgressIndicator
|
||||
android:id="@+id/now_playing_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="@id/constraint_layout_placeholder"
|
||||
android:progress="@{progress, default=40}"
|
||||
android:progressTint="@color/colorPrimaryDark"
|
||||
/>
|
||||
|
||||
<audio.funkwhale.ffa.views.SquareImageView
|
||||
android:id="@+id/now_playing_cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintStart_toStartOf="@id/constraint_layout_placeholder"
|
||||
app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
|
||||
app:layout_constraintBottom_toBottomOf="@id/constraint_layout_placeholder"
|
||||
android:scaleType="centerCrop"
|
||||
app:srcCompat="@drawable/cover"
|
||||
tools:src="@tools:sample/avatars"
|
||||
/>
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/now_playing_buffering"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintStart_toStartOf="@id/now_playing_cover"
|
||||
app:layout_constraintTop_toTopOf="@id/now_playing_cover"
|
||||
app:layout_constraintBottom_toBottomOf="@id/now_playing_cover"
|
||||
app:layout_constraintEnd_toEndOf="@id/now_playing_cover"
|
||||
android:visibility="@{isBuffering ? View.VISIBLE : View.INVISIBLE, default=invisible}"
|
||||
/>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/header_controls"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintHorizontal_weight="10"
|
||||
app:layout_constraintStart_toEndOf="@id/now_playing_cover"
|
||||
app:layout_constraintEnd_toEndOf="@id/constraint_layout_placeholder"
|
||||
app:layout_constraintTop_toBottomOf="@id/now_playing_progress"
|
||||
app:layout_constraintBottom_toBottomOf="@id/constraint_layout_placeholder"
|
||||
android:background="@color/elevatedSurface"
|
||||
android:padding="4dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/now_playing_toggle"
|
||||
style="@style/AppTheme.ItemTitle"
|
||||
android:text="@{currentTrackTitle}"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
tools:text="Supermassive Black Hole"
|
||||
/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/now_playing_toggle"
|
||||
android:ellipsize="end"
|
||||
android:lines="1"
|
||||
android:text="@{currentTrackArtist}"
|
||||
tools:text="Muse"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/now_playing_toggle"
|
||||
style="@style/AppTheme.OutlinedButton"
|
||||
android:layout_width="?attr/actionBarSize"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/now_playing_next"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:icon="@{isPlaying ? @drawable/pause : @drawable/play, default=@drawable/play}"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/now_playing_next"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
style="@style/IconButton"
|
||||
android:contentDescription="@string/control_next"
|
||||
android:src="@drawable/next"
|
||||
/>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</merge>
|
||||
|
||||
</layout>
|
|
@ -1,103 +1,119 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<navigation 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:id="@+id/main_nav"
|
||||
app:startDestination="@id/browseFragment">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main_nav"
|
||||
app:startDestination="@id/browseFragment">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/browseFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.BrowseFragment"
|
||||
android:label="BrowseFragment">
|
||||
<action
|
||||
android:id="@+id/browseToSearch"
|
||||
app:destination="@id/searchFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToAlbums"
|
||||
app:destination="@id/albumsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToTracks"
|
||||
app:destination="@id/tracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToArtists"
|
||||
app:destination="@id/artistsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToPlaylistTracks"
|
||||
app:destination="@id/playlistTracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playlistTracksFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.PlaylistTracksFragment"
|
||||
android:label="PlaylistTracksFragment" >
|
||||
<argument
|
||||
android:name="playlist"
|
||||
app:argType="audio.funkwhale.ffa.model.Playlist" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/tracksFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.TracksFragment"
|
||||
android:label="TracksFragment" >
|
||||
<argument
|
||||
android:name="album"
|
||||
app:argType="audio.funkwhale.ffa.model.Album" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/albumsFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.AlbumsFragment"
|
||||
android:label="AlbumsFragment" >
|
||||
<argument
|
||||
android:name="artist"
|
||||
app:argType="audio.funkwhale.ffa.model.Artist" />
|
||||
<argument
|
||||
android:name="cover"
|
||||
app:argType="string"
|
||||
app:nullable="true"
|
||||
android:defaultValue="@null" />
|
||||
<action
|
||||
android:id="@+id/albumsToTracks"
|
||||
app:destination="@id/tracksFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.SearchFragment"
|
||||
android:label="SearchFragment" >
|
||||
<action
|
||||
android:id="@+id/searchToAlbums"
|
||||
app:destination="@id/albumsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/searchToTracks"
|
||||
app:destination="@id/tracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/artistsFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.ArtistsFragment"
|
||||
android:label="ArtistsFragment" />
|
||||
<fragment
|
||||
android:id="@+id/browseFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.BrowseFragment"
|
||||
android:label="BrowseFragment">
|
||||
<action
|
||||
android:id="@+id/browseToSearch"
|
||||
app:destination="@id/searchFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToAlbums"
|
||||
app:destination="@id/albumsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToTracks"
|
||||
app:destination="@id/tracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToArtists"
|
||||
app:destination="@id/artistsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/browseToPlaylistTracks"
|
||||
app:destination="@id/playlistTracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/playlistTracksFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.PlaylistTracksFragment"
|
||||
android:label="PlaylistTracksFragment">
|
||||
<argument
|
||||
android:name="playlist"
|
||||
app:argType="audio.funkwhale.ffa.model.Playlist" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/tracksFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.TracksFragment"
|
||||
android:label="TracksFragment">
|
||||
<argument
|
||||
android:name="album"
|
||||
app:argType="audio.funkwhale.ffa.model.Album" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/albumsFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.AlbumsFragment"
|
||||
android:label="AlbumsFragment">
|
||||
<argument
|
||||
android:name="artist"
|
||||
app:argType="audio.funkwhale.ffa.model.Artist" />
|
||||
<argument
|
||||
android:name="cover"
|
||||
android:defaultValue="@null"
|
||||
app:argType="string"
|
||||
app:nullable="true" />
|
||||
<action
|
||||
android:id="@+id/albumsToTracks"
|
||||
app:destination="@id/tracksFragment" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/searchFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.SearchFragment"
|
||||
android:label="SearchFragment">
|
||||
<action
|
||||
android:id="@+id/searchToAlbums"
|
||||
app:destination="@id/albumsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
<action
|
||||
android:id="@+id/searchToTracks"
|
||||
app:destination="@id/tracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down" />
|
||||
</fragment>
|
||||
<fragment
|
||||
android:id="@+id/artistsFragment"
|
||||
android:name="audio.funkwhale.ffa.fragments.ArtistsFragment"
|
||||
android:label="ArtistsFragment" />
|
||||
<action
|
||||
android:id="@+id/globalBrowseToAlbums"
|
||||
app:destination="@id/albumsFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down"
|
||||
/>
|
||||
<action
|
||||
android:id="@+id/globalBrowseTracks"
|
||||
app:destination="@id/tracksFragment"
|
||||
app:enterAnim="@anim/slide_up"
|
||||
app:exitAnim="@anim/delayed_fade_out"
|
||||
app:popEnterAnim="@anim/none"
|
||||
app:popExitAnim="@anim/slide_down"
|
||||
/>
|
||||
</navigation>
|
|
@ -0,0 +1,2 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
|
@ -19,6 +19,11 @@
|
|||
<string name="settings_media_quality_quality">Qualitat màxima</string>
|
||||
<string name="settings_media_quality_size">Mida més petita</string>
|
||||
<string name="settings_media_quality_summary_quality">Es reproduirà la millor versió disponible</string>
|
||||
<string name="settings_bandwidth_limitation">Limitació d\'ample de banda</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Il·limitat</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limitat</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">El contingut original es recuperarà sense transcodificar</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint oferirà contingut que respecti la taxa de bits màxima de 320 kbps</string>
|
||||
<string name="settings_media_cache_size">Mida de la memòria cau dels mitjans</string>
|
||||
<string name="settings_play_order">Ordre de reproducció preferit</string>
|
||||
<string name="settings_play_order_shuffle">Àlbums aleatoris</string>
|
||||
|
|
|
@ -26,4 +26,113 @@
|
|||
<string name="login_error_userinfo">Nepodařilo se získat informace o uživateli</string>
|
||||
<string name="search_welcome">Pro prohledání vaší kolekce vložte klíčová slova výše a stiskněte enter</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB bude použito k ukládání skladeb pro offline přehrávání</string>
|
||||
<string name="login_error_hostname_https">Jméno hosta Funkwhale by mělo být zabezpečeno skrze HTTPS</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause_summary">Počet sekund, o které se skladba posune dozadu při pozastavení</string>
|
||||
<string name="settings_night_mode_system">Následovat systémové nastavení</string>
|
||||
<string name="settings_information">Informace</string>
|
||||
<string name="settings_information_repository_description">Funkwhale pro Android™</string>
|
||||
<string name="settings_crash_report_description">Pouze záznamy z posledních 5 minut před pádem budou zkopírovány</string>
|
||||
<string name="playback_queue_download">Stáhnout</string>
|
||||
<string name="error_playback">Tuto skladbu se nepodařilo přehrát</string>
|
||||
<string name="track_info_details">Informace</string>
|
||||
<string name="track_info_details_album">Album</string>
|
||||
<string name="track_info_details_track_license">Licence</string>
|
||||
<string name="radio_your_content_title">Váš obsah</string>
|
||||
<string name="radio_random_description">Úplně náhodný výběr, třeba objevíte něco nového\?</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d skladba • %2$s</item>
|
||||
<item quantity="few">%1$d skladby • %2$s</item>
|
||||
<item quantity="other">%1$d skladeb • %2$s</item>
|
||||
</plurals>
|
||||
<string name="filters_followed">Sledovaný obsah</string>
|
||||
<string name="login_hostname">Název hosta</string>
|
||||
<string name="settings_media_quality">Kvalita média</string>
|
||||
<string name="settings_bandwidth_limitation">Omezení šířky pásma</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Neomezený</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Omezený</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Původní obsah bude načten bez překódování</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint bude poskytovat obsah, který respektuje maximální bitovou rychlost 320 kbps</string>
|
||||
<string name="settings_media_cache_size">Velikost paměti cache pro média</string>
|
||||
<string name="settings_play_order_shuffle_summary">Preferujete promíchání skladeb v albech</string>
|
||||
<string name="settings_play_order_in_order">Přehrát alba v jejich pořadí</string>
|
||||
<string name="settings_play_order_in_order_summary">Preferujete přehrávat alba v pořadí</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause">Přeskočit dozadu při pozastavení</string>
|
||||
<string name="settings_other">Ostatní</string>
|
||||
<string name="settings_night_mode">Tmavý režim</string>
|
||||
<string name="settings_night_mode_on">Vždy zapnut (tmavý režim)</string>
|
||||
<string name="settings_night_mode_on_summary">Tmavý režim bude vždy zapnutý</string>
|
||||
<string name="settings_night_mode_off">Vždy vypnutý (světlý režim)</string>
|
||||
<string name="settings_night_mode_off_summary">Světlý režim bude vždy zapnutý</string>
|
||||
<string name="settings_night_mode_system_summary">Noční režim bude následovat systémové nastavení</string>
|
||||
<string name="settings_information_repository_title">Repozitář</string>
|
||||
<string name="settings_version_title">Verze</string>
|
||||
<string name="settings_information_license_title">Licence</string>
|
||||
<string name="settings_information_license_description">MIT licence</string>
|
||||
<string name="settings_crash_report_title">Zkopírovat záznamy o pádu</string>
|
||||
<string name="settings_crash_report_copied">Poslední hlášení o pádu bylo zkopírováno do schránky</string>
|
||||
<string name="settings_logout">Odhlásit se</string>
|
||||
<string name="artists">Interpreti</string>
|
||||
<string name="albums">Alba</string>
|
||||
<string name="tracks">Skladby</string>
|
||||
<string name="playlists">Seznamy skladeb</string>
|
||||
<string name="radios">Rádia</string>
|
||||
<string name="favorites">Oblíbené</string>
|
||||
<string name="playback_media_controls">Ovládání</string>
|
||||
<string name="playback_media_controls_description">Ovládat přehrávání</string>
|
||||
<string name="playback_play">Přehrát</string>
|
||||
<string name="playback_shuffle">Promíchat</string>
|
||||
<string name="playback_queue">Fronta</string>
|
||||
<string name="playback_queue_empty">Vaše fronta je prázdná</string>
|
||||
<string name="playback_queue_remove_item">Odstranit</string>
|
||||
<string name="playback_queue_add_item">Přidat do fronty</string>
|
||||
<string name="playback_queue_play_next">Přehrát další</string>
|
||||
<string name="playback_queue_clear">Vyčistit</string>
|
||||
<string name="playback_queue_save">Uložit</string>
|
||||
<string name="manage_add_to_favorites">Přidat do oblíbených</string>
|
||||
<string name="control_toggle">Přepnout přehrávání</string>
|
||||
<string name="control_previous">Předchozí skladba</string>
|
||||
<string name="control_next">Další skladba</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
<item quantity="few">%d alba</item>
|
||||
<item quantity="other">%d alb</item>
|
||||
</plurals>
|
||||
<string name="alt_app_logo">Logo aplikace</string>
|
||||
<string name="alt_artist_art">Přebal interpreta</string>
|
||||
<string name="alt_album_cover">Přebal alba</string>
|
||||
<string name="alt_more_options">Další možnosti</string>
|
||||
<string name="alt_track_info">Informace o skladbě</string>
|
||||
<string name="track_info_artist">Přejít na interpreta</string>
|
||||
<string name="track_info_album">Přejít na album</string>
|
||||
<string name="track_info_details_title">Detaily skladby</string>
|
||||
<string name="track_info_details_artist">Interpret</string>
|
||||
<string name="track_info_details_track_title">Název skladby</string>
|
||||
<string name="track_info_details_track_copyright">Copyright</string>
|
||||
<string name="track_info_details_track_duration">Délka</string>
|
||||
<string name="track_info_details_track_position">Pozice alba</string>
|
||||
<string name="track_info_details_track_bitrate">Přenosová rychlost</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instance</string>
|
||||
<string name="radio_playback_error">Při přehrávání tohoto rádia nastala chyba</string>
|
||||
<string name="radio_instance_radios">Rádia instance</string>
|
||||
<string name="radio_user_radios">Uživatelská rádia</string>
|
||||
<string name="radio_your_content_description">Výběry z vaší knihovny</string>
|
||||
<string name="radio_favorites_description">Poslouchejte své oblíbené skladby v nekonečné smyčce štěstí.</string>
|
||||
<string name="radio_random_title">Náhodné</string>
|
||||
<string name="radio_less_listened_title">Méně poslouchané</string>
|
||||
<string name="radio_less_listened_description">Poslouchejte skladby, které běžně neposloucháte. Je na čase obnovit rovnováhu.</string>
|
||||
<string name="logout_title">Odhlásit se</string>
|
||||
<string name="logout_content">Opravdu se chcete odhlásit z této Funkwhale instance\?</string>
|
||||
<string name="playlist_add_to">Přidat do seznamu skladeb</string>
|
||||
<string name="playlist_add_to_new">Nový seznam skladeb…</string>
|
||||
<string name="playlist_add_to_create">Vytvořit seznam skladeb</string>
|
||||
<string name="playlist_added_to">Přidáno do seznamu skladeb %s</string>
|
||||
<string name="filters">Filtry</string>
|
||||
<string name="fiters_all">Všechna hudba</string>
|
||||
<string name="filters_my_music">Moje hudba</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Stahuji %1$d skladbu</item>
|
||||
<item quantity="few">Stahuji %1$d skladby</item>
|
||||
<item quantity="other">Stahuji %1$d skladeb</item>
|
||||
</plurals>
|
||||
<string name="playlist">Seznam skladeb</string>
|
||||
</resources>
|
|
@ -23,6 +23,11 @@
|
|||
<string name="settings_media_quality_size">Kleine Dateigröße</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_bandwidth_limitation">Bandbreitenbegrenzung</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Unbegrenzt</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Begrenzt</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Originalinhalte werden ohne Transcodierung abgerufen</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint stellt Inhalte bereit, die eine maximale Bitrate von 320 kbps respektieren</string>
|
||||
<string name="settings_media_cache_size">Zwischenspeichergröße</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB werden für offline verfügbare Songs verwendet</string>
|
||||
<string name="settings_other">Andere</string>
|
||||
|
|
|
@ -16,6 +16,11 @@
|
|||
<string name="settings_media_quality_quality">Best quality</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_bandwidth_limitation">Bandwidth limitation</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Unlimited</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limited</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Original content will be fetched without transcoding</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint will deliver content that respects 320kbps maximum bitrate</string>
|
||||
<string name="settings_media_cache_size">Media cache size</string>
|
||||
<string name="settings_play_order">Preferred playback order</string>
|
||||
<string name="settings_play_order_shuffle">Shuffle albums</string>
|
||||
|
|
|
@ -88,6 +88,11 @@
|
|||
<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_bandwidth_limitation">Limitación de ancho de banda</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Ilimitada</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limitada</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">El contenido original se recuperará sin transcodificar</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint entregará contenido que respete la tasa de bits máxima de 320 kbps</string>
|
||||
<string name="settings_media_cache_size">Tamaño de el caché</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>
|
||||
|
|
|
@ -1,2 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources></resources>
|
||||
<resources>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Jatorrizko edukia transkodetzerik gabe jasoko da</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Amaiera-puntuak 320 kb/s-ko gehieneko bit-tasa jarraitzen duen edukia entregatuko du</string>
|
||||
<string name="settings_media_cache_size">Edukiaren cachearen tamaina</string>
|
||||
<string name="settings_bandwidth_limitation">Banda-zabaleraren muga</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Mugagabea</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Mugatua</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB erabiliko dira lineaz kanpo erreproduzituko diren pistak gordetzeko</string>
|
||||
<string name="settings_play_order">Hobetsitako erreproduzitzearen ordena</string>
|
||||
<string name="settings_play_order_shuffle">Nahastu albumak</string>
|
||||
<string name="settings_play_order_shuffle_summary">Albumetako pistak nahastea nahiago duzu</string>
|
||||
<string name="settings_play_order_in_order">Erreproduzitu albumak ordenaz</string>
|
||||
<string name="radio_less_listened_description">Entzun normalean entzuten ez dituzunak. Oreka berrezartzeko unea da.</string>
|
||||
<string name="logout_title">Amaitu saioa</string>
|
||||
<string name="settings_play_order_in_order_summary">Albumak ordenaz erreproduzitzea nahiago duzu</string>
|
||||
<string name="logout_content">Ziur zaude Funkwhale instantzia honetan saioa amaitu nahi duzula\?</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">Pista %1$d • %2$s</item>
|
||||
<item quantity="other">%1$d pista • %2$s</item>
|
||||
</plurals>
|
||||
<string name="playlist_add_to">Gehitu zerrendan</string>
|
||||
<string name="playlist_add_to_new">Zerrenda berria…</string>
|
||||
<string name="playlist_add_to_create">Sortu zerrenda</string>
|
||||
<string name="alt_artist_art">Artistaren irudia</string>
|
||||
<string name="alt_album_cover">Albumaren azala</string>
|
||||
<string name="alt_more_options">Aukera gehiago</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause_summary">Atzera egitea saltatzeko segundo kopurua erreproduzitzea pausatua dagoenean</string>
|
||||
<string name="settings_other">Besteak</string>
|
||||
<string name="settings_night_mode">Modu iluna</string>
|
||||
<string name="settings_night_mode_on">Beti gaituta (modu iluna)</string>
|
||||
<string name="settings_night_mode_off">Beti desgaituta (modu argia)</string>
|
||||
<string name="settings_night_mode_off_summary">Modu argia beti desgaituta egongo da</string>
|
||||
<string name="settings_night_mode_system">Jarraitu sistemako ezarpenak</string>
|
||||
<string name="settings_night_mode_system_summary">Gaueko moduak sistemako ezarpenak jarraituko ditu</string>
|
||||
<string name="settings_information">Informazioa</string>
|
||||
<string name="settings_information_repository_title">Biltegia</string>
|
||||
<string name="settings_version_title">Bertsioa</string>
|
||||
<string name="settings_information_license_title">Lizentzia</string>
|
||||
<string name="settings_information_license_description">MIT lizentzia</string>
|
||||
<string name="settings_crash_report_title">Kopiatu akats-egunkariak</string>
|
||||
<string name="settings_crash_report_copied">Azken akatsaren txostena arbelera kopiatu da</string>
|
||||
<string name="settings_logout">Amaitu saioa</string>
|
||||
<string name="artists">Artistak</string>
|
||||
<string name="albums">Albumak</string>
|
||||
<string name="tracks">Pistak</string>
|
||||
<string name="favorites">Gogokoenak</string>
|
||||
<string name="playback_media_controls">Edukiaren kontrolak</string>
|
||||
<string name="playback_play">Erreproduzitu</string>
|
||||
<string name="playback_shuffle">Nahastu</string>
|
||||
<string name="playback_queue">Jarri ilaran</string>
|
||||
<string name="playback_queue_empty">Ilara hutsik dago</string>
|
||||
<string name="playback_queue_remove_item">Kendu</string>
|
||||
<string name="playback_queue_add_item">Gehitu ilaran</string>
|
||||
<string name="playback_queue_play_next">Erreproduzitu jarraian</string>
|
||||
<string name="playback_queue_download">Deskargatu</string>
|
||||
<string name="playback_queue_clear">Garbitu</string>
|
||||
<string name="playback_queue_save">Gorde</string>
|
||||
<string name="manage_add_to_favorites">Gehitu gogokoenetara</string>
|
||||
<string name="control_previous">Aurreko pista</string>
|
||||
<string name="control_next">Hurrengo pista</string>
|
||||
<string name="error_playback">Ezin izan da erreproduzitu pista hau</string>
|
||||
<string name="alt_app_logo">Aplikazioaren logotipoa</string>
|
||||
<string name="alt_track_info">Pistaren informazioa</string>
|
||||
<string name="track_info_artist">Joan artistara</string>
|
||||
<string name="track_info_album">Joan albumera</string>
|
||||
<string name="track_info_details">Informazioa</string>
|
||||
<string name="track_info_details_title">Pistaren xehetasunak</string>
|
||||
<string name="track_info_details_artist">Artista</string>
|
||||
<string name="track_info_details_album">Albuma</string>
|
||||
<string name="track_info_details_track_copyright">Egile-eskubideak</string>
|
||||
<string name="track_info_details_track_license">Lizentzia</string>
|
||||
<string name="track_info_details_track_duration">Iraupena</string>
|
||||
<string name="track_info_details_track_position">Albumaren posizioa</string>
|
||||
<string name="track_info_details_track_bitrate">Bit-tasa</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale instantzia</string>
|
||||
<string name="radio_playback_error">Errore bat gertatu da irrati hau erreproduzitzean</string>
|
||||
<string name="radio_instance_radios">Instantziako irratiak</string>
|
||||
<string name="radio_user_radios">Erabiltzaileen irratiak</string>
|
||||
<string name="radio_your_content_description">Zure liburutegietako hautapenak</string>
|
||||
<string name="radio_random_title">Ausazkoa</string>
|
||||
<string name="radio_random_description">Ausaz hautatutakoak, agian zerbait berria aurkituko duzu!</string>
|
||||
<string name="radio_less_listened_title">Gutxien entzundakoak</string>
|
||||
<string name="control_add_to_favorites">Gehitu gogokoenetara</string>
|
||||
<string name="playlist_added_to">%s zerrendan gehitu da</string>
|
||||
<string name="filters">Iragazkiak</string>
|
||||
<string name="fiters_all">Musika guztia</string>
|
||||
<string name="filters_my_music">Nire musika</string>
|
||||
<string name="filters_followed">Jarraitutako edukia</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">Album %d</item>
|
||||
<item quantity="other">%d album</item>
|
||||
</plurals>
|
||||
<string name="hello_blank_fragment">Agurtzeko zati hutsa</string>
|
||||
<string name="playlist">Zerrenda</string>
|
||||
<string name="login_hostname">Ostalari-izena</string>
|
||||
<string name="login_cleartext">Onartu zifratu gabeko trafikoa (HTTP)</string>
|
||||
<string name="login_anonymous">Autentifikazio anonimoa</string>
|
||||
<string name="login_username">Erabiltzaile-izena</string>
|
||||
<string name="login_password">Pasahitza</string>
|
||||
<string name="login_submit">Hasi saioa</string>
|
||||
<string name="login_error">Ezin izan da hasi saioa: %s</string>
|
||||
<string name="login_error_funkwhale_not_found">Ez da aurkitu Funkwhale instantziarik</string>
|
||||
<string name="login_error_hostname_https">Funkwhale-ren ostalari-izena babestuta egon behar luke HTTPS bidez</string>
|
||||
<string name="login_error_userinfo">Ezin izan dugu berreskuratu erabiltzailearen informazioa</string>
|
||||
<string name="toolbar_search">Bilatu</string>
|
||||
<string name="title_downloads">Deskargak</string>
|
||||
<string name="title_settings">Ezarpenak</string>
|
||||
<string name="search_placeholder">Bilatu artistak, albumak eta pistak</string>
|
||||
<string name="search_welcome">Sartu bilaketa-terminoak goran eta sakatu Sartu bilduma arakatzeko</string>
|
||||
<string name="settings_general">Orokorra</string>
|
||||
<string name="settings_media_quality">Edukiaren kalitatea</string>
|
||||
<string name="settings_media_quality_quality">Kalitate onena</string>
|
||||
<string name="settings_media_quality_size">Tamaina txikiena</string>
|
||||
<string name="settings_media_quality_summary_size">Erabilgarri dagoen bertsiorik txikiena erreproduzituko da</string>
|
||||
<string name="login_welcome">Sartu Funkwhale instantziaren xehetasunak bere edukia atzitzeko</string>
|
||||
<string name="settings_information_repository_description">Android™-erako Funkwhale</string>
|
||||
<string name="login_logging_in">Saioa hasten</string>
|
||||
<string name="login_error_hostname">Ezin izan da ulertu URL baliozko gisa</string>
|
||||
<string name="settings_media_quality_summary_quality">Erabilgarri dagoen bertsiorik onena erreproduzituko da</string>
|
||||
<string name="settings_night_mode_on_summary">Modu iluna beti gaituta egongo da</string>
|
||||
<string name="search_no_results">Ez dira aurkitu kontsultaren emaitzarik</string>
|
||||
<string name="title_oss_licences">Ireki lizentzia-irekiak</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause">Saltatu atzera egitea pausatzean</string>
|
||||
<string name="radios">Irratiak</string>
|
||||
<string name="playlists">Zerrendak</string>
|
||||
<string name="playback_media_controls_description">Kontrolatu edukiaren erreproduzitzea</string>
|
||||
<string name="control_repeat_mode">Errepikapen modua</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Pista %1$d deskargatzen</item>
|
||||
<item quantity="other">%1$d pista deskargatzen</item>
|
||||
</plurals>
|
||||
<string name="settings_crash_report_description">Soilik akatsaren aurreko azken 5 minututako egunkariak bilduko dira</string>
|
||||
<string name="radio_your_content_title">Zure edukia</string>
|
||||
<string name="track_info_details_track_title">Pistaren izenburua</string>
|
||||
<string name="control_toggle">Aldatu erreproduzitzea</string>
|
||||
<string name="radio_favorites_description">Erreproduzitu abesti gogokoenak amaierarik gabeko begizta alai batean.</string>
|
||||
</resources>
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
<string name="settings_media_quality_size">Fichiers plus légers</string>
|
||||
<string name="settings_media_quality_summary_quality">Les pistes de meilleure qualité seront utilisées</string>
|
||||
<string name="settings_media_quality_summary_size">Les pistes les plus légères seront utilisées</string>
|
||||
<string name="settings_bandwidth_limitation">Limitation de bande passante</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Illimitée</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limitée</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Le contenu original sera téléchargé sans transcodage</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Le serveur délivrera du contenu transcodé avec un débit maximum de 320kbps</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>
|
||||
|
@ -131,4 +136,6 @@
|
|||
<string name="settings_auto_skip_backwards_on_pause_summary">Nombre de secondes à revenir en arrière lorsque la lecture est en pause</string>
|
||||
<string name="playlist">Liste de lecture</string>
|
||||
<string name="hello_blank_fragment">Bonjour, fragment vide</string>
|
||||
</resources>
|
||||
<string name="control_add_to_favorites">Ajouter aux favoris</string>
|
||||
<string name="control_repeat_mode">Mode répétition</string>
|
||||
</resources>
|
||||
|
|
|
@ -14,6 +14,11 @@
|
|||
<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_bandwidth_limitation">Limitación de ancho de banda</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Ilimitado</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limitado</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">O contido orixinal buscarase sen transcodificalo</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint ofrecerá contido que respecta a taxa de bits máxima de 320 kbps</string>
|
||||
<string name="settings_media_cache_size">Tamaño da caché</string>
|
||||
<string name="settings_media_quality_summary_size">Reproducirase a canción co menor tamaño dispoñible</string>
|
||||
<string name="settings_media_quality_summary_quality">Reproducirase a mellor calidade dispoñible</string>
|
||||
|
@ -124,4 +129,10 @@
|
|||
<string name="settings_information">Información</string>
|
||||
<string name="login_error_funkwhale_not_found">Non se atopa o nodo Funkwhale</string>
|
||||
<string name="login_error">Fallou a conexión: %s</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause">Rebobinar ao pausar</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause_summary">Número de segundos a rebobinar cando se pausou a reprodución</string>
|
||||
<string name="playlist">Lista de reprodución</string>
|
||||
<string name="control_repeat_mode">Modo repetición</string>
|
||||
<string name="control_add_to_favorites">Engadir a favoritos</string>
|
||||
<string name="hello_blank_fragment">Borrar esta cadea</string>
|
||||
</resources>
|
|
@ -100,6 +100,11 @@
|
|||
<string name="settings_play_order_shuffle">Izmiješ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_bandwidth_limitation">Ograničenje propusnosti</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Neograničen</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Ograničeno</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Izvorni sadržaj bit će dohvaćen bez transkodiranja</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint će isporučivati sadržaj koji poštuje maksimalnu brzinu prijenosa od 320 kbps</string>
|
||||
<string name="settings_media_cache_size">Veličina medijske predmemorije (cache)</string>
|
||||
<string name="settings_media_quality_summary_size">Svirat će se najmanja dostupna datoteka</string>
|
||||
<string name="settings_media_quality_summary_quality">Svirat će se najbolja dostupna verzija</string>
|
||||
|
|
|
@ -97,6 +97,11 @@
|
|||
<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_bandwidth_limitation">Limitazione della larghezza di banda</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Illimitata</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limitata</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Il contenuto originale verrà recuperato senza transcodifica</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint fornirà contenuti che rispettano il bitrate massimo di 320 kbps</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>
|
||||
|
|
|
@ -24,6 +24,11 @@
|
|||
<string name="title_downloads">ダウンロード</string>
|
||||
<string name="search_placeholder">アーティスト、アルバム、曲を探す</string>
|
||||
<string name="settings_general">一般設定</string>
|
||||
<string name="settings_bandwidth_limitation">帯域幅制限</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">無制限</string>
|
||||
<string name="settings_bandwidth_limitation_limited">限定</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">元のコンテンツはコード変換せずに取得されます</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">エンドポイントは、320kbps の最大ビットレートを尊重するコンテンツを配信します</string>
|
||||
<string name="settings_media_cache_size">メディアのキャッシュサイズ</string>
|
||||
<string name="login_anonymous">匿名での認証</string>
|
||||
<string name="login_hostname">ホストネーム</string>
|
||||
|
|
|
@ -19,6 +19,11 @@
|
|||
<string name="settings_media_quality">Media kwaliteit</string>
|
||||
<string name="settings_media_quality_quality">Beste kwaliteit</string>
|
||||
<string name="settings_media_quality_summary_quality">Best beschikbare versie zal worden afgespeeld</string>
|
||||
<string name="settings_bandwidth_limitation">Beperking van de bandbreedte</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Onbeperkt</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Beperkt</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Originele inhoud wordt opgehaald zonder transcodering</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint levert inhoud die de maximale bitsnelheid van 320 kbps respecteert</string>
|
||||
<string name="settings_media_cache_size">Media cache grootte</string>
|
||||
<string name="settings_play_order">Voorkeur afspeelvolgorde</string>
|
||||
<string name="settings_play_order_shuffle">Shuffle albums</string>
|
||||
|
@ -128,4 +133,7 @@
|
|||
</plurals>
|
||||
<string name="login_cleartext">Sta cleartext verkeer toe (HTTP)</string>
|
||||
<string name="settings_media_quality_summary_size">Lichtst beschikbaar nummer zal worden afgespeeld</string>
|
||||
<string name="control_repeat_mode">Herhalingsmodus</string>
|
||||
<string name="control_add_to_favorties">Toevoegen aan favorieten</string>
|
||||
<string name="control_add_to_favorites">Toevoegen aan favorieten</string>
|
||||
</resources>
|
|
@ -112,6 +112,11 @@
|
|||
<string name="settings_other">Inne</string>
|
||||
<string name="settings_play_order">Preferowana kolejność odtwarzania</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB zostanie przeznaczone na utwory dostępne offline</string>
|
||||
<string name="settings_bandwidth_limitation">Ograniczenie przepustowości</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Nieograniczony</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Ograniczony</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Oryginalna treść zostanie pobrana bez transkodowania</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint dostarczy treści, które respektują maksymalną przepływność 320 kb/s</string>
|
||||
<string name="settings_media_cache_size">Wielkość pamięci podręcznej mediów</string>
|
||||
<string name="settings_media_quality_summary_size">Odtwarzana będzie najmniejsza dostępna wersja</string>
|
||||
<string name="settings_media_quality_summary_quality">Odtwarzana będzie najlepsza dostępna jakość</string>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="login_error_hostname">Это не может быть понято как действительный URL</string>
|
||||
<string name="login_error_hostname">Это не может быть воспринято как действительный URL</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Загрузка %1$d трека</item>
|
||||
<item quantity="few">Загрузка %1$d треков</item>
|
||||
|
@ -11,19 +11,19 @@
|
|||
<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_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_description">Совершенно случайные подборки, может быть, вы откроете для себя что-то новое\?</string>
|
||||
<string name="radio_random_title">Случайный порядок</string>
|
||||
<string name="radio_favorites_description">Проигрывает ваши любимые мелодии по кругу и никогда не заканчивается.</string>
|
||||
<string name="radio_your_content_description">Подобрано из вашей библиотеки</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="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>
|
||||
|
@ -39,14 +39,14 @@
|
|||
<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>
|
||||
<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="error_playback">Этот трек не может быть воспроизведен</string>
|
||||
<string name="control_next">Следующий трек</string>
|
||||
<string name="control_previous">Прошлый трек</string>
|
||||
<string name="control_toggle">Переключить воспроизведение</string>
|
||||
|
@ -60,7 +60,7 @@
|
|||
<string name="playback_shuffle">Перемешать</string>
|
||||
<string name="playback_media_controls_description">Управление воспроизведением медиа</string>
|
||||
<string name="playback_media_controls">Управление медиа</string>
|
||||
<string name="favorites">Любимые</string>
|
||||
<string name="favorites">Избранное</string>
|
||||
<string name="radios">Радио</string>
|
||||
<string name="playlists">Плейлисты</string>
|
||||
<string name="tracks">Треки</string>
|
||||
|
@ -76,7 +76,7 @@
|
|||
<string name="settings_information_repository_description">Funkwhale для Android™</string>
|
||||
<string name="settings_information_repository_title">Репозиторий</string>
|
||||
<string name="settings_information">Информация</string>
|
||||
<string name="settings_night_mode_system_summary">Тёмный режим будет следовать настройкам системы</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>
|
||||
|
@ -85,6 +85,11 @@
|
|||
<string name="settings_night_mode">Тёмный режим</string>
|
||||
<string name="settings_other">Другие</string>
|
||||
<string name="settings_media_cache_size_summary">%d ГБ будет использовано для сохранения треков для оффлайн воспроизведения</string>
|
||||
<string name="settings_bandwidth_limitation">Ограничение пропускной способности</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Неограниченный</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Ограниченное</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Исходный контент будет загружен без перекодирования</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Конечная точка будет доставлять контент с максимальным битрейтом 320 кбит/с.</string>
|
||||
<string name="settings_media_cache_size">Размера медиакеша</string>
|
||||
<string name="settings_media_quality_summary_size">Будет проиграна наихудшая версия</string>
|
||||
<string name="settings_media_quality_summary_quality">Будет проиграна наилучшая версия</string>
|
||||
|
@ -93,7 +98,7 @@
|
|||
<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_settings">Настройки</string>
|
||||
|
@ -101,14 +106,14 @@
|
|||
<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_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="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>
|
||||
|
@ -120,11 +125,17 @@
|
|||
<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_summary">Вы предпочитаете слушать альбомы в порядке</string>
|
||||
<string name="settings_play_order_in_order">Проигрывать альбомы в порядке</string>
|
||||
<string name="settings_play_order_shuffle_summary">Перемешивать трэки в альбомах</string>
|
||||
<string name="settings_play_order_shuffle_summary">Вы предпочитаете перетасовывать дорожки альбомов</string>
|
||||
<string name="settings_play_order_shuffle">Перемешать альбомы</string>
|
||||
<string name="settings_play_order">Предпочитаемый порядок</string>
|
||||
<string name="login_error">Ошибка входа: %s</string>
|
||||
<string name="login_error_funkwhale_not_found">Сервер Funkwhale не найден</string>
|
||||
<string name="control_add_to_favorties">Добавить в избранное</string>
|
||||
<string name="hello_blank_fragment">Привет пустой фрагмент</string>
|
||||
<string name="playlist">Плейлист</string>
|
||||
<string name="control_repeat_mode">Режим повтора</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause">Откатиться назад при паузе</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause_summary">Количество секунд для отката назад, когда воспроизведение приостановлено</string>
|
||||
</resources>
|
|
@ -1,6 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="settings_media_quality_summary_size">Spår med minsta möjliga storlek kommer spelas</string>
|
||||
<string name="settings_bandwidth_limitation">Bandbreddsbegränsning</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Obegränsat</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Begränsad</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Originalinnehåll kommer att hämtas utan omkodning</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint kommer att leverera innehåll som respekterar 320 kbps maximal bithastighet</string>
|
||||
<string name="settings_media_cache_size">Cachningsstorlek för media</string>
|
||||
<string name="login_hostname">Servernamn</string>
|
||||
<string name="login_anonymous">Anonym autentisering</string>
|
||||
|
|
|
@ -0,0 +1,138 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="login_hostname">Sunucu adı</string>
|
||||
<string name="login_cleartext">Açık metin trafiğine izin ver (HTTP)</string>
|
||||
<string name="login_username">Kullanıcı Adı</string>
|
||||
<string name="login_password">Parola</string>
|
||||
<string name="login_submit">Giriş</string>
|
||||
<string name="login_logging_in">Oturum aç</string>
|
||||
<string name="login_error">Giriş başarısız oldu: %s</string>
|
||||
<string name="login_error_hostname">Bu geçerli bir Bağlantı olarak anlaşılamaz</string>
|
||||
<string name="login_error_hostname_https">Funkwhale ana sunucu adı HTTPS aracılığıyla güvenli olmalıdır</string>
|
||||
<string name="toolbar_search">Ara</string>
|
||||
<string name="title_downloads">İndirilenler</string>
|
||||
<string name="title_settings">Ayarlar</string>
|
||||
<string name="title_oss_licences">Açık kaynak lisansları</string>
|
||||
<string name="search_placeholder">Sanatçıları, albümleri ve parçaları arayın</string>
|
||||
<string name="search_no_results">Sorgunuz için sonuç bulunamadı</string>
|
||||
<string name="settings_general">Genel</string>
|
||||
<string name="settings_media_quality">Ortam kalitesi</string>
|
||||
<string name="settings_media_quality_quality">En iyi kalite</string>
|
||||
<string name="settings_media_quality_size">En küçük boyut</string>
|
||||
<string name="settings_media_quality_summary_quality">Mevcut en iyi sürüm oynatılacaktır</string>
|
||||
<string name="settings_media_quality_summary_size">Mevcut en küçük parça çalınacaktır</string>
|
||||
<string name="settings_bandwidth_limitation">Bant genişliği sınırlaması</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Sınırsız</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Sınırlı</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Uç nokta 320kbps maksimum bit hızına uygun içerik sunacaktır</string>
|
||||
<string name="settings_media_cache_size">Medya önbellek boyutu</string>
|
||||
<string name="settings_media_cache_size_summary">Çevrimdışı oynatma için parçaları depolamak üzere %d GB kullanılacaktır</string>
|
||||
<string name="settings_play_order">Tercih edilen oynatma sırası</string>
|
||||
<string name="settings_play_order_in_order">Albümleri sırayla çalın</string>
|
||||
<string name="settings_play_order_in_order_summary">Albümleri sırayla çalmayı tercih ediyorsunuz</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause">Duraklatırken geriye doğru atlama</string>
|
||||
<string name="settings_other">Diğer</string>
|
||||
<string name="settings_night_mode">Karanlık</string>
|
||||
<string name="settings_night_mode_on">Her zaman açık (karanlık)</string>
|
||||
<string name="settings_night_mode_on_summary">Karanlık tema her zaman açık olacak</string>
|
||||
<string name="settings_night_mode_off">Her zaman kapalı (açık)</string>
|
||||
<string name="settings_night_mode_off_summary">Açık tema her zaman açık olacak</string>
|
||||
<string name="settings_night_mode_system">Sistem ayarlarını takip edin</string>
|
||||
<string name="settings_play_order_shuffle">Karıştır albümleri</string>
|
||||
<string name="settings_information">Bilgi</string>
|
||||
<string name="settings_information_repository_title">Depo</string>
|
||||
<string name="settings_version_title">Sürüm</string>
|
||||
<string name="settings_information_license_title">Lisans</string>
|
||||
<string name="settings_information_license_description">MIT lisans</string>
|
||||
<string name="settings_crash_report_title">Kilitlenme günlüklerini kopyalama</string>
|
||||
<string name="settings_crash_report_copied">Son çökme raporu panonuza kopyalandı</string>
|
||||
<string name="settings_logout">Çıkış yapınız</string>
|
||||
<string name="artists">Sanatçılar</string>
|
||||
<string name="albums">Albümler</string>
|
||||
<string name="tracks">Parçalar</string>
|
||||
<string name="playlists">Çalma Listeleri</string>
|
||||
<string name="radios">Radyolar</string>
|
||||
<string name="favorites">Gözdeler</string>
|
||||
<string name="playback_media_controls">Ortam kontrolleri</string>
|
||||
<string name="playback_media_controls_description">Ortam oynatmayı kontrol etme</string>
|
||||
<string name="playback_play">Oynat</string>
|
||||
<string name="playback_shuffle">Karıştır</string>
|
||||
<string name="playback_queue">Kuyruk</string>
|
||||
<string name="playback_queue_empty">Kuyruk boş</string>
|
||||
<string name="playback_queue_remove_item">Kaldır</string>
|
||||
<string name="playback_queue_add_item">Kuyruğa ekle</string>
|
||||
<string name="playback_queue_download">İndir</string>
|
||||
<string name="playback_queue_clear">Temizle</string>
|
||||
<string name="playback_queue_save">Kaydet</string>
|
||||
<string name="manage_add_to_favorites">Gözdelerine ekle</string>
|
||||
<string name="control_toggle">Oynatmayı aç kapat</string>
|
||||
<string name="control_previous">Önceki parça</string>
|
||||
<string name="control_next">Sonraki parça</string>
|
||||
<string name="control_repeat_mode">Tekrar modu</string>
|
||||
<string name="control_add_to_favorites">Gözdelere ekle</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d albüm</item>
|
||||
<item quantity="other">%d albümler</item>
|
||||
</plurals>
|
||||
<string name="alt_app_logo">Uygulama logosu</string>
|
||||
<string name="alt_artist_art">Sanatçı sanatı</string>
|
||||
<string name="alt_album_cover">Albüm kapağı</string>
|
||||
<string name="alt_more_options">Daha fazla seçenek</string>
|
||||
<string name="alt_track_info">Parça hakkında bilgi</string>
|
||||
<string name="track_info_artist">Sanatçıya git</string>
|
||||
<string name="track_info_album">Albüme git</string>
|
||||
<string name="track_info_details">Bilgi</string>
|
||||
<string name="track_info_details_artist">Sanatçı</string>
|
||||
<string name="track_info_details_album">Albüm</string>
|
||||
<string name="track_info_details_track_title">Parça başlığı</string>
|
||||
<string name="track_info_details_track_copyright">Telif Hakkı</string>
|
||||
<string name="track_info_details_track_license">Lisans</string>
|
||||
<string name="login_error_funkwhale_not_found">Funkwhale kapsülü bulunamadı</string>
|
||||
<string name="track_info_details_track_duration">Süre</string>
|
||||
<string name="track_info_details_track_position">Albüm konumu</string>
|
||||
<string name="track_info_details_track_bitrate">BitHızı</string>
|
||||
<string name="track_info_details_track_instance">Funkwhale örneği</string>
|
||||
<string name="radio_playback_error">Bu radyoyu oynatmaya çalışırken bir hata oluştu</string>
|
||||
<string name="radio_instance_radios">Örnek radyolar</string>
|
||||
<string name="radio_user_radios">Kullanıcı radyoları</string>
|
||||
<string name="radio_your_content_title">İçeriğiniz</string>
|
||||
<string name="radio_your_content_description">Kendi kütüphanelerinizden seçtikleriniz</string>
|
||||
<string name="radio_random_title">Rastgele</string>
|
||||
<string name="radio_random_description">Tamamen rastgele seçimler, belki yeni şeyler keşfedersiniz\?</string>
|
||||
<string name="radio_less_listened_title">Daha az dinlendi</string>
|
||||
<string name="radio_less_listened_description">Genelde dinlemediğiniz parçaları dinleyin. Biraz denge sağlamanın zamanı geldi.</string>
|
||||
<string name="logout_title">Çıkış yapınız</string>
|
||||
<plurals name="playlist_description">
|
||||
<item quantity="one">%1$d parça • %2$s</item>
|
||||
<item quantity="other">%1$d parçalar • %2$s</item>
|
||||
</plurals>
|
||||
<string name="playlist_add_to">Oynatma listesine ekle</string>
|
||||
<string name="playlist_add_to_new">Yeni oynatma listesi…</string>
|
||||
<string name="playlist_add_to_create">Oynatma listesi oluştur</string>
|
||||
<string name="playlist_added_to">Oynatma listesi %s eklendi</string>
|
||||
<string name="filters">Süzgeçler</string>
|
||||
<string name="fiters_all">Tüm müzikler</string>
|
||||
<string name="filters_my_music">Benim müzikler</string>
|
||||
<string name="filters_followed">Takip edilen içerik</string>
|
||||
<string name="login_welcome">İçeriğine erişmek için lütfen Funkwhale örneğinizin ayrıntılarını girin</string>
|
||||
<string name="login_anonymous">Anonim kimlik doğrulama</string>
|
||||
<string name="logout_content">Bu Funkwhale örneğinden çıkmak istediğinizden emin misiniz\?</string>
|
||||
<plurals name="downloads_description">
|
||||
<item quantity="one">Parça %1$d indiriliyor</item>
|
||||
<item quantity="other">Parçalar %1$d indiriliyor</item>
|
||||
</plurals>
|
||||
<string name="login_error_userinfo">Kullanıcınız hakkında bilgi alamadık</string>
|
||||
<string name="settings_play_order_shuffle_summary">Albüm parçalarını karıştırmayı tercih ediyorsunuz</string>
|
||||
<string name="hello_blank_fragment">Merhaba boş parça</string>
|
||||
<string name="settings_auto_skip_backwards_on_pause_summary">Oynatma duraklatıldığında geriye doğru atlanacak saniye sayısı</string>
|
||||
<string name="settings_information_repository_description">Android™ için Funkwhale</string>
|
||||
<string name="settings_crash_report_description">Yalnızca çökmeden önceki son 5 dakikaya ait günlükler toplanacaktır</string>
|
||||
<string name="track_info_details_title">Parça detayları</string>
|
||||
<string name="search_welcome">Arama kelimerinizi yukarıya yazın ve koleksiyonunuzda arama yapmak için enter tuşuna basın</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Özgün içerik kod dönüştürme olmadan getirilecektir</string>
|
||||
<string name="settings_night_mode_system_summary">Gece teması sistem ayarlarını takip edecek</string>
|
||||
<string name="playback_queue_play_next">Sonrakini oynat</string>
|
||||
<string name="error_playback">Bu parça çalınamadı</string>
|
||||
<string name="radio_favorites_description">Hiç bitmeyen bir mutluluk döngüsünde en sevdiğiniz şarkıları çalın.</string>
|
||||
<string name="playlist">Oynatma listesi</string>
|
||||
</resources>
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- LAUNCH_SINGLE_INSTANCE -> overrides library mode because of
|
||||
bug in Android 14 (34) which causes infinite loop
|
||||
should be fixed soon with version 14.1 -->
|
||||
<integer name="launch_mode_for_app_auth">3</integer>
|
||||
</resources>
|
|
@ -71,6 +71,11 @@
|
|||
<string name="settings_night_mode">深色模式</string>
|
||||
<string name="settings_other">其它</string>
|
||||
<string name="settings_media_cache_size_summary">%d GB 将用于存储歌曲以进行脱机播放</string>
|
||||
<string name="settings_bandwidth_limitation">带宽限制</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">无限</string>
|
||||
<string name="settings_bandwidth_limitation_limited">有限的</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">将获取原始内容而不转码</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">端点将交付符合 320kbps 最大比特率的内容</string>
|
||||
<string name="settings_media_cache_size">媒体缓存大小</string>
|
||||
<string name="settings_media_quality_summary_size">将播放歌曲的最小体积版本</string>
|
||||
<string name="settings_media_quality_summary_quality">将播放歌曲的最佳质量版本</string>
|
||||
|
|
|
@ -10,6 +10,16 @@
|
|||
<item>size</item>
|
||||
</array>
|
||||
|
||||
<array name="bandwidth_limitation">
|
||||
<item>@string/settings_bandwidth_limitation_unlimited</item>
|
||||
<item>@string/settings_bandwidth_limitation_limited</item>
|
||||
</array>
|
||||
|
||||
<array name="bandwidth_limitation_values">
|
||||
<item>unlimited</item>
|
||||
<item>limited</item>
|
||||
</array>
|
||||
|
||||
<array name="play_orders">
|
||||
<item>@string/settings_play_order_shuffle</item>
|
||||
<item>@string/settings_play_order_in_order</item>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<declare-styleable name="NowPlaying">
|
||||
<attr name="target_header" format="reference" />
|
||||
</declare-styleable>
|
||||
</resources>
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<!-- LAUNCH_SINGLE_TASK -> default value from library -->
|
||||
<integer name="launch_mode_for_app_auth">2</integer>
|
||||
</resources>
|
|
@ -27,6 +27,11 @@
|
|||
<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_bandwidth_limitation">Bandwidth limitation</string>
|
||||
<string name="settings_bandwidth_limitation_unlimited">Unlimited</string>
|
||||
<string name="settings_bandwidth_limitation_limited">Limited</string>
|
||||
<string name="settings_bandwidth_limitation_summary_unlimited">Original content will be fetched without transcoding</string>
|
||||
<string name="settings_bandwidth_limitation_summary_limited">Endpoint will deliver content that respects 320kbps maximum bitrate</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_play_order">Preferred playback order</string>
|
||||
|
@ -76,6 +81,8 @@
|
|||
<string name="control_toggle">Toggle playback</string>
|
||||
<string name="control_previous">Previous track</string>
|
||||
<string name="control_next">Next track</string>
|
||||
<string name="control_repeat_mode">Repeat mode</string>
|
||||
<string name="control_add_to_favorites">Add to favorites</string>
|
||||
<string name="error_playback">This track could not be played</string>
|
||||
<plurals name="album_count">
|
||||
<item quantity="one">%d album</item>
|
||||
|
|
|
@ -68,9 +68,9 @@
|
|||
<style name="AppTheme.AppBar" parent="ThemeOverlay.MaterialComponents.Toolbar.Primary">
|
||||
<item name="android:drawableTint" tools:targetApi="m">@color/colorPrimary</item>
|
||||
<item name="android:tint">@color/colorPrimary</item>
|
||||
|
||||
<item name="actionBarPopupTheme">@style/AppTheme.PopupMenu</item>
|
||||
<item name="popupTheme">@style/AppTheme.PopupMenu</item>
|
||||
<item name="android:elevation">16dp</item>
|
||||
</style>
|
||||
|
||||
<style name="AppTheme.PopupMenu" parent="ThemeOverlay.MaterialComponents.Toolbar.Primary">
|
||||
|
|
|
@ -0,0 +1,110 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<MotionScene
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:motion="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ConstraintSet android:id="@+id/start">
|
||||
<Constraint android:id="@id/now_playing_details_info">
|
||||
<PropertySet android:alpha="0" android:visibility="invisible" />
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/header_controls">
|
||||
<PropertySet android:alpha="1" android:visibility="visible" />
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/constraint_layout_placeholder">
|
||||
<PropertySet android:visibility="visible" />
|
||||
</Constraint>
|
||||
<!--
|
||||
I don't know why MotionLayout tries to control visibility for the buffer progress bar,
|
||||
but it's messing with its display…
|
||||
-->
|
||||
<ConstraintOverride
|
||||
android:id="@id/now_playing_buffering"
|
||||
motion:visibilityMode="ignore"
|
||||
/>
|
||||
</ConstraintSet>
|
||||
|
||||
<ConstraintSet android:id="@+id/end">
|
||||
<Constraint
|
||||
android:id="@id/now_playing_cover"
|
||||
motion:layout_constraintEnd_toEndOf="@id/detail_image_placeholder"
|
||||
motion:layout_constraintStart_toStartOf="@id/detail_image_placeholder"
|
||||
motion:layout_constraintTop_toBottomOf="@id/detail_image_placeholder"
|
||||
motion:layout_constraintTop_toTopOf="@id/detail_image_placeholder"
|
||||
/>
|
||||
<!--
|
||||
I don't know why MotionLayout tries to control visibility for the buffer progress bar,
|
||||
but it's messing with its display…
|
||||
-->
|
||||
<ConstraintOverride
|
||||
android:id="@id/now_playing_buffering"
|
||||
motion:visibilityMode="ignore"
|
||||
/>
|
||||
<Constraint android:id="@id/now_playing_progress">
|
||||
<PropertySet android:alpha="0" android:visibility="gone" />
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/header_controls">
|
||||
<PropertySet android:alpha="0" android:visibility="invisible" />
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/constraint_layout_placeholder">
|
||||
<PropertySet android:visibility="invisible" />
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/now_playing_details_info">
|
||||
<PropertySet android:alpha="1" android:visibility="visible"/>
|
||||
</Constraint>
|
||||
<Constraint android:id="@id/controls">
|
||||
<PropertySet android:alpha="1" />
|
||||
</Constraint>
|
||||
</ConstraintSet>
|
||||
|
||||
<Transition
|
||||
motion:constraintSetEnd="@id/end"
|
||||
motion:constraintSetStart="@+id/start"
|
||||
>
|
||||
<KeyFrameSet>
|
||||
<KeyPosition
|
||||
motion:percentX="1"
|
||||
motion:framePosition="50"
|
||||
motion:motionTarget="@id/now_playing_cover"
|
||||
motion:curveFit="spline"
|
||||
/>
|
||||
|
||||
<KeyPosition
|
||||
motion:percentX="1"
|
||||
motion:framePosition="50"
|
||||
motion:motionTarget="@id/now_playing_buffering"
|
||||
motion:curveFit="spline"
|
||||
/>
|
||||
|
||||
<KeyAttribute
|
||||
android:alpha="0"
|
||||
motion:framePosition="10"
|
||||
motion:motionTarget="@id/header_controls"
|
||||
/>
|
||||
|
||||
<KeyPosition
|
||||
motion:percentX="1"
|
||||
motion:framePosition="50"
|
||||
motion:motionTarget="@id/header_controls"
|
||||
motion:curveFit="spline"
|
||||
/>
|
||||
|
||||
<KeyAttribute
|
||||
android:alpha="0"
|
||||
motion:framePosition="10"
|
||||
motion:motionTarget="@id/now_playing_progress"
|
||||
/>
|
||||
|
||||
<KeyAttribute
|
||||
android:alpha="0"
|
||||
motion:framePosition="90"
|
||||
motion:motionTarget="@id/now_playing_details_info"
|
||||
/>
|
||||
|
||||
<KeyAttribute
|
||||
android:alpha="0"
|
||||
motion:framePosition="90"
|
||||
motion:motionTarget="@id/controls"
|
||||
/>
|
||||
</KeyFrameSet>
|
||||
</Transition>
|
||||
</MotionScene>
|
|
@ -5,12 +5,22 @@
|
|||
<PreferenceCategory android:title="@string/settings_general">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="quality"
|
||||
android:entries="@array/media_qualities"
|
||||
android:entryValues="@array/media_qualities_values"
|
||||
android:icon="@drawable/quality"
|
||||
android:key="media_quality"
|
||||
android:title="@string/settings_media_quality" />
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:defaultValue="quality"
|
||||
android:entries="@array/media_qualities"
|
||||
android:entryValues="@array/media_qualities_values"
|
||||
android:icon="@drawable/quality"
|
||||
android:key="media_quality"
|
||||
android:title="@string/settings_media_quality" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="unlimited"
|
||||
android:entries="@array/bandwidth_limitation"
|
||||
android:entryValues="@array/bandwidth_limitation_values"
|
||||
android:icon="@drawable/network_beta"
|
||||
android:key="bandwidth_limitation"
|
||||
android:title="Bandwidth limitation" />
|
||||
|
||||
<SeekBarPreference
|
||||
android:defaultValue="1"
|
||||
|
|
|
@ -13,10 +13,10 @@ buildscript {
|
|||
val navVersion: String by extra
|
||||
|
||||
dependencies {
|
||||
classpath("com.android.tools.build:gradle:7.4.2")
|
||||
classpath("com.android.tools.build:gradle:8.1.2")
|
||||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20")
|
||||
classpath("com.github.bjoernq:unmockplugin:0.7.9")
|
||||
classpath("com.github.ben-manes:gradle-versions-plugin:0.44.0")
|
||||
classpath("com.github.ben-manes:gradle-versions-plugin:0.46.0")
|
||||
classpath("org.jacoco:org.jacoco.core:0.8.8")
|
||||
classpath("androidx.navigation:navigation-safe-args-gradle-plugin:$navVersion")
|
||||
}
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
+ Workaround Android 14 authentication breakage. Issue #148 (contributed by hdasch)
|
|
@ -0,0 +1 @@
|
|||
Log user out when authorization token expires (#154)
|
|
@ -0,0 +1 @@
|
|||
Remember server settings in login dialog (#154)
|
|
@ -1,2 +1,2 @@
|
|||
versionCode = 200000
|
||||
versionName = 0.2.0
|
||||
versionCode = 300000
|
||||
versionName = 0.3.0
|
||||
|
|
|
@ -4,3 +4,6 @@ kotlin.code.style=official
|
|||
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
android.defaults.buildfeatures.buildconfig=true
|
||||
android.nonTransitiveRClass=false
|
||||
android.nonFinalResIds=false
|
||||
|
|
Binary file not shown.
|
@ -1,6 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
|
@ -83,10 +83,8 @@ done
|
|||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD=maximum
|
||||
|
@ -133,10 +131,13 @@ location of your Java installation."
|
|||
fi
|
||||
else
|
||||
JAVACMD=java
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
if ! command -v java >/dev/null 2>&1
|
||||
then
|
||||
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
|
@ -144,7 +145,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
|
@ -152,7 +153,7 @@ if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
|||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
# shellcheck disable=SC2039,SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
@ -197,11 +198,15 @@ if "$cygwin" || "$msys" ; then
|
|||
done
|
||||
fi
|
||||
|
||||
# Collect all arguments for the java command;
|
||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
||||
# shell script including quotes and variable substitutions, so put them in
|
||||
# double quotes to make sure that they get re-expanded; and
|
||||
# * put everything else in single quotes, so that it's not re-expanded.
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
|
||||
# Collect all arguments for the java command:
|
||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||
# and any embedded shellness will be escaped.
|
||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||
# treated as '${Hostname}' itself on the command line.
|
||||
|
||||
set -- \
|
||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||
|
|
Loading…
Reference in New Issue