Rewrite loading UI
Completely rewrite the loading UI to be far more understandable.
This commit is contained in:
parent
92e9e3282c
commit
e3e0015237
12 changed files with 150 additions and 230 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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?
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue