Skip to content

Commit 1ad9df3

Browse files
committed
music: share code between music/excluded locations
Co-authored by Claude.
1 parent 15c1c23 commit 1ad9df3

18 files changed

Lines changed: 249 additions & 345 deletions

File tree

app/src/main/java/org/oxycblt/auxio/music/MusicSettings.kt

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import javax.inject.Inject
2727
import org.oxycblt.auxio.R
2828
import org.oxycblt.auxio.settings.Settings
2929
import org.oxycblt.musikr.fs.Location
30-
import org.oxycblt.musikr.fs.OpenedLocation
3130
import timber.log.Timber as L
3231

3332
/**
@@ -39,9 +38,9 @@ interface MusicSettings : Settings<MusicSettings.Listener> {
3938
/** The current library revision. */
4039
var revision: UUID?
4140
/** The locations of music to load. */
42-
var musicLocations: List<OpenedLocation>
41+
var musicLocations: List<Location.Opened>
4342
/** The locations to exclude from music loading. */
44-
var excludedLocations: List<Location>
43+
var excludedLocations: List<Location.Unopened>
4544
/** Whether to exclude non-music audio files from the music library. */
4645
val excludeNonMusic: Boolean
4746
/** Whether to ignore hidden files and directories during music loading. */
@@ -78,7 +77,7 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
7877
}
7978
}
8079

81-
override var musicLocations: List<OpenedLocation>
80+
override var musicLocations: List<Location.Opened>
8281
get() {
8382
val locations =
8483
sharedPreferences.getString(getString(R.string.set_key_music_locations), null)
@@ -95,12 +94,12 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
9594
}
9695
}
9796

