Compare commits

...

54 Commits

Author SHA1 Message Date
Umut Solmaz 2d705609df Translated using Weblate (Turkish)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/tr/
2024-05-07 15:50:34 +00:00
josé m 5de5adfda0 Translated using Weblate (Galician)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/gl/
2024-05-07 15:50:30 +00:00
Umut Solmaz 93123f2df6 Added translation using Weblate (Turkish) 2024-05-06 14:36:45 +00:00
Bruno-Van-den-Bosch 52a45bc5df Translated using Weblate (Dutch)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/nl/
2024-04-12 13:50:30 +00:00
Renovate Bot bea9c5b75a chore(deps): update dependency io.insert-koin:koin-test to v3.5.3
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/347>
2024-02-22 19:01:22 +00:00
Renovate Bot 3ebf11a6f6 chore(deps): update dependency io.insert-koin:koin-core to v3.5.3
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/346>
2024-02-22 18:24:22 +00:00
Hugh Daschbach 5ee798abfb Remember hostname of last login.
Seed the login screen with saved host name, checkboxes.

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/342>
2024-02-22 18:15:04 +00:00
Hugh Daschbach 6f24535b79 Auto logout on unrecoverable authentication error.
When an unrecoverable authentication error occurs, automatically log
the user out.  This seems better than leaving the user wondering why
the UI is unresponsive or why each track they try to play fails with a
quickly disappearing toast.

Unrecoverable authentication errors typically mean the server has
timed out the session out or the session token has been deleted on the
server.  Tokens expire after 14 days without use.

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/342>
2024-02-22 18:15:04 +00:00
Hugh Daschbach 467556d75c Suppress some authentication noise in the log.
Most of this was added to debug issue !102.  So these are vestigial.

