music: revamp excluded into music dirs

Completely rework the excluded directory system into a new
"Music Folders" system.

This is implemented alongside a new "Include" mode. This mode
allows the user to restrict music indexing to a parsicular folder.
I've been reluctant to add this feature, as having two separate
options seemed bad. This resolves it by effectively packing whether
to include/exclude directories into a single option.

Resolves #154.
This commit is contained in:
OxygenCobalt 2022-06-12 15:51:05 -06:00
parent 53f3d0faef
commit 9b13b4c94e
No known key found for this signature in database
GPG key ID: 37DBE3621FE9AD47
28 changed files with 351 additions and 230 deletions

View file

@ -3,10 +3,12 @@
## dev [v2.3.2, v2.4.0, or v3.0.0]
#### What's New
- Added a new view for song properties (Such as Bitrate)
- Excluded directories has been revampled into "Music folders"
- Folders on external drives can now be excluded on Android Q+ [#134]
- The playback bar now has a new design, with an improved progress
indicator and a skip action
- Added new "Include" option to restrict indexing to a particular folder [#154]
- Added a new view for song properties (Such as Bitrate)
- The playback bar now has a new design, with an improved progress indicator and a
skip action
- When playing, covers now shows an animated indicator
#### What's Improved
@ -14,6 +16,7 @@ indicator and a skip action
- The toolbar layout is now consistent with Material Design 3
- Genre parsing now handles multiple integer values and cover/remix indicators (May wipe playback state)
- "Rounded album covers" option is no longer dependent on "Show album covers" option
- Added song actions to the playback panel
#### What's Fixed
- Playback bar now picks the larger inset in case that gesture inset is missing [#149]
@ -25,6 +28,7 @@ indicator and a skip action
- Moved music loading to a foreground service
- Phased out `ImageButton` for `MaterialButton`
- Unified icon sizing
- Added original date support to ExoPlayer parser (Not exposed in app)
## v2.3.1

View file

@ -70,6 +70,7 @@ sealed class Dir {
/**
* Represents a mime type as it is loaded by Auxio. [fromExtension] is based on the file extension
* should always exist, while [fromFormat] is based on the file itself and may not be available.
* @author OxygenCobalt
*/
data class MimeType(val fromExtension: String, val fromFormat: String?) {
fun resolveName(context: Context): String {
@ -86,18 +87,26 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) {
// We have special names for the most common formats.
val readableStringRes =
when (readableMime) {
// Classic formats
// MPEG formats
// While MP4 is AAC, it's considered separate given how common it is.
"audio/mpeg",
"audio/mp3" -> R.string.cdc_mp3
"audio/mp4",
"audio/mp4a-latm",
"audio/mpeg4-generic" -> R.string.cdc_mp4
// Free formats
// Generic Ogg is included here as it's actually formatted as "Ogg", not "OGG"
"audio/ogg",
"application/ogg" -> R.string.cdc_ogg
"audio/vorbis" -> R.string.cdc_ogg_vorbis
"audio/opus" -> R.string.cdc_ogg_opus
"audio/flac" -> R.string.cdc_flac
// MP4, 3GPP, M4A, etc. are all based on AAC
"audio/mp4",
"audio/mp4a-latm",
"audio/mpeg4-generic",
// The other AAC containers have a generic name
"audio/aac",
"audio/aacp",
"audio/aac-adts",
"audio/3gpp",
"audio/3gpp2", -> R.string.cdc_aac
@ -107,6 +116,8 @@ data class MimeType(val fromExtension: String, val fromFormat: String?) {
"audio/wave",
"audio/vnd.wave" -> R.string.cdc_wav
"audio/x-ms-wma" -> R.string.cdc_wma
// Don't know
else -> -1
}

View file

@ -203,6 +203,8 @@ class Task(context: Context, private val audio: MediaStoreBackend.Audio) {
}
}
// TODO: Release types
private fun populateId3v2(tags: Map<String, String>) {
// Title
tags["TIT2"]?.let { audio.title = it }

View file

@ -33,6 +33,7 @@ import org.oxycblt.auxio.music.Path
import org.oxycblt.auxio.music.Song
import org.oxycblt.auxio.music.albumCoverUri
import org.oxycblt.auxio.music.audioUri
import org.oxycblt.auxio.music.dirs.MusicDirs
import org.oxycblt.auxio.music.id3GenreName
import org.oxycblt.auxio.music.no
import org.oxycblt.auxio.music.queryCursor
@ -122,7 +123,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
override fun query(context: Context): Cursor {
val settingsManager = SettingsManager.getInstance()
val selector = buildExcludedSelector(settingsManager.excludedDirs)
val selector = buildMusicDirsSelector(settingsManager.musicDirs)
return requireNotNull(
context.contentResolverSafe.queryCursor(
@ -187,7 +188,7 @@ abstract class MediaStoreBackend : Indexer.Backend {
open val projection: Array<String>
get() = BASE_PROJECTION
abstract fun buildExcludedSelector(dirs: List<Dir.Relative>): Selector
abstract fun buildMusicDirsSelector(dirs: MusicDirs): Selector
/**
* Build an [Audio] based on the current cursor values. Each implementation should try to obtain
@ -367,19 +368,25 @@ open class Api21MediaStoreBackend : MediaStoreBackend() {
super.projection +
arrayOf(MediaStore.Audio.AudioColumns.TRACK, MediaStore.Audio.AudioColumns.DATA)
override fun buildExcludedSelector(dirs: List<Dir.Relative>): Selector {
override fun buildMusicDirsSelector(dirs: MusicDirs): Selector {
val base = Environment.getExternalStorageDirectory().absolutePath
var selector = BASE_SELECTOR
val args = mutableListOf<String>()
// Apply the excluded directories by filtering out specific DATA values.
for (dir in dirs) {
// Apply directories by filtering out specific DATA values.
for (dir in dirs.dirs) {
if (dir.volume is Dir.Volume.Secondary) {
logW("Cannot exclude directories on secondary drives")
continue
// Should never happen.
throw IllegalStateException()
}
selector +=
if (dirs.shouldInclude) {
" AND ${MediaStore.Audio.Media.DATA} LIKE ?"
} else {
" AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?"
}
selector += " AND ${MediaStore.Audio.Media.DATA} NOT LIKE ?"
args += "${base}/${dir.relativePath}%"
}
@ -442,17 +449,22 @@ open class Api29MediaStoreBackend : Api21MediaStoreBackend() {
MediaStore.Audio.AudioColumns.VOLUME_NAME,
MediaStore.Audio.AudioColumns.RELATIVE_PATH)
override fun buildExcludedSelector(dirs: List<Dir.Relative>): Selector {
override fun buildMusicDirsSelector(dirs: MusicDirs): Selector {
var selector = BASE_SELECTOR
val args = mutableListOf<String>()
// Starting in Android Q, we finally have access to the volume name. This allows
// use to properly exclude folders on secondary devices such as SD cards.
for (dir in dirs) {
for (dir in dirs.dirs) {
selector +=
if (dirs.shouldInclude) {
" AND (${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " +
"AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)"
} else {
" AND NOT (${MediaStore.Audio.AudioColumns.VOLUME_NAME} LIKE ? " +
"AND ${MediaStore.Audio.AudioColumns.RELATIVE_PATH} LIKE ?)"
}
// Assume that volume names are always lowercase counterparts to the volume
// name stored in-app. I have no idea how well this holds up on other devices.

View file

@ -15,10 +15,10 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.excluded
package org.oxycblt.auxio.music.dirs
import android.content.Context
import org.oxycblt.auxio.databinding.ItemExcludedDirBinding
import org.oxycblt.auxio.databinding.ItemMusicDirBinding
import org.oxycblt.auxio.music.Dir
import org.oxycblt.auxio.ui.BackingData
import org.oxycblt.auxio.ui.BindingViewHolder
@ -31,16 +31,16 @@ import org.oxycblt.auxio.util.textSafe
* Adapter that shows the excluded directories and their "Clear" button.
* @author OxygenCobalt
*/
class ExcludedAdapter(listener: Listener) :
MonoAdapter<Dir.Relative, ExcludedAdapter.Listener, ExcludedViewHolder>(listener) {
class MusicDirAdapter(listener: Listener) :
MonoAdapter<Dir.Relative, MusicDirAdapter.Listener, MusicDirViewHolder>(listener) {
override val data = ExcludedBackingData(this)
override val creator = ExcludedViewHolder.CREATOR
override val creator = MusicDirViewHolder.CREATOR
interface Listener {
fun onRemoveDirectory(dir: Dir.Relative)
}
class ExcludedBackingData(private val adapter: ExcludedAdapter) : BackingData<Dir.Relative>() {
class ExcludedBackingData(private val adapter: MusicDirAdapter) : BackingData<Dir.Relative>() {
private val _currentList = mutableListOf<Dir.Relative>()
val currentList: List<Dir.Relative> = _currentList
@ -70,22 +70,22 @@ class ExcludedAdapter(listener: Listener) :
}
}
/** The viewholder for [ExcludedAdapter]. Not intended for use in other adapters. */
class ExcludedViewHolder private constructor(private val binding: ItemExcludedDirBinding) :
BindingViewHolder<Dir.Relative, ExcludedAdapter.Listener>(binding.root) {
override fun bind(item: Dir.Relative, listener: ExcludedAdapter.Listener) {
binding.excludedPath.textSafe = item.resolveName(binding.context)
binding.excludedClear.setOnClickListener { listener.onRemoveDirectory(item) }
/** The viewholder for [MusicDirAdapter]. Not intended for use in other adapters. */
class MusicDirViewHolder private constructor(private val binding: ItemMusicDirBinding) :
BindingViewHolder<Dir.Relative, MusicDirAdapter.Listener>(binding.root) {
override fun bind(item: Dir.Relative, listener: MusicDirAdapter.Listener) {
binding.dirPath.textSafe = item.resolveName(binding.context)
binding.dirDelete.setOnClickListener { listener.onRemoveDirectory(item) }
}
companion object {
val CREATOR =
object : Creator<ExcludedViewHolder> {
object : Creator<MusicDirViewHolder> {
override val viewType: Int
get() = throw UnsupportedOperationException()
override fun create(context: Context) =
ExcludedViewHolder(ItemExcludedDirBinding.inflate(context.inflater))
MusicDirViewHolder(ItemMusicDirBinding.inflate(context.inflater))
}
}
}

View file

@ -0,0 +1,65 @@
/*
* 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.music.dirs
import android.os.Build
import java.io.File
import org.oxycblt.auxio.music.Dir
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
data class MusicDirs(val dirs: List<Dir.Relative>, val shouldInclude: Boolean) {
companion object {
private const val VOLUME_PRIMARY_NAME = "primary"
fun parseDir(dir: String): Dir.Relative? {
logD("Parse from string $dir")
val split = dir.split(File.pathSeparator, limit = 2)
val volume =
when (split[0]) {
VOLUME_PRIMARY_NAME -> Dir.Volume.Primary
else ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Dir.Volume.Secondary(split[0])
} else {
// While Android Q provides a stable way of accessing volumes, we can't
// trust that DATA provides a stable volume scheme on older versions, so
// external volumes are not supported.
logW("Cannot use secondary volumes below Android 10")
return null
}
}
val relativePath = split.getOrNull(1) ?: return null
return Dir.Relative(volume, relativePath)
}
fun toDir(dir: Dir.Relative): String {
val volume =
when (dir.volume) {
is Dir.Volume.Primary -> VOLUME_PRIMARY_NAME
is Dir.Volume.Secondary -> dir.volume.name
}
return "${volume}:${dir.relativePath}"
}
}
}

View file

@ -15,7 +15,7 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.oxycblt.auxio.music.excluded
package org.oxycblt.auxio.music.dirs
import android.net.Uri
import android.os.Bundle
@ -25,42 +25,41 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.fragment.app.activityViewModels
import kotlinx.coroutines.delay
import org.oxycblt.auxio.BuildConfig
import org.oxycblt.auxio.R
import org.oxycblt.auxio.databinding.DialogExcludedBinding
import org.oxycblt.auxio.databinding.DialogMusicDirsBinding
import org.oxycblt.auxio.music.Dir
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.settings.SettingsManager
import org.oxycblt.auxio.ui.ViewBindingDialogFragment
import org.oxycblt.auxio.util.hardRestart
import org.oxycblt.auxio.util.launch
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.showToast
/**
* Dialog that manages the currently excluded directories.
* Dialog that manages the music dirs setting.
* @author OxygenCobalt
*/
class ExcludedDialog :
ViewBindingDialogFragment<DialogExcludedBinding>(), ExcludedAdapter.Listener {
class MusicDirsDialog :
ViewBindingDialogFragment<DialogMusicDirsBinding>(), MusicDirAdapter.Listener {
private val settingsManager = SettingsManager.getInstance()
private val playbackModel: PlaybackViewModel by activityViewModels()
private val excludedAdapter = ExcludedAdapter(this)
private val dirAdapter = MusicDirAdapter(this)
override fun onCreateBinding(inflater: LayoutInflater) = DialogExcludedBinding.inflate(inflater)
override fun onCreateBinding(inflater: LayoutInflater) =
DialogMusicDirsBinding.inflate(inflater)
override fun onConfigDialog(builder: AlertDialog.Builder) {
// Don't set the click listener here, we do some custom magic in onCreateView instead.
builder
.setTitle(R.string.set_excluded)
.setTitle(R.string.set_dirs)
.setNeutralButton(R.string.lbl_add, null)
.setPositiveButton(R.string.lbl_save, null)
.setNegativeButton(R.string.lbl_cancel, null)
}
override fun onBindingCreated(binding: DialogExcludedBinding, savedInstanceState: Bundle?) {
override fun onBindingCreated(binding: DialogMusicDirsBinding, savedInstanceState: Bundle?) {
val launcher =
registerForActivityResult(ActivityResultContracts.OpenDocumentTree(), ::addDocTreePath)
@ -77,7 +76,10 @@ class ExcludedDialog :
}
dialog.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener {
if (settingsManager.excludedDirs != excludedAdapter.data.currentList) {
val dirs = settingsManager.musicDirs
if (dirs.dirs != dirAdapter.data.currentList ||
dirs.shouldInclude != isInclude(requireBinding())) {
logD("Committing changes")
saveAndRestart()
} else {
@ -87,34 +89,55 @@ class ExcludedDialog :
}
}
binding.excludedRecycler.apply {
adapter = excludedAdapter
binding.dirsRecycler.apply {
adapter = dirAdapter
itemAnimator = null
}
val dirs =
savedInstanceState
?.getStringArrayList(KEY_PENDING_DIRS)
?.mapNotNull(ExcludedDirectories::fromString)
?: settingsManager.excludedDirs
var dirs = settingsManager.musicDirs
excludedAdapter.data.addAll(dirs)
requireBinding().excludedEmpty.isVisible = dirs.isEmpty()
if (savedInstanceState != null) {
val pendingDirs = savedInstanceState.getStringArrayList(KEY_PENDING_DIRS)
if (pendingDirs != null) {
dirs =
MusicDirs(
pendingDirs.mapNotNull(MusicDirs::parseDir),
savedInstanceState.getBoolean(KEY_PENDING_MODE))
}
}
dirAdapter.data.addAll(dirs.dirs)
requireBinding().dirsEmpty.isVisible = dirs.dirs.isEmpty()
binding.folderModeGroup.apply {
check(
if (dirs.shouldInclude) {
R.id.dirs_mode_include
} else {
R.id.dirs_mode_exclude
})
updateMode()
addOnButtonCheckedListener { _, _, _ -> updateMode() }
}
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putStringArrayList(
KEY_PENDING_DIRS, ArrayList(excludedAdapter.data.currentList.map { it.toString() }))
KEY_PENDING_DIRS, ArrayList(dirAdapter.data.currentList.map { it.toString() }))
outState.putBoolean(KEY_PENDING_MODE, isInclude(requireBinding()))
}
override fun onDestroyBinding(binding: DialogExcludedBinding) {
override fun onDestroyBinding(binding: DialogMusicDirsBinding) {
super.onDestroyBinding(binding)
binding.excludedRecycler.adapter = null
binding.dirsRecycler.adapter = null
}
override fun onRemoveDirectory(dir: Dir.Relative) {
excludedAdapter.data.remove(dir)
dirAdapter.data.remove(dir)
requireBinding().dirsEmpty.isVisible = dirAdapter.data.currentList.isEmpty()
}
private fun addDocTreePath(uri: Uri?) {
@ -126,8 +149,8 @@ class ExcludedDialog :
val dir = parseExcludedUri(uri)
if (dir != null) {
excludedAdapter.data.add(dir)
requireBinding().excludedEmpty.isVisible = false
dirAdapter.data.add(dir)
requireBinding().dirsEmpty.isVisible = false
} else {
requireContext().showToast(R.string.err_bad_dir)
}
@ -143,22 +166,31 @@ class ExcludedDialog :
val treeUri = DocumentsContract.getTreeDocumentId(docUri)
// Parsing handles the rest
return ExcludedDirectories.fromString(treeUri)
return MusicDirs.parseDir(treeUri)
}
private fun updateMode() {
val binding = requireBinding()
if (isInclude(binding)) {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_include_desc)
} else {
binding.dirsModeDesc.setText(R.string.set_dirs_mode_exclude_desc)
}
}
private fun isInclude(binding: DialogMusicDirsBinding) =
binding.folderModeGroup.checkedButtonId == R.id.dirs_mode_include
private fun saveAndRestart() {
settingsManager.excludedDirs = excludedAdapter.data.currentList
settingsManager.musicDirs =
MusicDirs(dirAdapter.data.currentList, isInclude(requireBinding()))
// TODO: Dumb stopgap measure until automatic rescanning, REMOVE THIS BEFORE
// MAKING ANY RELEASE!!!!!!
launch {
delay(1000)
playbackModel.savePlaybackState(requireContext()) { requireContext().hardRestart() }
}
}
companion object {
const val TAG = BuildConfig.APPLICATION_ID + ".tag.EXCLUDED"
const val KEY_PENDING_DIRS = BuildConfig.APPLICATION_ID + ".key.PENDING_DIRS"
const val KEY_PENDING_MODE = BuildConfig.APPLICATION_ID + ".key.SHOULD_INCLUDE"
}
}

View file

@ -1,64 +0,0 @@
/*
* 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.music.excluded
import android.os.Build
import java.io.File
import org.oxycblt.auxio.music.Dir
import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.logW
object ExcludedDirectories {
private const val VOLUME_PRIMARY_NAME = "primary"
fun fromString(dir: String): Dir.Relative? {
logD("Parse from string $dir")
val split = dir.split(File.pathSeparator, limit = 2)
val volume =
when (split[0]) {
VOLUME_PRIMARY_NAME -> Dir.Volume.Primary
else ->
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
Dir.Volume.Secondary(split[0])
} else {
// While Android Q provides a stable way of accessing volumes, we can't
// trust
// that DATA provides a stable volume scheme on older versions, so external
// volumes are not supported.
logW("Cannot use secondary volumes below Android 10")
return null
}
}
val relativePath = split.getOrNull(1) ?: return null
return Dir.Relative(volume, relativePath)
}
fun toString(dir: Dir.Relative): String {
val volume =
when (dir.volume) {
is Dir.Volume.Primary -> VOLUME_PRIMARY_NAME
is Dir.Volume.Secondary -> dir.volume.name
}
return "${volume}:${dir.relativePath}"
}
}

View file

@ -23,7 +23,6 @@ import com.google.android.exoplayer2.audio.BaseAudioProcessor
import com.google.android.exoplayer2.metadata.Metadata
import com.google.android.exoplayer2.metadata.id3.TextInformationFrame
import com.google.android.exoplayer2.metadata.vorbis.VorbisComment
import java.lang.UnsupportedOperationException
import java.nio.ByteBuffer
import kotlin.math.pow
import org.oxycblt.auxio.music.Album
@ -79,7 +78,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
// ReplayGain is configurable, so determine what to do based off of the mode.
val useAlbumGain =
when (settingsManager.replayGainMode) {
ReplayGainMode.OFF -> throw UnsupportedOperationException()
ReplayGainMode.OFF -> throw IllegalStateException()
// User wants track gain to be preferred. Default to album gain only if
// there is no track gain.
@ -226,8 +225,7 @@ class ReplayGainAudioProcessor : BaseAudioProcessor() {
val buffer = replaceOutputBuffer(size)
if (volume == 1f) {
// No need to apply ReplayGain, do a mem move using put instead of
// a for loop (the latter is not efficient)
// No need to apply ReplayGain.
buffer.put(inputBuffer.slice())
} else {
for (i in position until limit step 2) {

View file

@ -36,6 +36,12 @@ import org.oxycblt.auxio.util.logD
import org.oxycblt.auxio.util.unlikelyToBeNull
/**
* The component managing the [MediaSessionCompat] instance.
*
* I really don't like how I have to do this, but until I can feasibly work with the ExoPlayer queue
* system using something like MediaSessionConnector is more or less impossible.
*
* @author OxygenCobalt
*/
class MediaSessionComponent(private val context: Context, private val player: Player) :
Player.Listener,

View file

@ -31,7 +31,7 @@ import androidx.recyclerview.widget.RecyclerView
import coil.Coil
import org.oxycblt.auxio.R
import org.oxycblt.auxio.home.tabs.TabCustomizeDialog
import org.oxycblt.auxio.music.excluded.ExcludedDialog
import org.oxycblt.auxio.music.dirs.MusicDirsDialog
import org.oxycblt.auxio.playback.PlaybackViewModel
import org.oxycblt.auxio.playback.replaygain.PreAmpCustomizeDialog
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
@ -193,10 +193,10 @@ class SettingsListFragment : PreferenceFragmentCompat() {
true
}
}
SettingsManager.KEY_EXCLUDED -> {
SettingsManager.KEY_MUSIC_DIRS -> {
onPreferenceClickListener =
Preference.OnPreferenceClickListener {
ExcludedDialog().show(childFragmentManager, ExcludedDialog.TAG)
MusicDirsDialog().show(childFragmentManager, MusicDirsDialog.TAG)
true
}
}

View file

@ -23,8 +23,7 @@ import androidx.appcompat.app.AppCompatDelegate
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import org.oxycblt.auxio.home.tabs.Tab
import org.oxycblt.auxio.music.Dir
import org.oxycblt.auxio.music.excluded.ExcludedDirectories
import org.oxycblt.auxio.music.dirs.MusicDirs
import org.oxycblt.auxio.playback.replaygain.ReplayGainMode
import org.oxycblt.auxio.playback.replaygain.ReplayGainPreAmp
import org.oxycblt.auxio.playback.state.PlaybackMode
@ -138,15 +137,22 @@ class SettingsManager private constructor(context: Context) :
val pauseOnRepeat: Boolean
get() = inner.getBoolean(KEY_PAUSE_ON_REPEAT, false)
/** The list of directories excluded from indexing. */
var excludedDirs: List<Dir.Relative>
get() =
(inner.getStringSet(KEY_EXCLUDED, null) ?: emptySet()).mapNotNull(
ExcludedDirectories::fromString)
/** The list of directories that music should be hidden/loaded from. */
var musicDirs: MusicDirs
get() {
val dirs =
(inner.getStringSet(KEY_MUSIC_DIRS, null) ?: emptySet()).mapNotNull(
MusicDirs::parseDir)
return MusicDirs(dirs, inner.getBoolean(KEY_SHOULD_INCLUDE, false))
}
set(value) {
inner.edit {
putStringSet(KEY_EXCLUDED, value.map(ExcludedDirectories::toString).toSet())
apply()
putStringSet(KEY_MUSIC_DIRS, value.dirs.map(MusicDirs::toDir).toSet())
putBoolean(KEY_SHOULD_INCLUDE, value.shouldInclude)
// TODO: This is a stopgap measure before automatic rescanning, remove
commit()
}
}
@ -252,12 +258,12 @@ class SettingsManager private constructor(context: Context) :
init {
inner.registerOnSharedPreferenceChangeListener(this)
if (!inner.contains(KEY_EXCLUDED)) {
if (!inner.contains(KEY_MUSIC_DIRS)) {
logD("Attempting to migrate excluded directories")
// We need to migrate this setting now while we have a context. Note that while
// this does do IO work, the old excluded directory database is so small as to make
// it negligible.
excludedDirs = handleExcludedCompat(context)
musicDirs = MusicDirs(handleExcludedCompat(context), false)
}
}
@ -325,7 +331,8 @@ class SettingsManager private constructor(context: Context) :
const val KEY_SAVE_STATE = "auxio_save_state"
const val KEY_REINDEX = "auxio_reindex"
const val KEY_EXCLUDED = "auxio_excluded_dirs"
const val KEY_MUSIC_DIRS = "auxio_music_dirs"
const val KEY_SHOULD_INCLUDE = "auxio_include_dirs"
const val KEY_SEARCH_FILTER_MODE = "KEY_SEARCH_FILTER"

View file

@ -1,34 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingTop="@dimen/spacing_medium">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/excluded_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="1"
tools:listitem="@layout/item_excluded_dir" />
<TextView
android:id="@+id/excluded_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_medium"
android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_medium"
android:paddingBottom="@dimen/spacing_medium"
android:text="@string/err_no_dirs"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Auxio.TitleMidLarge"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.core.widget.NestedScrollView 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="wrap_content"
android:orientation="vertical"
style="@style/Widget.Auxio.Dialog.NestedScrollView">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/dirs_recycler"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_gravity="center"
android:layout_weight="1"
android:overScrollMode="never"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:itemCount="1"
tools:listitem="@layout/item_music_dir" />
<TextView
android:id="@+id/dirs_empty"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_mid_large"
android:paddingTop="@dimen/spacing_medium"
android:paddingEnd="@dimen/spacing_mid_large"
android:paddingBottom="@dimen/spacing_medium"
android:text="@string/err_no_dirs"
android:textAlignment="center"
android:textAppearance="@style/TextAppearance.Auxio.TitleMidLarge"
android:textColor="?android:attr/textColorSecondary" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/spacing_mid_large"
android:paddingEnd="@dimen/spacing_mid_large"
style="@style/Widget.Auxio.TextView.Header"
android:text="@string/set_dirs_mode" />
<com.google.android.material.divider.MaterialDivider
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/folder_mode_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:layout_marginTop="@dimen/spacing_medium"
android:gravity="center"
app:singleSelection="true"
app:selectionRequired="true"
app:checkedButton="@+id/dirs_mode_exclude">
<Button
android:id="@+id/dirs_mode_exclude"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="@string/set_dirs_mode_exclude"
style="@style/Widget.Auxio.Button.Secondary" />
<Button
android:id="@+id/dirs_mode_include"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="wrap_content"
android:text="@string/set_dirs_mode_include"
style="@style/Widget.Auxio.Button.Secondary" />
</com.google.android.material.button.MaterialButtonToggleGroup>
<TextView
android:id="@+id/dirs_mode_desc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/spacing_mid_large"
android:layout_marginEnd="@dimen/spacing_mid_large"
android:layout_marginTop="@dimen/spacing_small"
tools:text="Mode description" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>

View file

@ -7,7 +7,7 @@
android:layout_height="wrap_content">
<TextView
android:id="@+id/excluded_path"
android:id="@+id/dir_path"
style="@style/Widget.Auxio.TextView.Item.Primary"
android:layout_width="0dp"
android:layout_height="wrap_content"
@ -19,18 +19,18 @@
android:paddingBottom="@dimen/spacing_small"
android:textAppearance="@style/TextAppearance.Auxio.BodyLarge"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/excluded_clear"
app:layout_constraintEnd_toStartOf="@+id/dir_delete"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="/storage/emulated/0/directory" />
<Button
android:id="@+id/excluded_clear"
android:id="@+id/dir_delete"
style="@style/Widget.Auxio.Button.Icon.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/spacing_button_dialog"
android:contentDescription="@string/desc_blacklist_delete"
android:contentDescription="@string/desc_music_dir_delete"
app:icon="@drawable/ic_delete"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"

View file

@ -99,8 +99,6 @@
<string name="set_content">محتوى</string>
<string name="set_save">حفظ حالة التشغيل</string>
<string name="set_save_desc">حفظ حالة التشغيل الحالية الآن</string>
<string name="set_excluded">استبعاد مجلدات</string>
<string name="set_excluded_desc">محتوى المجلدات المستبعدة يتم اخفائها من مكتبتك</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">لم يتم ايجاد موسيقى</string>
@ -126,7 +124,7 @@
<string name="desc_queue_handle">نقل اغنية من الطابور</string>
<string name="desc_tab_handle">تحريك التبويت</string>
<string name="desc_clear_search">إزالة كلمة البحث</string>
<string name="desc_blacklist_delete">إزالة المجلد المستبعد</string>
<string name="desc_music_dir_delete">إزالة المجلد المستبعد</string>
<string name="desc_auxio_icon">ايقونة اوكسيو</string>
<string name="desc_no_cover">غلاف الالبوم</string>

View file

@ -117,8 +117,6 @@
<string name="set_save_desc">Uložit aktuální stav přehrávání</string>
<string name="set_reindex">Znovu načíst hudbu</string>
<string name="set_reindex_desc">Aplikace bude restartována</string>
<string name="set_excluded">Vyloučené složky</string>
<string name="set_excluded_desc">Obsah vyloučených složek je skrytý z vaší knihovny</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Nenalezena žádná hudba</string>
@ -146,7 +144,7 @@
<string name="desc_queue_handle">Přesunout tuto skladbu ve frontě</string>
<string name="desc_tab_handle">Přesunout tuto kartu</string>
<string name="desc_clear_search">Vymazat hledání</string>
<string name="desc_blacklist_delete">Odebrat vyloučený adresář</string>
<string name="desc_music_dir_delete">Odebrat vyloučený adresář</string>
<string name="desc_auxio_icon">Ikona Auxio</string>
<string name="desc_no_cover">Obal alba</string>

View file

@ -95,8 +95,6 @@
<string name="set_save_desc">Den aktuellen Wiedergabezustand speichern</string>
<string name="set_reindex">Musik neu laden</string>
<string name="set_reindex_desc">Startet die App neu</string>
<string name="set_excluded">Ausgeschlossene Ordner</string>
<string name="set_excluded_desc">Die Inhalte der ausgeschlossenen Ordner werden nicht deiner Musikbibliothek angezeigt</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Keine Musik gefunden</string>
@ -168,7 +166,7 @@
<string name="set_repeat_pause_desc">Pausieren, wenn ein Song wiederholt wird</string>
<string name="desc_shuffle">Zufällig an- oder ausschalten</string>
<string name="desc_queue_handle">Lied in der Warteschlange verschieben</string>
<string name="desc_blacklist_delete">Ausgeschlossenes Verzechnis entfernen</string>
<string name="desc_music_dir_delete">Ausgeschlossenes Verzechnis entfernen</string>
<string name="desc_no_cover">Albumcover</string>
<string name="def_playback">Keine Musik wird gespielt</string>
<string name="def_widget_song">Liedname</string>

View file

@ -101,8 +101,6 @@
<string name="set_save_desc">Guardar el estado de reproduccion ahora</string>
<string name="set_reindex">Recargar música</string>
<string name="set_reindex_desc">Se reiniciará la aplicación</string>
<string name="set_excluded">Directorios excluidos</string>
<string name="set_excluded_desc">El contenido de los directorios excluidos no se mostrará</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Sin música</string>
@ -129,7 +127,7 @@
<string name="desc_queue_handle">Mover canción en la cola</string>
<string name="desc_tab_handle">Mover pestaña</string>
<string name="desc_clear_search">Borrar historial de búsqueda</string>
<string name="desc_blacklist_delete">Quitar directorio excluido</string>
<string name="desc_music_dir_delete">Quitar directorio excluido</string>
<string name="desc_auxio_icon">Icono de Auxio</string>
<string name="desc_no_cover">Carátula de álbum</string>

View file

@ -102,8 +102,6 @@
<string name="set_save_desc">Salva lo stato di riproduzione corrente</string>
<string name="set_reindex">Ricarica musica</string>
<string name="set_reindex_desc">L\'applicazione sarà riavviata</string>
<string name="set_excluded">Cartelle escluse</string>
<string name="set_excluded_desc">Il contenuto delle cartelle escluse sarà nascosto dalla tua libreria</string>
<string name="lbl_off">Spento</string>
@ -132,7 +130,7 @@
<string name="desc_queue_handle">Muove questa canzone della coda</string>
<string name="desc_tab_handle">Muove questa scheda</string>
<string name="desc_clear_search">Cancella la query di ricerca</string>
<string name="desc_blacklist_delete">Rimuove cartella esclusa</string>
<string name="desc_music_dir_delete">Rimuove cartella esclusa</string>
<string name="desc_auxio_icon">Icona Auxio</string>
<string name="desc_no_cover">Copertina disco</string>

View file

@ -115,8 +115,6 @@
<string name="set_save_desc">현재 재생 상태를 지금 저장</string>
<string name="set_reindex">음악 다시 불러오기</string>
<string name="set_reindex_desc">앱이 다시 시작됩니다.</string>
<string name="set_excluded">폴더 제외</string>
<string name="set_excluded_desc">제외한 폴더는 라이브러리에서 숨겨집니다.</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">음악 없음</string>
@ -144,7 +142,7 @@
<string name="desc_queue_handle">이 대기열의 음악 이동</string>
<string name="desc_tab_handle">이 탭 이동</string>
<string name="desc_clear_search">검색 기록 삭제</string>
<string name="desc_blacklist_delete">제외한 디렉터리 제거</string>
<string name="desc_music_dir_delete">제외한 디렉터리 제거</string>
<string name="desc_auxio_icon">Auxio 아이콘</string>
<string name="desc_no_cover">앨범 커버</string>

View file

@ -81,8 +81,6 @@
<string name="set_content">Inhoud</string>
<string name="set_save">Afspeelstatus opslaan</string>
<string name="set_save_desc">Sla de huidige afspeelstatus nu op </string>
<string name="set_excluded">Uitgesloten mappen </string>
<string name="set_excluded_desc">De inhoud van uitgesloten mappen wordt verborgen voor uw bibliotheek</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Geen muziek aangetroffen</string>
@ -103,7 +101,7 @@
<string name="desc_change_repeat">Herhaalfunctie wijzigen</string>
<string name="desc_clear_search">Zoekopdracht wissen</string>
<string name="desc_blacklist_delete">Verwijder uitgesloten map</string>
<string name="desc_music_dir_delete">Verwijder uitgesloten map</string>
<string name="desc_auxio_icon">Auxio pictogram</string>
<string name="desc_album_cover">Artist Image voor %s</string>

View file

@ -103,8 +103,6 @@
<string name="set_save_desc">Запоминать позицию в треке</string>
<string name="set_reindex">Перезагрузить музыку</string>
<string name="set_reindex_desc">Это перезапустит приложение</string>
<string name="set_excluded">Исключённые папки</string>
<string name="set_excluded_desc">Содержимое исключённых папок будет скрыто из библиотеки</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">Треков нет</string>
@ -131,7 +129,7 @@
<string name="desc_queue_handle">Переместить трек в очереди</string>
<string name="desc_tab_handle">Переместить вкладку</string>
<string name="desc_clear_search">Очистить поисковый запрос</string>
<string name="desc_blacklist_delete">Удалить исключенную папку</string>
<string name="desc_music_dir_delete">Удалить исключенную папку</string>
<string name="desc_auxio_icon">Иконка Auxio</string>
<string name="desc_no_cover">Обложка альбома</string>

View file

@ -101,8 +101,6 @@
<string name="set_save_desc">立即保存当前播放状态</string>
<string name="set_reindex">重新加载音乐</string>
<string name="set_reindex_desc">将会重启应用</string>
<string name="set_excluded">排除文件夹</string>
<string name="set_excluded_desc">被排除文件夹的内容将从媒体库中隐藏</string>
<string name="lbl_off">关闭</string>
@ -131,7 +129,7 @@
<string name="desc_queue_handle">移动队列曲目</string>
<string name="desc_tab_handle">移动该标签</string>
<string name="desc_clear_search">清除搜索队列</string>
<string name="desc_blacklist_delete">移除排除路径</string>
<string name="desc_music_dir_delete">移除排除路径</string>
<string name="desc_auxio_icon">Auxio 图标</string>
<string name="desc_no_cover">专辑封面</string>

View file

@ -126,8 +126,13 @@
<string name="set_save_desc">Save the current playback state now</string>
<string name="set_reindex">Reload music</string>
<string name="set_reindex_desc">Will restart app</string>
<string name="set_excluded">Excluded folders</string>
<string name="set_excluded_desc">The content of excluded folders is hidden from your library</string>
<string name="set_dirs">Music folders</string>
<string name="set_dirs_desc">Manage where music should be loaded from</string>
<string name="set_dirs_mode">Mode</string>
<string name="set_dirs_mode_exclude">Exclude</string>
<string name="set_dirs_mode_exclude_desc">Music will <b>not</b> be loaded from the folders you add.</string>
<string name="set_dirs_mode_include">Include</string>
<string name="set_dirs_mode_include_desc">Music will <b>only</b> be loaded from the folders you add.</string>
<!-- Error Namespace | Error Labels -->
<string name="err_no_music">No music found</string>
@ -155,7 +160,7 @@
<string name="desc_queue_handle">Move this queue song</string>
<string name="desc_tab_handle">Move this tab</string>
<string name="desc_clear_search">Clear search query</string>
<string name="desc_blacklist_delete">Remove excluded directory</string>
<string name="desc_music_dir_delete">Remove directory</string>
<string name="desc_auxio_icon">Auxio icon</string>
<string name="desc_no_cover">Album cover</string>
@ -176,13 +181,14 @@
<string name="def_widget_artist">Artist Name</string>
<!-- Codec Namespace | Format names -->
<string name="cdc_mp3">MPEG-1 Layer 3 (MP3)</string>
<string name="cdc_ogg">OGG</string>
<string name="cdc_ogg_vorbis">OGG Vorbis</string>
<string name="cdc_ogg_opus">OGG Opus</string>
<string name="cdc_mp3">MPEG-1 Layer 3 </string>
<string name="cdc_mp4">MPEG-4 Audio (AAC)</string>
<string name="cdc_ogg">Ogg</string>
<string name="cdc_ogg_vorbis">Ogg Vorbis</string>
<string name="cdc_ogg_opus">Ogg Opus</string>
<string name="cdc_flac">Free Lossless Audio Codec (FLAC)</string>
<string name="cdc_aac">Advanced Audio Coding (AAC)</string>
<string name="cdc_wav">Microsoft WAV</string>
<string name="cdc_wav">Microsoft WAVE</string>
<string name="cdc_wma">Windows Media Audio (WMA)</string>
<!-- Color Label namespace | Accent names -->

View file

@ -235,6 +235,7 @@
<style name="Widget.Auxio.Button.Secondary" parent="Widget.Material3.Button.OutlinedButton">
<item name="android:textAppearance">@style/TextAppearance.Auxio.LabelLarger</item>
<item name="strokeColor">?attr/colorOutline</item>
</style>
<style name="Widget.Auxio.FloatingActionButton.PlayPause" parent="Widget.Material3.FloatingActionButton.Secondary">

View file

@ -158,9 +158,9 @@
<Preference
app:iconSpaceReserved="false"
app:key="auxio_excluded_dirs"
app:summary="@string/set_excluded_desc"
app:title="@string/set_excluded" />
app:key="auxio_music_dirs"
app:summary="@string/set_dirs_desc"
app:title="@string/set_dirs" />
</PreferenceCategory>
</PreferenceScreen>

View file

@ -1,6 +1,6 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.6.21'
ext.kotlin_version = '1.7.0'
ext.navigation_version = "2.4.2"
repositories {