98-
override var excludedLocations: List<Location>
97+
override var excludedLocations: List<Location.Unopened>
9998
get() {
10099
val locations =
101100
sharedPreferences.getString(getString(R.string.set_key_excluded_locations), null)
102101
?: return emptyList()
103-
return locations.toLocations()
102+
return locations.toUnopenedLocations()
104103
}
105104
set(value) {
106105
sharedPreferences.edit {
@@ -157,17 +156,17 @@ class MusicSettingsImpl @Inject constructor(@ApplicationContext private val cont
157156
}
158157
}
159158

160-
private fun List<OpenedLocation>.stringify(): String =
159+
private fun List<Location.Opened>.stringify(): String =
161160
joinToString(separator = ";") { it.uri.toString().replace(";", "\\;") }
162161

163-
private fun String.toOpenedLocations(): List<OpenedLocation> =
164-
splitEscaped { it == ';' }.mapNotNull { Location.from(context, it.toUri())?.open(context) }
162+
private fun String.toOpenedLocations(): List<Location.Opened> =
163+
splitEscaped { it == ';' }.mapNotNull { Location.Unopened.from(context, it.toUri())?.open(context) }
165164

166165
private fun List<Location>.stringifyLocations(): String =
167166
joinToString(separator = ";") { it.uri.toString().replace(";", "\\;") }
168167

169-
private fun String.toLocations(): List<Location> =
170-
splitEscaped { it == ';' }.mapNotNull { Location.from(context, it.toUri()) }
168+
private fun String.toUnopenedLocations(): List<Location.Unopened> =
169+
splitEscaped { it == ';' }.mapNotNull { Location.Unopened.from(context, it.toUri()) }
171170

172171
private inline fun String.splitEscaped(selector: (Char) -> Boolean): List<String> {
173172
val split = mutableListOf<String>()

app/src/main/java/org/oxycblt/auxio/music/locations/ExcludedLocationAdapter.kt

Lines changed: 9 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -18,98 +18,29 @@
1818

1919
package org.oxycblt.auxio.music.locations
2020

21-
import android.view.View
2221
import android.view.ViewGroup
23-
import androidx.recyclerview.widget.RecyclerView
2422
import org.oxycblt.auxio.databinding.ItemMusicLocationBinding
25-
import org.oxycblt.auxio.list.recycler.DialogRecyclerView
26-
import org.oxycblt.auxio.util.context
2723
import org.oxycblt.auxio.util.inflater
2824
import org.oxycblt.musikr.fs.Location
29-
import timber.log.Timber as L
3025

3126
/**
32-
* [RecyclerView.Adapter] that manages a list of [Location] excluded directory instances.
27+
* [LocationAdapter] that manages a list of [Location.Unopened] excluded directory instances.
3328
*
34-
* @param listener A [ExcludedLocationAdapter.Listener] to bind interactions to.
29+
* @param listener A [LocationAdapter.Listener] to bind interactions to.
3530
* @author Alexander Capehart (OxygenCobalt)
3631
*/
37-
class ExcludedLocationAdapter(private val listener: Listener) : RecyclerView.Adapter<ExcludedDirViewHolder>() {
38-
private val _locations = mutableListOf<Location>()
39-
/**
40-
* The current list of [Location]s, may not line up with [Location]s due to
41-
* removals.
42-
*/
43-
val locations: List<Location> = _locations
44-
45-
override fun getItemCount() = locations.size
46-
47-
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =
48-
ExcludedDirViewHolder.from(parent)
49-
50-
override fun onBindViewHolder(holder: ExcludedDirViewHolder, position: Int) =
51-
holder.bind(locations[position], listener)
52-
53-
/**
54-
* Add a [Location] to the end of the list.
55-
*
56-
* @param location The [Location] to add.
57-
*/
58-
fun add(location: Location) {
59-
if (_locations.contains(location)) return
60-
L.d("Adding $location")
61-
_locations.add(location)
62-
notifyItemInserted(_locations.lastIndex)
63-
}
64-
65-
/**
66-
* Add a list of [Location] instances to the end of the list.
67-
*
68-
* @param locations The [Location] instances to add.
69-
*/
70-
fun addAll(locations: List<Location>) {
71-
L.d("Adding ${locations.size} locations")
72-
val oldLastIndex = _locations.size
73-
_locations.addAll(locations)
74-
notifyItemRangeInserted(oldLastIndex, locations.size)
75-
}
76-
77-
/**
78-
* Remove a [Location] from the list.
79-
*
80-
* @param location The [Location] to remove. Must exist in the list.
81-
*/
82-
fun remove(location: Location) {
83-
L.d("Removing $location")
84-
val idx = _locations.indexOf(location)
85-
_locations.removeAt(idx)
86-
notifyItemRemoved(idx)
87-
}
88-
89-
/** A Listener for [ExcludedLocationAdapter] interactions. */
90-
interface Listener {
91-
/** Called when the delete button on a directory item is clicked. */
92-
fun onRemoveLocation(location: Location)
93-
}
32+
class ExcludedLocationAdapter(listener: LocationAdapter.Listener<Location.Unopened>) : LocationAdapter<Location.Unopened>(listener) {
33+
override fun createViewHolder(parent: ViewGroup): LocationViewHolder<Location.Unopened> =
34+
ExcludedLocationViewHolder.from(parent)
9435
}
9536

9637
/**
97-
* A [RecyclerView.Recycler] that displays a [Location]. Use [from] to create an instance.
38+
* A [LocationViewHolder] that displays a [Location.Unopened]. Use [from] to create an instance.
9839
*
9940
* @author Alexander Capehart (OxygenCobalt)
10041
*/
101-
class ExcludedDirViewHolder private constructor(private val binding: ItemMusicLocationBinding) :
102-
DialogRecyclerView.ViewHolder(binding.root) {
103-
/**
104-
* Bind new data to this instance.
105-
*
106-
* @param location The new [Location] to bind.
107-
* @param listener A [ExcludedLocationAdapter.Listener] to bind interactions to.
108-
*/
109-
fun bind(location: Location, listener: ExcludedLocationAdapter.Listener) {
110-
binding.locationPath.text = location.path.resolve(binding.context)
111-
binding.locationDelete.setOnClickListener { listener.onRemoveLocation(location) }
112-
}
42+
class ExcludedLocationViewHolder private constructor(binding: ItemMusicLocationBinding) :
43+
LocationViewHolder<Location.Unopened>(binding) {
11344

11445
companion object {
11546
/**
@@ -119,6 +50,6 @@ class ExcludedDirViewHolder private constructor(private val binding: ItemMusicLo
11950
* @return A new instance.
12051
*/
12152
fun from(parent: ViewGroup) =
122-
ExcludedDirViewHolder(ItemMusicLocationBinding.inflate(parent.context.inflater, parent, false))
53+
ExcludedLocationViewHolder(ItemMusicLocationBinding.inflate(parent.context.inflater, parent, false))
12354
}
12455
}

app/src/main/java/org/oxycblt/auxio/music/locations/ExcludedLocationsDialog.kt

Lines changed: 16 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -18,127 +18,46 @@
1818

1919
package org.oxycblt.auxio.music.locations
2020

21-
import android.content.ActivityNotFoundException
21+
import android.content.Context
2222
import android.net.Uri
23-
import android.os.Bundle
24-
import android.view.LayoutInflater
25-
import androidx.activity.result.ActivityResultLauncher
26-
import androidx.activity.result.contract.ActivityResultContracts
27-
import androidx.appcompat.app.AlertDialog
28-
import androidx.core.net.toUri
29-
import androidx.recyclerview.widget.ConcatAdapter
3023
import dagger.hilt.android.AndroidEntryPoint
31-
import javax.inject.Inject
3224
import org.oxycblt.auxio.BuildConfig
3325
import org.oxycblt.auxio.R
34-
import org.oxycblt.auxio.databinding.DialogMusicLocationsBinding
3526
import org.oxycblt.auxio.music.MusicSettings
36-
import org.oxycblt.auxio.ui.ViewBindingMaterialDialogFragment
37-
import org.oxycblt.auxio.util.showToast
3827
import org.oxycblt.musikr.fs.Location
39-
import timber.log.Timber as L
28+
import javax.inject.Inject
4029

4130
/**
4231
* Dialog that manages the excluded locations setting.
4332
*
4433
* @author Alexander Capehart (OxygenCobalt)
4534
*/
4635
@AndroidEntryPoint
47-
class ExcludedLocationsDialog :
48-
ViewBindingMaterialDialogFragment<DialogMusicLocationsBinding>(),
49-
ExcludedLocationAdapter.Listener,
50-
NewLocationFooterAdapter.Listener {
51-
private val locationAdapter = ExcludedLocationAdapter(this)
52-
private val locationFooterAdapter = NewLocationFooterAdapter(this)
53-
private var openDocumentTreeLauncher: ActivityResultLauncher<Uri?>? = null
54-
@Inject lateinit var musicSettings: MusicSettings
55-
56-
override fun onCreateBinding(inflater: LayoutInflater) =
57-
DialogMusicLocationsBinding.inflate(inflater)
58-
59-
override fun onConfigDialog(builder: AlertDialog.Builder) {
60-
builder
61-
.setTitle(R.string.set_excluded_locations)
62-
.setNegativeButton(R.string.lbl_cancel, null)
63-
.setPositiveButton(R.string.lbl_save) { _, _ ->
64-
val newDirs = locationAdapter.locations
65-
musicSettings.excludedLocations = newDirs
66-
}
67-
}
68-
69-
override fun onBindingCreated(
70-
binding: DialogMusicLocationsBinding,
71-
savedInstanceState: Bundle?
72-
) {
73-
openDocumentTreeLauncher =
74-
registerForActivityResult(
75-
ActivityResultContracts.OpenDocumentTree(), ::addDocumentTreeUriToDirs)
36+
class ExcludedLocationsDialog : LocationsDialog<Location.Unopened>() {
37+
override val locationAdapter = ExcludedLocationAdapter(this)
7638

77-
binding.locationsRecycler.apply {
78-
adapter = ConcatAdapter(locationAdapter, locationFooterAdapter)
79-
itemAnimator = null
80-
}
39+
@Inject
40+
override lateinit var musicSettings: MusicSettings
8141

82-
val locations =
83-
savedInstanceState?.getStringArrayList(KEY_PENDING_LOCATIONS)?.mapNotNull {
84-
Location.from(requireContext(), it.toUri())
85-
} ?: musicSettings.excludedLocations
42+
override fun getDialogTitle(): Int = R.string.set_excluded_locations
8643

87-
locationAdapter.addAll(locations)
88-
}
89-
90-
override fun onSaveInstanceState(outState: Bundle) {
91-
super.onSaveInstanceState(outState)
92-
outState.putStringArrayList(
93-
KEY_PENDING_LOCATIONS, ArrayList(locationAdapter.locations.map { it.uri.toString() }))
94-
}
44+
override fun getCurrentLocations(): List<Location.Unopened> = musicSettings.excludedLocations
9545

96-
override fun onDestroyBinding(binding: DialogMusicLocationsBinding) {
97-
super.onDestroyBinding(binding)
98-
openDocumentTreeLauncher = null
99-
binding.locationsRecycler.adapter = null
46+
override fun saveLocations(locations: List<Location.Unopened>) {
47+
musicSettings.excludedLocations = locations
10048
}
10149

102-
override fun onRemoveLocation(location: Location) {
103-
locationAdapter.remove(location)
104-
}
105-
106-
override fun onNewLocation() {
107-
L.d("Opening launcher")
108-
val launcher =
109-
requireNotNull(openDocumentTreeLauncher) { "Document tree launcher was not available" }
50+
override fun getPendingLocationsKey(): String = KEY_PENDING_EXCLUDED_LOCATIONS
11051

111-
try {
112-
launcher.launch(null)
113-
} catch (e: ActivityNotFoundException) {
114-
// User doesn't have a capable file manager.
115-
requireContext().showToast(R.string.err_no_app)
116-
}
52+
override fun convertUriToLocation(uri: Uri): Location.Unopened? {
53+
return Location.Unopened.from(requireContext(), uri)
11754
}
11855

119-
/**
120-
* Add a Document Tree [Uri] chosen by the user to the current [Location]s.
121-
*
122-
* @param uri The document tree [Uri] to add, chosen by the user. Will do nothing if the [Uri]
123-
* is null or not valid.
124-
*/
125-
private fun addDocumentTreeUriToDirs(uri: Uri?) {
126-
if (uri == null) {
127-
// A null URI means that the user left the file picker without picking a directory
128-
L.d("No URI given (user closed the dialog)")
129-
return
130-
}
131-
val context = requireContext()
132-
val location = Location.from(context, uri)
133-
134-
if (location != null) {
135-
locationAdapter.add(location)
136-
} else {
137-
requireContext().showToast(R.string.err_bad_location)
138-
}
56+
override fun createLocationFromUri(context: Context, uri: Uri): Location.Unopened? {
57+
return Location.Unopened.from(context, uri)
13958
}
14059

14160
private companion object {
142-
const val KEY_PENDING_LOCATIONS = BuildConfig.APPLICATION_ID + ".key.PENDING_EXCLUDED_LOCATIONS"
61+
const val KEY_PENDING_EXCLUDED_LOCATIONS = BuildConfig.APPLICATION_ID + ".key.PENDING_EXCLUDED_LOCATIONS"
14362
}
14463
}

0 commit comments

Comments
 (0)