Rewrite loading UI

Completely rewrite the loading UI to be far more understandable.
This commit is contained in:
OxygenCobalt 2021-02-19 09:33:49 -07:00
parent 92e9e3282c
commit e3e0015237
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
12 changed files with 150 additions and 230 deletions

View file

@ -11,6 +11,7 @@
<img alt="Minimum SDK" src="https://img.shields.io/badge/API-21%2B-32B5ED"> <img alt="Minimum SDK" src="https://img.shields.io/badge/API-21%2B-32B5ED">
</p> </p>
<h4 align="center"><a href="/info/FAQ.md">FAQ</a> / <a href="/info/FORMATS.md">Formats</a> / <a href="/info/LICENSES.md">Licenses</a> / <a href="/.github/CONTRIBUTING.md">Contributing</a></h4> <h4 align="center"><a href="/info/FAQ.md">FAQ</a> / <a href="/info/FORMATS.md">Formats</a> / <a href="/info/LICENSES.md">Licenses</a> / <a href="/.github/CONTRIBUTING.md">Contributing</a></h4>
<p align="center"><a href="https://apt.izzysoft.de/fdroid/index/apk/org.oxycblt.auxio"><img src="https://gitlab.com/IzzyOnDroid/repo/-/raw/master/assets/IzzyOnDroid.png" width="170"></a></p>
## About ## About

View file

