detail: add framework for song details
Add the basic framework for a song detail view.
This commit is contained in:
parent
277e5a151f
commit
3f85678d99
12 changed files with 387 additions and 2 deletions
|
@ -17,9 +17,16 @@
|
|||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.media.MediaExtractor
|
||||
import android.media.MediaFormat
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
||||
|
@ -27,11 +34,13 @@ import org.oxycblt.auxio.music.Album
|
|||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
import org.oxycblt.auxio.music.MusicStore
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.settings.SettingsManager
|
||||
import org.oxycblt.auxio.ui.Header
|
||||
import org.oxycblt.auxio.ui.Item
|
||||
import org.oxycblt.auxio.ui.Sort
|
||||
import org.oxycblt.auxio.util.logD
|
||||
import org.oxycblt.auxio.util.logW
|
||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||
|
||||
/**
|
||||
|
@ -42,9 +51,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
|||
* @author OxygenCobalt
|
||||
*/
|
||||
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||
data class DetailSong(val song: Song, val bitrateKbps: Int?, val sampleRate: Int?)
|
||||
|
||||
private val musicStore = MusicStore.getInstance()
|
||||
private val settingsManager = SettingsManager.getInstance()
|
||||
|
||||
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
||||
val currentSong: StateFlow<DetailSong?>
|
||||
get() = _currentSong
|
||||
|
||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||
val currentAlbum: StateFlow<Album?>
|
||||
get() = _currentAlbum
|
||||
|
@ -88,6 +103,13 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
currentGenre.value?.let(::refreshGenreData)
|
||||
}
|
||||
|
||||
fun setSongId(context: Context, id: Long) {
|
||||
if (_currentSong.value?.run { song.id } == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
val song = requireNotNull(library.songs.find { it.id == id }) { "Invalid song id provided" }
|
||||
generateDetailSong(context, song)
|
||||
}
|
||||
|
||||
fun setAlbumId(id: Long) {
|
||||
if (_currentAlbum.value?.id == id) return
|
||||
val library = unlikelyToBeNull(musicStore.library)
|
||||
|
@ -120,6 +142,41 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
musicStore.addCallback(this)
|
||||
}
|
||||
|
||||
private fun generateDetailSong(context: Context, song: Song) {
|
||||
viewModelScope.launch {
|
||||
_currentSong.value =
|
||||
withContext(Dispatchers.IO) {
|
||||
val extractor = MediaExtractor()
|
||||
|
||||
try {
|
||||
extractor.setDataSource(context, song.uri, emptyMap())
|
||||
} catch (e: Exception) {
|
||||
logW("Unable to extract song attributes.")
|
||||
logW(e.stackTraceToString())
|
||||
return@withContext DetailSong(song, null, null)
|
||||
}
|
||||
|
||||
val format = extractor.getTrackFormat(0)
|
||||
|
||||
val bitrate =
|
||||
try {
|
||||
format.getInteger(MediaFormat.KEY_BIT_RATE) / 1000 // bps -> kbps
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
val sampleRate =
|
||||
try {
|
||||
format.getInteger(MediaFormat.KEY_SAMPLE_RATE)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
DetailSong(song, bitrate, sampleRate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshAlbumData(album: Album) {
|
||||
logD("Refreshing album data")
|
||||
val data = mutableListOf<Item>(album)
|
||||
|
@ -166,6 +223,15 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
|||
|
||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||
if (library != null) {
|
||||
// TODO: Add when we have a context
|
||||
// val song = currentSong.value
|
||||
// if (song != null) {
|
||||
// val newSong = library.sanitize(song.song)
|
||||
// if (newSong != null) {
|
||||
// generateDetailSong(newSong)
|
||||
// }
|
||||
// }
|
||||
|
||||
val album = currentAlbum.value
|
||||
if (album != null) {
|
||||
val newAlbum = library.sanitize(album).also { _currentAlbum.value = it }
|
||||
|
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.graphics.drawable.RippleDrawable
|
||||
import android.os.Build
|
||||
import android.text.method.MovementMethod
|
||||
import android.util.AttributeSet
|
||||
import android.view.View
|
||||
import androidx.annotation.AttrRes
|
||||
import androidx.core.graphics.drawable.DrawableCompat.setTint
|
||||
import com.google.android.material.shape.MaterialShapeDrawable
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.util.getAttrStateListSafe
|
||||
|
||||
class ReadOnlyTextInput : TextInputEditText {
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet?,
|
||||
@AttrRes defStyleAttr: Int
|
||||
) : super(context, attrs, defStyleAttr)
|
||||
|
||||
init {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
focusable = View.FOCUSABLE_AUTO
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFreezesText(): Boolean = false
|
||||
|
||||
override fun getDefaultEditable(): Boolean = false
|
||||
|
||||
override fun getDefaultMovementMethod(): MovementMethod? = null
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Auxio Project
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.oxycblt.auxio.detail
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.core.view.isGone
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import org.oxycblt.auxio.BuildConfig
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.databinding.DialogSongDetailBinding
|
||||
import org.oxycblt.auxio.music.Song
|
||||
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
|
||||
import org.oxycblt.auxio.util.launch
|
||||
|
||||
class SongDetailDialog : ViewBindingDialogFragment<DialogSongDetailBinding>() {
|
||||
private val detailModel: DetailViewModel by activityViewModels()
|
||||
|
||||
override fun onCreateBinding(inflater: LayoutInflater) =
|
||||
DialogSongDetailBinding.inflate(inflater)
|
||||
|
||||
override fun onConfigDialog(builder: AlertDialog.Builder) {
|
||||
super.onConfigDialog(builder)
|
||||
builder.setTitle(R.string.lbl_props).setPositiveButton(R.string.lbl_ok, null)
|
||||
}
|
||||
|
||||
override fun onBindingCreated(binding: DialogSongDetailBinding, savedInstanceState: Bundle?) {
|
||||
super.onBindingCreated(binding, savedInstanceState)
|
||||
detailModel.setSongId(requireContext(), requireNotNull(arguments).getLong(ARG_ID))
|
||||
launch { detailModel.currentSong.collect(::updateSong) }
|
||||
}
|
||||
|
||||
private fun updateSong(song: DetailViewModel.DetailSong?) {
|
||||
val binding = requireBinding()
|
||||
|
||||
if (song != null) {
|
||||
binding.detailContainer.isGone = false
|
||||
binding.detailFileName.setText(song.song.fileName)
|
||||
|
||||
if (song.bitrateKbps != null) {
|
||||
binding.detailBitrate.setText(getString(R.string.fmt_bitrate, song.bitrateKbps))
|
||||
} else {
|
||||
binding.detailBitrate.setText(R.string.def_bitrate)
|
||||
}
|
||||
|
||||
if (song.sampleRate != null) {
|
||||
binding.detailSampleRate.setText(
|
||||
getString(R.string.fmt_sample_rate, song.sampleRate))
|
||||
} else {
|
||||
binding.detailSampleRate.setText(R.string.def_sample_rate)
|
||||
}
|
||||
} else {
|
||||
binding.detailContainer.isGone = true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun from(song: Song): SongDetailDialog {
|
||||
val instance = SongDetailDialog()
|
||||
instance.arguments = Bundle().apply { putLong(ARG_ID, song.id) }
|
||||
return instance
|
||||
}
|
||||
|
||||
const val TAG = BuildConfig.APPLICATION_ID + ".tag.SONG_DETAILS"
|
||||
private const val ARG_ID = BuildConfig.APPLICATION_ID + ".arg.SONG_ID"
|
||||
}
|
||||
}
|
|
@ -226,7 +226,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
|
|||
val buffer = replaceOutputBuffer(size)
|
||||
|
||||
if (volume == 1f) {
|
||||
// No need to apply ReplayGain, do a memmove using put instead of
|
||||
// No need to apply ReplayGain, do a mem move using put instead of
|
||||
// a for loop (the latter is not efficient)
|
||||
buffer.put(inputBuffer.slice())
|
||||
} else {
|
||||
|
|
|
@ -206,6 +206,7 @@ class PlaybackService :
|
|||
}
|
||||
|
||||
override fun onPlayerError(error: PlaybackException) {
|
||||
// TODO: Replace with no skipping and a notification instead
|
||||
// If there's any issue, just go to the next song.
|
||||
playbackManager.next()
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import androidx.core.view.children
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import org.oxycblt.auxio.R
|
||||
import org.oxycblt.auxio.detail.SongDetailDialog
|
||||
import org.oxycblt.auxio.music.Album
|
||||
import org.oxycblt.auxio.music.Artist
|
||||
import org.oxycblt.auxio.music.Genre
|
||||
|
@ -59,7 +60,7 @@ fun Fragment.newMenu(anchor: View, data: Item, flag: Int = ActionMenu.FLAG_NONE)
|
|||
* TODO: Add multi-select
|
||||
*/
|
||||
class ActionMenu(
|
||||
activity: AppCompatActivity,
|
||||
private val activity: AppCompatActivity,
|
||||
anchor: View,
|
||||
private val data: Item,
|
||||
private val flag: Int
|
||||
|
@ -180,6 +181,12 @@ class ActionMenu(
|
|||
navModel.exploreNavigateTo(data.artist)
|
||||
}
|
||||
}
|
||||
R.id.action_song_detail -> {
|
||||
if (data is Song) {
|
||||
SongDetailDialog.from(data)
|
||||
.show(activity.supportFragmentManager, SongDetailDialog.TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -131,6 +131,28 @@ fun Context.getAttrColorSafe(@AttrRes attr: Int): Int {
|
|||
return getColorSafe(color)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting a color attribute safely.
|
||||
* @param attr The color attribute
|
||||
* @return The attribute requested, or black if an error occurred.
|
||||
*/
|
||||
@ColorInt
|
||||
fun Context.getAttrStateListSafe(@AttrRes attr: Int): ColorStateList {
|
||||
// First resolve the attribute into its ID
|
||||
val resolvedAttr = TypedValue()
|
||||
theme.resolveAttribute(attr, resolvedAttr, true)
|
||||
|
||||
// Then convert it to a proper color
|
||||
val color =
|
||||
if (resolvedAttr.resourceId != 0) {
|
||||
resolvedAttr.resourceId
|
||||
} else {
|
||||
resolvedAttr.data
|
||||
}
|
||||
|
||||
return getColorStateListSafe(color)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting a [Drawable] safely.
|
||||
* @param drawable The drawable resource
|
||||
|
|
131
app/src/main/res/layout/dialog_song_detail.xml
Normal file
131
app/src/main/res/layout/dialog_song_detail.xml
Normal file
|
@ -0,0 +1,131 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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:paddingTop="@dimen/spacing_medium">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/detail_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="@dimen/spacing_mid_large"
|
||||
android:paddingEnd="@dimen/spacing_mid_large"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="@string/lbl_file_name"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_file_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="file.mp3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:hint="@string/lbl_relative_path"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_relative_path"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="/path/to" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:hint="@string/lbl_format"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_format"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="MP3" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
app:expandedHintEnabled="false"
|
||||
android:hint="@string/lbl_size">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_size"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="16 MB" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:hint="@string/lbl_sort_duration"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_duration"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="3:20" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:hint="@string/lbl_bitrate"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_bitrate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="320 kb/s" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/spacing_medium"
|
||||
android:hint="@string/lbl_sample_rate"
|
||||
app:expandedHintEnabled="false">
|
||||
|
||||
<org.oxycblt.auxio.detail.ReadOnlyTextInput
|
||||
android:id="@+id/detail_sample_rate"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
tools:text="44100 Hz" />
|
||||
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
|
||||
</FrameLayout>
|
|
@ -12,4 +12,7 @@
|
|||
<item
|
||||
android:id="@+id/action_go_album"
|
||||
android:title="@string/lbl_go_album" />
|
||||
<item
|
||||
android:id="@+id/action_song_detail"
|
||||
android:title="@string/lbl_song_detail" />
|
||||
</menu>
|
|
@ -47,6 +47,15 @@
|
|||
|
||||
<string name="lbl_go_artist">Go to artist</string>
|
||||
<string name="lbl_go_album">Go to album</string>
|
||||
<string name="lbl_song_detail">View properties</string>
|
||||
|
||||
<string name="lbl_props">File properties</string>
|
||||
<string name="lbl_file_name">File name</string>
|
||||
<string name="lbl_relative_path">Relative path</string>
|
||||
<string name="lbl_format">Format</string>
|
||||
<string name="lbl_size">Size</string>
|
||||
<string name="lbl_bitrate">Bitrate</string>
|
||||
<string name="lbl_sample_rate">Sample rate</string>
|
||||
|
||||
<string name="lbl_state_saved">State saved</string>
|
||||
|
||||
|
@ -160,6 +169,8 @@
|
|||
<string name="def_date">No Date</string>
|
||||
<string name="def_track">No Track Number</string>
|
||||
<string name="def_playback">No music playing</string>
|
||||
<string name="def_bitrate">No Bitrate</string>
|
||||
<string name="def_sample_rate">No Sample Rate</string>
|
||||
<string name="def_widget_song">Song Name</string>
|
||||
<string name="def_widget_artist">Artist Name</string>
|
||||
|
||||
|
@ -187,6 +198,8 @@
|
|||
|
||||
<string name="fmt_db_pos">+%.1f dB</string>
|
||||
<string name="fmt_db_neg">-%.1f dB</string>
|
||||
<string name="fmt_bitrate">%d KB/s</string>
|
||||
<string name="fmt_sample_rate">%d Hz</string>
|
||||
|
||||
<string name="fmt_indexing">Loading your music library… (%1$d/%2$d)</string>
|
||||
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
<item name="materialAlertDialogTheme">@style/Theme.Auxio.Dialog</item>
|
||||
<item name="sliderStyle">@style/Widget.Auxio.Slider</item>
|
||||
<item name="linearProgressIndicatorStyle">@style/Widget.Auxio.LinearProgressIndicator</item>
|
||||
<item name="textInputStyle">@style/Widget.Material3.TextInputLayout.OutlinedBox</item>
|
||||
|
||||
<item name="textAppearanceDisplayLarge">@style/TextAppearance.Auxio.DisplayLarge</item>
|
||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Auxio.DisplayMedium</item>
|
||||
|
|
|
@ -54,6 +54,7 @@
|
|||
app:summary="@string/set_quality_covers_desc"
|
||||
app:title="@string/set_quality_covers" />
|
||||
|
||||
<!-- FIXME: Should not be dependent on cover option -->
|
||||
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
|
||||
app:defaultValue="false"
|
||||
app:dependency="KEY_SHOW_COVERS"
|
||||
|
|
Loading…
Reference in a new issue