The exception here is the handling of AuthorizationException type 2.
These are produced by racing authentication requests and are
successfully managed.  So we need not report these.

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/342>
2024-02-22 18:15:04 +00:00
Renovate Bot 10035aa5fe chore(deps): update dependency io.insert-koin:koin-android to v3.5.3
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/345>
2024-02-22 09:44:21 +00:00
Georg Krause 8b9a1201af fix(copy): Use correct spelling of favorites
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/344>
2024-02-22 09:35:03 +00:00
Hugh Daschbach 2088e06a68 Do not close NowPlayingBottomSheet between tracks.
If the user opens the NowPlayingBottomSheet whilst playing a non empty
queue, leave the BottomSheet open at the end of the playing track.

Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/343>
2024-02-21 21:59:28 -08:00
Renovate Bot 22a72d9e83 chore(deps): update dependency androidx.preference:preference-ktx to v1.2.1
Part-of: <https://dev.funkwhale.audio/funkwhale/funkwhale-android/-/merge_requests/341>
2024-01-25 22:44:39 +00:00
Bruno-Van-den-Bosch 2bdf904804 Translated using Weblate (Dutch)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/nl/
2023-12-30 12:07:45 +00:00
mittwerk 042d6b4d6e Translated using Weblate (Russian)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/ru/
2023-12-30 12:07:44 +00:00
mittwerk 67aa47a4cb Translated using Weblate (Russian)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/ru/
2023-12-17 14:50:32 +00:00
Hugh Daschbach 01c676acd8 Update build tools path.
Gradle now fetches command line tools 30.0.3.
2023-12-14 22:14:31 -08:00
Hugh Daschbach b27e4c85ee Adjustments for Gradle 8 build environment.
Gradle 8 requires JDK version 11.
Update build class path to pick up new Gradle version.
2023-12-14 22:13:18 -08:00
Renovate Bot c061c64c3d chore(deps): update dependency gradle to v8 2023-12-12 13:08:10 +00:00
Georg Krause 554bc0ca5c Update version information for F-Droid 2023-12-12 13:44:40 +01:00
Georg Krause ef54aad835 Update changelog for version 0.3.0 2023-12-12 13:44:40 +01:00
Aitor d23456d334 Translated using Weblate (Basque)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/eu/
2023-12-12 11:50:28 +00:00
Georg Krause fef2d5b05f Added translation using Weblate (Bengali (Bangladesh)) 2023-12-09 12:31:10 +00:00
josé m 64f947aa23 Translated using Weblate (Galician)
Currently translated at 99.2% (126 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/gl/
2023-12-06 03:50:26 +00:00
Thomas 9666cccd5b Translated using Weblate (French)
Currently translated at 100.0% (127 of 127 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/fr/
2023-12-06 03:50:26 +00:00
Hugh Daschbach 36f1c7ba66 Enable landscape mode (auto-rotation). 2023-11-15 11:28:29 -08:00
Georg Krause 1978fc4fb4 Add missing changelog snippets 2023-11-15 07:53:24 +00:00
Hugh Daschbach c9056a2dbe Fix Android 14 authentication breakage.
Workaround to fix issue #148: authentication failure to redirect back
to FFA.

Google issue tracker: https://issuetracker.google.com/issues/210886001

Workaround suggested by AppAuth:
https://github.com/openid/AppAuth-Android/issues/977#issuecomment-1785604118
2023-11-08 09:06:06 +00:00
Hugh Daschbach c1eb9d6b2a Fix landscape view induced MainActivity leak.
With landscape view enabled (e.g. e06b2c7) in the app and auto
rotation enabled on the phone, switching between portrait and
landscape orientations leaks instances of MainActivity.  This prevents
garbage collection of not just the MainActivity object, but fragments
and other objects referenced by the Activity.

This is caused by repositories, the AppContext instance, the player
service, and authentication code maintaining a reference to the
context which with they are initialized.  So rather than initialize
these with an Activity context, pass them the Application context.

Activities are torn down and rebuilt on screen rotation.  The
Application context is not.

To enable instantiation of the FavoritedRepository with the
Application context, delay that repository’s initialization until
first use.  This ensures the Application context is fully initialized.
It is not fully initialized until the MainActivity has been fully
initialized.
2023-11-07 08:33:36 +00:00
Hugh Daschbach b9ade47988 Increase player controls touchpoint size.
Adopting AndroidStudio suggestion to help those of us with fat
fingers.
2023-11-07 08:33:36 +00:00
Hugh Daschbach 2133d4a4fb Prevent BottomSheet tap leaking to nav panels.
With the BottomSheet open, while trying to tap one of the
controls (esp. add to playlist and favorite buttons) it is easy to
miss the touch point and tap directly on the BottomSheet.

This tap bleeds through to whatever fragment is currently displayed in
the navigation area (Artist, Album, Playlists, etc.).  That tap
changes the view in the navigation panel.  For example, if the Artist
fragment it current, it will open a list of the artists albums.

That change may be surprising when the BottomSheet is toggled closed.
So, ignore BottomSheet taps outside the active controls.
2023-11-07 08:33:36 +00:00
Hugh Daschbach feb86fe9c0 Refactor CoverArt.withContext().
Having changed the context object in CoverArt from a received function
parameter to an initialization time derived variable, withContext no
longer needs a Context parameter.

That leaves the method misnamed.  So rename withContext ->
requestCreator and drop the first parameter.
2023-11-07 08:33:36 +00:00
Hugh Daschbach f65e29af39 Do not create unnecessary Picasso objects.
Address "java.lang.IllegalStateException: Too many receivers"
exceptions.  (See Issue #145).  Each new Picasso object registers its
own NetworkBroadcastReceiver.  Worse, we create a new Picasso object
each time we transform an AlbumCover image.  So do not create
unnecessary Picasso objects.

Rather than depend on receiving a Context object when called to load
an cover art, fetch the Application context as returned from FFA.get()
at singleton construction time.  The Application context is long
lived.

This has an additional advantage.  Not generation new Picasso objects
for each CoverArt image avoids holding a reference to an object that
cannot, later, be garbage collected.
2023-11-07 08:33:36 +00:00
Hugh Daschbach 4dba9e29dd chore(deps): update dependency io.insert-koin:koin-test to v3.5.0 2023-11-06 12:21:47 -08:00
Hugh Daschbach 629ce2b309 chore(deps): update dependency io.insert-koin:koin-core to v3.5.0 2023-11-06 12:21:07 -08:00
Renovate Bot 080ba21c35 chore(deps): update dependency io.insert-koin:koin-android to v3.5.0 2023-11-06 12:19:23 -08:00
Christophe Henry 31908b6175 Fix buffering progress bar display 2023-10-02 20:30:09 +02:00
Christophe Henry 1a050c2d73 Fixes form peer review 2023-09-28 17:32:53 +02:00
Christophe Henry 056e3a4d66 Use MotionLayout to animate bottom sheet opening 2023-09-27 15:56:15 +02:00
Christophe Henry b924a0c655 Fix bottom sheet being hidden in certain conditions 2023-09-18 20:18:52 +02:00
Christophe Henry 822adcac4a Fix overlap between main fragment and player bottom bar 2023-09-18 17:35:26 +02:00
Christophe Henry fbbd90111d Fix a few regressions with the new bottom sheet 2023-09-18 17:35:26 +02:00
Christophe Henry 45773aac8d Improve player bottom sheet, in particular fling support 2023-09-18 17:35:26 +02:00
josé m 6472a3743e Translated using Weblate (Galician)
Currently translated at 99.2% (124 of 125 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/gl/
2023-06-03 20:50:20 +00:00
Thomas ada0b09a66 Translated using Weblate (French)
Currently translated at 100.0% (125 of 125 strings)

Translation: Funkwhale/Funkwhale For Android
Translate-URL: https://translate.funkwhale.audio/projects/funkwhale/ffa/fr/
2023-06-03 20:50:15 +00:00
Hugh Daschbach 9c3d965a7e Fix Java version for gradle:7.4.2.
When Gradle version was bumped to 7.0.0, it required Java version 11.
In 82b9121 (2022/01/05), .sdkmanrc was adjusted to accommodate.  But
app/build.gradle.kts was not.

More recent Gradle releases have started to complain:
,----
| > Could not resolve all files for configuration ':classpath'.
|    > Could not resolve com.android.tools.build:gradle:7.4.2.
|      Required by:
|          project :
|       > No matching variant of com.android.tools.build:gradle:7.4.2 was found. The consumer was configured to find a library for use during runtime, compatible with Java 8, packaged as a jar, and its dependencies declared externally, as well as attribute 'org.gradle.plugin
| .api-version' with value '8.0.2' but:
|           - Variant 'apiElements' capability com.android.tools.build:gradle:7.4.2 declares a library, packaged as a jar, and its dependencies declared externally:
|               - Incompatible because this component declares a component for use during compile-time, compatible with Java 11 and the consumer needed a component for use during runtime, compatible with Java 8
|               - Other compatible attribute:
|                   - Doesn't say anything about org.gradle.plugin.api-version (required '8.0.2')
`----

Adjust gradle’s specification to suit.
2023-06-03 15:20:48 +00:00
Dylan Gageot 5c5d86a728 Add beta sign on network bandwidth limitation icon 2023-04-24 18:14:14 +02:00
Dylan Gageot 1288e050fd Add translations for other languanges than default 2023-04-24 17:25:24 +02:00
Dylan Gageot 8e09dccb9f Transcode at 320kbps when bandwidth limitation is enabled 2023-04-23 17:50:32 +00:00
Dylan Gageot 45ad4bdb8e Add summary for bandwidth limitation 2023-04-23 17:50:32 +00:00
Dylan Gageot 27e751df35 Add network icon for bandwidth limitation setting 2023-04-23 17:50:32 +00:00
Dylan Gageot 33938e3705 Add bandwidth limitation setting in Settings activity 2023-04-23 17:50:32 +00:00
Renovate Bot 1d5578febf chore(deps): update plugin com.github.triplet.play to v3.8.1 2023-04-19 10:30:51 +00:00
Georg krause e1be5b1303 Fix: Make check for proprietary code working with releases as well 2023-04-19 12:05:39 +02:00
82 changed files with 1944 additions and 1497 deletions

View File

@ -39,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
@ -75,7 +75,7 @@ test_nonfree_code:
stage: test-after-build
image: registry.funkwhale.audio/funkwhale/ci/android-fdroidserver
script:
- fdroid scanner -v app/build/outputs/apk/debug/app-debug.apk |& tee output.txt
- fdroid scanner -v app/build/outputs/apk/*/app-*.apk |& tee output.txt
- cat output.txt
- (! grep "CRITICAL" output.txt)

View File

@ -1,3 +1,22 @@
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:

View File

@ -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

View File

@ -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"

View File

@ -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()
}
}
}
}

View File

@ -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()
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 {

View File

@ -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)

View File

@ -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)

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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()

View File

@ -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))
}
}
}
}

View File

@ -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))

View File

@ -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()

View File

@ -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? {

View File

@ -68,7 +68,7 @@ class MediaControlsManager(
.run {
coverUrl?.let {
try {
setLargeIcon(CoverArt.withContext(context, coverUrl).get())
setLargeIcon(CoverArt.requestCreator(coverUrl).get())
} catch (_: Exception) {
}

View File

@ -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))
}
}
}

View File

@ -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)

View File

@ -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()
}

View File

@ -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 {

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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")

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()
}
}

View File

@ -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()
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;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>

View File

@ -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>

View File

@ -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>

View File

@ -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&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;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>

View File

@ -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>

View File

@ -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>

View File

@ -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&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;String>" />
<variable name="isCurrentTrackFavorite" type="LiveData&lt;Boolean>" />
<variable name="repeatModeResource" type="LiveData&lt;Drawable>" />
<variable name="repeatModeAlpha" type="LiveData&lt;Float>" />
<variable name="currentProgressText" type="LiveData&lt;String>" />
<variable name="currentDurationText" type="LiveData&lt;String>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;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>

View File

@ -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&lt;Boolean>" />
<variable name="isPlaying" type="LiveData&lt;Boolean>" />
<variable name="progress" type="LiveData&lt;Integer>" />
<variable name="currentTrackTitle" type="LiveData&lt;String>" />
<variable name="currentTrackArtist" type="LiveData&lt;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>

View File

@ -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>

View File

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>

View File

@ -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>

View File

@ -47,6 +47,11 @@
<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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,138 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="login_hostname">Sunucu adı</string>
<string name="login_cleartext">ı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">ı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">ı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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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"

View File

@ -13,7 +13,7 @@ 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.46.0")

View File

@ -0,0 +1 @@
+ Workaround Android 14 authentication breakage. Issue #148 (contributed by hdasch)

View File

@ -0,0 +1 @@
Log user out when authorization token expires (#154)

View File

@ -0,0 +1 @@
Remember server settings in login dialog (#154)

View File

@ -1,2 +1,2 @@
versionCode = 201000
versionName = 0.2.1
versionCode = 300000
versionName = 0.3.0

View File

@ -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.

View File

@ -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

29
gradlew vendored
View File

@ -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" \