@ -13,17 +13,9 @@ import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.FragmentLoadingBinding import org.oxycblt.auxio.databinding.FragmentLoadingBinding
import org.oxycblt.auxio.logD
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.processing.MusicLoader
/** class LoadingFragment : Fragment() {
* An intermediary [Fragment] that asks for the READ_EXTERNAL_STORAGE permission and runs
* the music loading process in the background.
* @author OxygenCobalt
*/
class LoadingFragment : Fragment(R.layout.fragment_loading) {
// LoadingViewModel is scoped to this fragment only
private val loadingModel: LoadingViewModel by viewModels { private val loadingModel: LoadingViewModel by viewModels {
LoadingViewModel.Factory(requireActivity().application) LoadingViewModel.Factory(requireActivity().application)
} }
@ -35,115 +27,110 @@ class LoadingFragment : Fragment(R.layout.fragment_loading) {
): View { ): View {
val binding = FragmentLoadingBinding.inflate(inflater) val binding = FragmentLoadingBinding.inflate(inflater)
// Set up the permission launcher, as its disallowed outside of onCreate. // Build the permission launcher here as you can only do it in onCreateView/onCreate
val permLauncher = val permLauncher = registerForActivityResult(
registerForActivityResult( ActivityResultContracts.RequestPermission(), ::onPermResult
ActivityResultContracts.RequestPermission() )
) { granted: Boolean ->
// If its actually granted, restart the loading process again.
if (granted) {
returnToLoading(binding)
loadingModel.reload()
} else {
showError(binding)
binding.loadingGrantButton.visibility = View.VISIBLE
binding.loadingErrorText.text = getString(R.string.error_no_perms)
}
}
// --- UI SETUP --- // --- UI SETUP ---
binding.lifecycleOwner = this
binding.loadingModel = loadingModel binding.loadingModel = loadingModel
// --- VIEWMODEL SETUP --- // --- VIEWMODEL SETUP ---
loadingModel.response.observe(viewLifecycleOwner) {
if (it == MusicLoader.Response.SUCCESS) {
findNavController().navigate(
LoadingFragmentDirections.actionToMain()
)
} else {
// If the response wasn't a success, then show the specific error message
// depending on which error response was given, along with a retry button
binding.loadingErrorText.text =
if (it == MusicLoader.Response.NO_MUSIC)
getString(R.string.error_no_music)
else
getString(R.string.error_music_load_failed)
showError(binding)
binding.loadingRetryButton.visibility = View.VISIBLE
}
}
loadingModel.doReload.observe(viewLifecycleOwner) {
if (it) {
returnToLoading(binding)
loadingModel.doneWithReload()
}
}
loadingModel.doGrant.observe(viewLifecycleOwner) { loadingModel.doGrant.observe(viewLifecycleOwner) {
if (it) { if (it) {
permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE) permLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
returnToLoading(binding)
loadingModel.doneWithGrant() loadingModel.doneWithGrant()
} }
} }
// Force an error screen if the permissions are denied or the prompt needs to be shown. loadingModel.response.observe(viewLifecycleOwner) { response ->
if (checkPerms()) { when (response) {
showError(binding) // Success should lead to Auxio navigating away from the fragment
MusicStore.Response.SUCCESS -> findNavController().navigate(
LoadingFragmentDirections.actionToMain()
)
binding.loadingGrantButton.visibility = View.VISIBLE // Null means that the loading process is going on
binding.loadingErrorText.text = getString(R.string.error_no_perms) null -> showLoading(binding)
} else {
loadingModel.go() // Anything else is an error
else -> {
showError(binding, response)
}
}
} }
logD("Fragment created.") if (noPermissions()) {
// MusicStore.Response.NO_PERMS isnt actually returned by MusicStore, its just
// a way to keep the current permission state on_hand
loadingModel.notifyNoPermissions()
}
if (loadingModel.response.value == null) {
loadingModel.load()
}
return binding.root return binding.root
} }
override fun onResume() { // --- PERMISSIONS ---
super.onResume()
// If the music was already loaded, then don't do it again. private fun noPermissions(): Boolean {
if (MusicStore.getInstance().loaded) { val needRationale = shouldShowRequestPermissionRationale(
findNavController().navigate( Manifest.permission.READ_EXTERNAL_STORAGE
LoadingFragmentDirections.actionToMain() )
)
val notGranted = ContextCompat.checkSelfPermission(
requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_DENIED
return needRationale || notGranted
}
private fun onPermResult(granted: Boolean) {
if (granted) {
// If granted, its now safe to load [Which will clear the NO_PERMS response we applied
// earlier]
loadingModel.load()
} }
} }
// Check for two things: // --- UI DISPLAY ---
// - If Auxio needs to show the rationale for getting the READ_EXTERNAL_STORAGE permission.
// - If Auxio straight up doesn't have the READ_EXTERNAL_STORAGE permission. private fun showLoading(binding: FragmentLoadingBinding) {
private fun checkPerms(): Boolean { binding.apply {
return shouldShowRequestPermissionRationale( loadingCircle.visibility = View.VISIBLE
Manifest.permission.READ_EXTERNAL_STORAGE loadingErrorIcon.visibility = View.GONE
) || ContextCompat.checkSelfPermission( loadingErrorText.visibility = View.GONE
requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE loadingRetryButton.visibility = View.GONE
) == PackageManager.PERMISSION_DENIED loadingGrantButton.visibility = View.GONE
}
} }
// Remove the loading indicator and show the error groups private fun showError(binding: FragmentLoadingBinding, error: MusicStore.Response) {
private fun showError(binding: FragmentLoadingBinding) { binding.loadingCircle.visibility = View.GONE
binding.loadingBar.visibility = View.GONE
binding.loadingErrorIcon.visibility = View.VISIBLE binding.loadingErrorIcon.visibility = View.VISIBLE
binding.loadingErrorText.visibility = View.VISIBLE binding.loadingErrorText.visibility = View.VISIBLE
}
// Wipe views and switch back to the plain ProgressBar when (error) {
private fun returnToLoading(binding: FragmentLoadingBinding) { MusicStore.Response.NO_MUSIC -> {
binding.loadingBar.visibility = View.VISIBLE binding.loadingRetryButton.visibility = View.VISIBLE
binding.loadingErrorText.visibility = View.GONE binding.loadingErrorText.text = getString(R.string.error_no_music)
binding.loadingErrorIcon.visibility = View.GONE }
binding.loadingRetryButton.visibility = View.GONE
binding.loadingGrantButton.visibility = View.GONE MusicStore.Response.NO_PERMS -> {
binding.loadingGrantButton.visibility = View.VISIBLE
binding.loadingErrorText.text = getString(R.string.error_no_perms)
}
MusicStore.Response.FAILED -> {
binding.loadingRetryButton.visibility = View.VISIBLE
binding.loadingErrorText.text = getString(R.string.error_load_failed)
}
else -> {}
}
} }
} }

