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
|
package org.oxycblt.auxio.detail
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaExtractor
|
||||||
|
import android.media.MediaFormat
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
import org.oxycblt.auxio.detail.recycler.DiscHeader
|
||||||
import org.oxycblt.auxio.detail.recycler.SortHeader
|
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.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
import org.oxycblt.auxio.music.Genre
|
||||||
import org.oxycblt.auxio.music.MusicStore
|
import org.oxycblt.auxio.music.MusicStore
|
||||||
|
import org.oxycblt.auxio.music.Song
|
||||||
import org.oxycblt.auxio.settings.SettingsManager
|
import org.oxycblt.auxio.settings.SettingsManager
|
||||||
import org.oxycblt.auxio.ui.Header
|
import org.oxycblt.auxio.ui.Header
|
||||||
import org.oxycblt.auxio.ui.Item
|
import org.oxycblt.auxio.ui.Item
|
||||||
import org.oxycblt.auxio.ui.Sort
|
import org.oxycblt.auxio.ui.Sort
|
||||||
import org.oxycblt.auxio.util.logD
|
import org.oxycblt.auxio.util.logD
|
||||||
|
import org.oxycblt.auxio.util.logW
|
||||||
import org.oxycblt.auxio.util.unlikelyToBeNull
|
import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,9 +51,15 @@ import org.oxycblt.auxio.util.unlikelyToBeNull
|
||||||
* @author OxygenCobalt
|
* @author OxygenCobalt
|
||||||
*/
|
*/
|
||||||
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
data class DetailSong(val song: Song, val bitrateKbps: Int?, val sampleRate: Int?)
|
||||||
|
|
||||||
private val musicStore = MusicStore.getInstance()
|
private val musicStore = MusicStore.getInstance()
|
||||||
private val settingsManager = SettingsManager.getInstance()
|
private val settingsManager = SettingsManager.getInstance()
|
||||||
|
|
||||||
|
private val _currentSong = MutableStateFlow<DetailSong?>(null)
|
||||||
|
val currentSong: StateFlow<DetailSong?>
|
||||||
|
get() = _currentSong
|
||||||
|
|
||||||
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
private val _currentAlbum = MutableStateFlow<Album?>(null)
|
||||||
val currentAlbum: StateFlow<Album?>
|
val currentAlbum: StateFlow<Album?>
|
||||||
get() = _currentAlbum
|
get() = _currentAlbum
|
||||||
|
@ -88,6 +103,13 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||||
currentGenre.value?.let(::refreshGenreData)
|
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) {
|
fun setAlbumId(id: Long) {
|
||||||
if (_currentAlbum.value?.id == id) return
|
if (_currentAlbum.value?.id == id) return
|
||||||
val library = unlikelyToBeNull(musicStore.library)
|
val library = unlikelyToBeNull(musicStore.library)
|
||||||
|
@ -120,6 +142,41 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||||
musicStore.addCallback(this)
|
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) {
|
private fun refreshAlbumData(album: Album) {
|
||||||
logD("Refreshing album data")
|
logD("Refreshing album data")
|
||||||
val data = mutableListOf<Item>(album)
|
val data = mutableListOf<Item>(album)
|
||||||
|
@ -166,6 +223,15 @@ class DetailViewModel : ViewModel(), MusicStore.Callback {
|
||||||
|
|
||||||
override fun onLibraryChanged(library: MusicStore.Library?) {
|
override fun onLibraryChanged(library: MusicStore.Library?) {
|
||||||
if (library != null) {
|
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
|
val album = currentAlbum.value
|
||||||
if (album != null) {
|
if (album != null) {
|
||||||
val newAlbum = library.sanitize(album).also { _currentAlbum.value = it }
|
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)
|
val buffer = replaceOutputBuffer(size)
|
||||||
|
|
||||||
if (volume == 1f) {
|
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)
|
// a for loop (the latter is not efficient)
|
||||||
buffer.put(inputBuffer.slice())
|
buffer.put(inputBuffer.slice())
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -206,6 +206,7 @@ class PlaybackService :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onPlayerError(error: PlaybackException) {
|
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.
|
// If there's any issue, just go to the next song.
|
||||||
playbackManager.next()
|
playbackManager.next()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ import androidx.core.view.children
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import org.oxycblt.auxio.R
|
import org.oxycblt.auxio.R
|
||||||
|
import org.oxycblt.auxio.detail.SongDetailDialog
|
||||||
import org.oxycblt.auxio.music.Album
|
import org.oxycblt.auxio.music.Album
|
||||||
import org.oxycblt.auxio.music.Artist
|
import org.oxycblt.auxio.music.Artist
|
||||||
import org.oxycblt.auxio.music.Genre
|
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
|
* TODO: Add multi-select
|
||||||
*/
|
*/
|
||||||
class ActionMenu(
|
class ActionMenu(
|
||||||
activity: AppCompatActivity,
|
private val activity: AppCompatActivity,
|
||||||
anchor: View,
|
anchor: View,
|
||||||
private val data: Item,
|
private val data: Item,
|
||||||
private val flag: Int
|
private val flag: Int
|
||||||
|
@ -180,6 +181,12 @@ class ActionMenu(
|
||||||
navModel.exploreNavigateTo(data.artist)
|
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)
|
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.
|
* Convenience method for getting a [Drawable] safely.
|
||||||
* @param drawable The drawable resource
|
* @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
|
<item
|
||||||
android:id="@+id/action_go_album"
|
android:id="@+id/action_go_album"
|
||||||
android:title="@string/lbl_go_album" />
|
android:title="@string/lbl_go_album" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_song_detail"
|
||||||
|
android:title="@string/lbl_song_detail" />
|
||||||
</menu>
|
</menu>
|
|
@ -47,6 +47,15 @@
|
||||||
|
|
||||||
<string name="lbl_go_artist">Go to artist</string>
|
<string name="lbl_go_artist">Go to artist</string>
|
||||||
<string name="lbl_go_album">Go to album</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>
|
<string name="lbl_state_saved">State saved</string>
|
||||||
|
|
||||||
|
@ -160,6 +169,8 @@
|
||||||
<string name="def_date">No Date</string>
|
<string name="def_date">No Date</string>
|
||||||
<string name="def_track">No Track Number</string>
|
<string name="def_track">No Track Number</string>
|
||||||
<string name="def_playback">No music playing</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_song">Song Name</string>
|
||||||
<string name="def_widget_artist">Artist 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_pos">+%.1f dB</string>
|
||||||
<string name="fmt_db_neg">-%.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>
|
<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="materialAlertDialogTheme">@style/Theme.Auxio.Dialog</item>
|
||||||
<item name="sliderStyle">@style/Widget.Auxio.Slider</item>
|
<item name="sliderStyle">@style/Widget.Auxio.Slider</item>
|
||||||
<item name="linearProgressIndicatorStyle">@style/Widget.Auxio.LinearProgressIndicator</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="textAppearanceDisplayLarge">@style/TextAppearance.Auxio.DisplayLarge</item>
|
||||||
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Auxio.DisplayMedium</item>
|
<item name="textAppearanceDisplayMedium">@style/TextAppearance.Auxio.DisplayMedium</item>
|
||||||
|
|
|
@ -54,6 +54,7 @@
|
||||||
app:summary="@string/set_quality_covers_desc"
|
app:summary="@string/set_quality_covers_desc"
|
||||||
app:title="@string/set_quality_covers" />
|
app:title="@string/set_quality_covers" />
|
||||||
|
|
||||||
|
<!-- FIXME: Should not be dependent on cover option -->
|
||||||
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
|
<org.oxycblt.auxio.settings.pref.M3SwitchPreference
|
||||||
app:defaultValue="false"
|
app:defaultValue="false"
|
||||||
app:dependency="KEY_SHOW_COVERS"
|
app:dependency="KEY_SHOW_COVERS"
|
||||||
|
|
Loading…
Reference in a new issue