View file

@ -6,87 +6,47 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.oxycblt.auxio.music.MusicStore import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.processing.MusicLoader
/**
* A [ViewModel] responsible for getting the music loading process going and managing the response
* returned.
* @author OxygenCobalt
*/
class LoadingViewModel(private val app: Application) : ViewModel() { class LoadingViewModel(private val app: Application) : ViewModel() {
private val mResponse = MutableLiveData<MusicLoader.Response>() private val mResponse = MutableLiveData<MusicStore.Response?>(null)
val response: LiveData<MusicLoader.Response> get() = mResponse val response: LiveData<MusicStore.Response?> = mResponse
private val mRedo = MutableLiveData<Boolean>() private val mDoGrant = MutableLiveData(false)
val doReload: LiveData<Boolean> get() = mRedo val doGrant: LiveData<Boolean> = mDoGrant
private val mDoGrant = MutableLiveData<Boolean>() private var isBusy = false
val doGrant: LiveData<Boolean> get() = mDoGrant
private var started = false private val musicStore = MusicStore.getInstance()
/** fun load() {
* Start the music loading sequence. // Dont start a new load if the last one hasnt finished
* This should only be ran once, use reload() for all other loads. if (isBusy) return
*/
fun go() { isBusy = true
if (!started) { mResponse.value = null
started = true
doLoad()
}
}
private fun doLoad() {
viewModelScope.launch { viewModelScope.launch {
val musicStore = MusicStore.getInstance() mResponse.value = musicStore.load(app)
isBusy = false
val response = musicStore.load(app)
withContext(Dispatchers.Main) {
mResponse.value = response
}
} }
} }
/**
* Reload the music
*/
fun reload() {
mRedo.value = true
doLoad()
}
/**
* Mark that the UI is done with the reload call
*/
fun doneWithReload() {
mRedo.value = false
}
/**
* Mark to start the grant process
*/
fun grant() { fun grant() {
mDoGrant.value = true mDoGrant.value = true
} }
/**
* Mark that the UI is done with the grant process.
*/
fun doneWithGrant() { fun doneWithGrant() {
mDoGrant.value = false mDoGrant.value = false
} }
/** fun notifyNoPermissions() {
* Factory for [LoadingViewModel] instances. mResponse.value = MusicStore.Response.NO_PERMS
*/ }
@Suppress("UNCHECKED_CAST")
class Factory(private val application: Application) : ViewModelProvider.Factory { class Factory(private val application: Application) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel?> create(modelClass: Class<T>): T { override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) { if (modelClass.isAssignableFrom(LoadingViewModel::class.java)) {
return LoadingViewModel(application) as T return LoadingViewModel(application) as T

View file

@ -41,7 +41,7 @@ class MusicStore private constructor() {
* ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.*** * ***THIS SHOULD ONLY BE RAN FROM AN IO THREAD.***
* @param app [Application] required to load the music. * @param app [Application] required to load the music.
*/ */
suspend fun load(app: Application): MusicLoader.Response { suspend fun load(app: Application): Response {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {
this@MusicStore.logD("Starting initial music load...") this@MusicStore.logD("Starting initial music load...")
@ -50,7 +50,7 @@ class MusicStore private constructor() {
val loader = MusicLoader(app) val loader = MusicLoader(app)
val response = loader.loadMusic() val response = loader.loadMusic()
if (response == MusicLoader.Response.SUCCESS) { if (response == Response.SUCCESS) {
// If the loading succeeds, then sort the songs and update the value // If the loading succeeds, then sort the songs and update the value
val sorter = MusicSorter(loader.songs, loader.albums) val sorter = MusicSorter(loader.songs, loader.albums)
@ -72,6 +72,10 @@ class MusicStore private constructor() {
} }
} }
enum class Response {
NO_MUSIC, NO_PERMS, FAILED, SUCCESS
}
companion object { companion object {
@Volatile @Volatile
private var INSTANCE: MusicStore? = null private var INSTANCE: MusicStore? = null

View file

@ -12,6 +12,7 @@ import org.oxycblt.auxio.logD
import org.oxycblt.auxio.logE import org.oxycblt.auxio.logE
import org.oxycblt.auxio.music.Album import org.oxycblt.auxio.music.Album
import org.oxycblt.auxio.music.Genre import org.oxycblt.auxio.music.Genre
import org.oxycblt.auxio.music.MusicStore
import org.oxycblt.auxio.music.Song import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.toAlbumArtURI import org.oxycblt.auxio.music.toAlbumArtURI
@ -26,23 +27,25 @@ class MusicLoader(private val app: Application) {
private val resolver = app.contentResolver private val resolver = app.contentResolver
fun loadMusic(): Response { fun loadMusic(): MusicStore.Response {
try { try {
loadGenres() loadGenres()
loadAlbums() loadAlbums()
loadSongs() loadSongs()
} catch (error: Exception) { } catch (error: Exception) {
logE("Something went horribly wrong.") val trace = error.stackTraceToString()
error.printStackTrace()
return Response.FAILED logE("Something went horribly wrong.")
logE(trace)
return MusicStore.Response.FAILED
} }
if (songs.isEmpty()) { if (songs.isEmpty()) {
return Response.NO_MUSIC return MusicStore.Response.NO_MUSIC
} }
return Response.SUCCESS return MusicStore.Response.SUCCESS
} }
private fun loadGenres() { private fun loadGenres() {
@ -89,7 +92,6 @@ class MusicLoader(private val app: Application) {
Albums._ID, // 0 Albums._ID, // 0
Albums.ALBUM, // 1 Albums.ALBUM, // 1
Albums.ARTIST, // 2 Albums.ARTIST, // 2
Albums.FIRST_YEAR, // 3 Albums.FIRST_YEAR, // 3
), ),
null, null, null, null,
@ -207,8 +209,4 @@ class MusicLoader(private val app: Application) {
logD("Song search finished with ${songs.size} found") logD("Song search finished with ${songs.size} found")
} }
enum class Response {
SUCCESS, FAILED, NO_MUSIC
}
} }

View file

@ -7,7 +7,7 @@ import android.text.Spanned
import androidx.annotation.ColorRes import androidx.annotation.ColorRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.annotation.StyleRes import androidx.annotation.StyleRes
import androidx.core.text.toSpanned import androidx.core.text.HtmlCompat
import org.oxycblt.auxio.R import org.oxycblt.auxio.R
import org.oxycblt.auxio.settings.SettingsManager import org.oxycblt.auxio.settings.SettingsManager
import java.util.Locale import java.util.Locale
@ -59,10 +59,10 @@ data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes
val name = context.getString(name) val name = context.getString(name)
val hex = context.getString(color).toUpperCase(Locale.getDefault()) val hex = context.getString(color).toUpperCase(Locale.getDefault())
return context.getString( return HtmlCompat.fromHtml(
R.string.format_accent_summary, context.getString(R.string.format_accent_summary, name, hex),
name, hex HtmlCompat.FROM_HTML_MODE_COMPACT
).toSpanned().render() )
} }
companion object { companion object {
@ -70,8 +70,7 @@ data class Accent(@ColorRes val color: Int, @StyleRes val theme: Int, @StringRes
private var current: Accent? = null private var current: Accent? = null
/** /**
* Get the current accent, will default to whatever is stored in [SettingsManager] * Get the current accent.
* if there isnt one.
* @return The current accent * @return The current accent
*/ */
fun get(): Accent { fun get(): Accent {

View file

@ -8,7 +8,6 @@ import android.content.res.Resources
import android.graphics.Point import android.graphics.Point
import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.AnimatedVectorDrawable
import android.os.Build import android.os.Build
import android.text.Spanned
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -22,7 +21,6 @@ import androidx.annotation.DrawableRes
import androidx.annotation.PluralsRes import androidx.annotation.PluralsRes
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.text.HtmlCompat
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
@ -109,16 +107,6 @@ fun Fragment.requireCompatActivity(): AppCompatActivity {
} }
} }
/**
* "Render" a [Spanned] using [HtmlCompat]. (As in making text bolded and whatnot).
* @return A [Spanned] that actually works.
*/
fun Spanned.render(): Spanned {
return HtmlCompat.fromHtml(
this.toString(), HtmlCompat.FROM_HTML_OPTION_USE_CSS_COLORS
)
}
/** /**
* Resolve a color. * Resolve a color.
* @param context [Context] required * @param context [Context] required

View file

@ -39,7 +39,7 @@ class SlideLinearLayout @JvmOverloads constructor(
init { init {
if (disappearingChildrenField != null) { if (disappearingChildrenField != null) {
// Create a junk view and add it, which makes all the magic happen [I think]. // Create a invisible junk view and add it, which makes all the magic happen [I think].
dumpView = View(context) dumpView = View(context)
addView(dumpView, 0, 0) addView(dumpView, 0, 0)
} }
@ -58,13 +58,18 @@ class SlideLinearLayout @JvmOverloads constructor(
val children = getDisappearingChildren() val children = getDisappearingChildren()
if (doDrawingTrick && children != null) { if (doDrawingTrick && children != null) {
if (child == dumpView) { // Use the dump view as a marker for when to do the trick // Use the dump view as a marker for when to do the trick
var more = false if (child == dumpView) {
// I dont even know what this does.
var consumed = false
children.forEach { children.forEach {
more = more or super.drawChild(canvas, it, drawingTime) // What???? consumed = consumed or super.drawChild(canvas, it, drawingTime)
} }
return more
} else if (children.contains(child)) { // Ignore the disappearing children return consumed
} else if (children.contains(child)) {
// Ignore the disappearing children
return false return false
} }
} }

View file

@ -1,34 +1,30 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android" <layout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools">
tools:context=".loading.LoadingFragment">
<data> <data>
<variable <variable
name="loadingModel" name="loadingModel"
type="org.oxycblt.auxio.loading.LoadingViewModel" /> type="org.oxycblt.auxio.loading.LoadingViewModel" />
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:animateLayoutChanges="true"> android:orientation="vertical"
android:animateLayoutChanges="true"
android:gravity="center">
<ProgressBar <ProgressBar
android:id="@+id/loading_bar" android:id="@+id/loading_circle"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:indeterminateTint="?attr/colorPrimary" android:indeterminateTint="?attr/colorPrimary"
android:indeterminateTintMode="src_in" android:indeterminateTintMode="src_in"
android:paddingBottom="@dimen/padding_tiny" android:layout_margin="@dimen/margin_small"
app:layout_constraintBottom_toTopOf="@+id/loading_error_icon" android:paddingBottom="@dimen/padding_tiny" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />
<ImageView <ImageView
android:id="@+id/loading_error_icon" android:id="@+id/loading_error_icon"
@ -38,28 +34,19 @@
android:indeterminateTint="?attr/colorPrimary" android:indeterminateTint="?attr/colorPrimary"
android:indeterminateTintMode="src_in" android:indeterminateTintMode="src_in"
android:src="@drawable/ic_error" android:src="@drawable/ic_error"
android:layout_margin="@dimen/margin_small"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/loading_error_text" tools:visibility="visible"/>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_bar"
app:srcCompat="@drawable/ic_error" />
<TextView <TextView
android:id="@+id/loading_error_text" android:id="@+id/loading_error_text"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:fontFamily="@font/inter" android:fontFamily="@font/inter"
android:textAlignment="center" android:textAlignment="center"
android:textAppearance="?android:attr/textAppearanceMedium" android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?android:attr/textColorPrimary" android:textColor="?android:attr/textColorPrimary"
app:layout_constraintBottom_toTopOf="@+id/loading_retry_button" tools:text="Some kind of error" />
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_error_icon"
tools:text="Some kind of error." />
<Button <Button
android:id="@+id/loading_retry_button" android:id="@+id/loading_retry_button"
@ -67,17 +54,10 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold" android:fontFamily="@font/inter_semibold"
android:onClick="@{() -> loadingModel.reload()}"
android:text="@string/label_retry" android:text="@string/label_retry"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" android:onClick="@{() -> loadingModel.load()}"
app:layout_constraintBottom_toTopOf="@+id/loading_grant_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_error_text"
app:layout_constraintVertical_bias="0.673"
tools:visibility="visible" /> tools:visibility="visible" />
<Button <Button
@ -86,15 +66,12 @@
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fontFamily="@font/inter_semibold" android:fontFamily="@font/inter_semibold"
android:onClick="@{() -> loadingModel.grant()}"
android:text="@string/label_grant" android:text="@string/label_grant"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent" android:onClick="@{() -> loadingModel.grant()}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/loading_retry_button"
tools:visibility="visible" /> tools:visibility="visible" />
</androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
</layout> </layout>

View file

@ -89,7 +89,7 @@
<!-- Error Namespace | Error Labels --> <!-- Error Namespace | Error Labels -->
<string name="error_no_music">Keine Musik gefunden</string> <string name="error_no_music">Keine Musik gefunden</string>
<string name="error_music_load_failed">Laden die Musik fehlgeschlagen</string> <string name="error_load_failed">Laden die Musik fehlgeschlagen</string>
<string name="error_no_perms">Auxio braucht Berechtigung, zu lesen deine Musikbibliothek</string> <string name="error_no_perms">Auxio braucht Berechtigung, zu lesen deine Musikbibliothek</string>
<string name="error_no_browser">Link könnte nicht geöffnet werden</string> <string name="error_no_browser">Link könnte nicht geöffnet werden</string>

View file

@ -8,6 +8,7 @@
<!-- Label Namespace | Static Labels --> <!-- Label Namespace | Static Labels -->
<string name="label_retry">Retry</string> <string name="label_retry">Retry</string>
<string name="label_grant">Grant</string> <string name="label_grant">Grant</string>
<string name="label_view_trace">View Error</string>
<string name="label_library">Library</string> <string name="label_library">Library</string>
<string name="label_genres">Genres</string> <string name="label_genres">Genres</string>
@ -90,7 +91,7 @@
<!-- Error Namespace | Error Labels --> <!-- Error Namespace | Error Labels -->
<string name="error_no_music">No music found</string> <string name="error_no_music">No music found</string>
<string name="error_music_load_failed">Music loading failed</string> <string name="error_load_failed">Music loading failed</string>
<string name="error_no_perms">Auxio needs permission to read your music library</string> <string name="error_no_perms">Auxio needs permission to read your music library</string>
<string name="error_no_browser">Could not open link</string> <string name="error_no_browser">Could not open link</string>

View file

@ -14,7 +14,7 @@ Currently, its available on the [IzzyOnDroid F-Droid repository](https://apt.izz
## Can I translate Auxio to my native language? ## Can I translate Auxio to my native language?
See the [Translations](https://github.com/OxygenCobalt/Auxio/issues/3) issue for guidance on how to create translations and submit them to the project. Any contributions are apprieciated. See the [Translations](https://github.com/OxygenCobalt/Auxio/issues/3) issue for guidance on how to create translations and submit them to the project. Any contributions are appreciated.
## How can I contribute/report issues? ## How can I contribute/report issues?