Skip to content

Commit 0550295

Browse files
committed
search: implement chip pattern prototype
very rough right now, settings are not final at all
1 parent 6accc50 commit 0550295

9 files changed

Lines changed: 198 additions & 114 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@
5050
android:launchMode="singleTask"
5151
android:allowCrossUidActivitySwitchFromBelow="false"
5252
android:roundIcon="@mipmap/ic_launcher"
53-
android:windowSoftInputMode="adjustPan">
53+
android:windowSoftInputMode="adjustNothing">
5454

5555
<intent-filter>
5656
<!-- Expose that we are a music player. -->

app/src/main/java/org/oxycblt/auxio/home/HomeFragment.kt

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import org.oxycblt.auxio.music.PlaylistDecision
6363
import org.oxycblt.auxio.music.PlaylistMessage
6464
import org.oxycblt.auxio.playback.PlaybackDecision
6565
import org.oxycblt.auxio.playback.PlaybackViewModel
66+
import org.oxycblt.auxio.ui.FadingToolbarOffsetListener
6667
import org.oxycblt.auxio.util.collect
6768
import org.oxycblt.auxio.util.collectImmediately
6869
import org.oxycblt.auxio.util.lazyReflectedField
@@ -82,7 +83,7 @@ import timber.log.Timber as L
8283
*/
8384
@AndroidEntryPoint
8485
class HomeFragment :
85-
SelectionFragment<FragmentHomeBinding>(), AppBarLayout.OnOffsetChangedListener {
86+
SelectionFragment<FragmentHomeBinding>() {
8687
override val listModel: ListViewModel by activityViewModels()
8788
override val musicModel: MusicViewModel by activityViewModels()
8889
override val playbackModel: PlaybackViewModel by activityViewModels()
@@ -128,7 +129,12 @@ class HomeFragment :
128129

129130
// --- UI SETUP ---
130131

131-
binding.homeAppbar.addOnOffsetChangedListener(this)
132+
binding.homeAppbar.addOnOffsetChangedListener(
133+
FadingToolbarOffsetListener(
134+
binding.homeToolbar,
135+
binding.homeContent
136+
)
137+
)
132138
binding.homeNormalToolbar.apply {
133139
setOnMenuItemClickListener(this@HomeFragment)
134140
MenuCompat.setGroupDividerEnabled(menu, true)
@@ -187,22 +193,9 @@ class HomeFragment :
187193
override fun onDestroyBinding(binding: FragmentHomeBinding) {
188194
super.onDestroyBinding(binding)
189195
storagePermissionLauncher = null
190-
binding.homeAppbar.removeOnOffsetChangedListener(this)
191196
binding.homeNormalToolbar.setOnMenuItemClickListener(null)
192197
}
193198

194-
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
195-
val binding = requireBinding()
196-
val range = appBarLayout.totalScrollRange
197-
// Fade out the toolbar as the AppBarLayout collapses. To prevent status bar overlap,
198-
// the alpha transition is shifted such that the Toolbar becomes fully transparent
199-
// when the AppBarLayout is only at half-collapsed.
200-
binding.homeToolbar.alpha = 1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
201-
binding.homeContent.updatePadding(
202-
bottom = binding.homeAppbar.totalScrollRange + verticalOffset
203-
)
204-
}
205-
206199
override fun onMenuItemClick(item: MenuItem): Boolean {
207200
if (super.onMenuItemClick(item)) {
208201
return true

app/src/main/java/org/oxycblt/auxio/search/SearchFragment.kt

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ import org.oxycblt.auxio.list.ListViewModel
4444
import org.oxycblt.auxio.list.PlainDivider
4545
import org.oxycblt.auxio.list.PlainHeader
4646
import org.oxycblt.auxio.list.menu.Menu
47+
import org.oxycblt.auxio.music.MusicType
4748
import org.oxycblt.auxio.music.MusicViewModel
4849
import org.oxycblt.auxio.music.PlaylistDecision
4950
import org.oxycblt.auxio.music.PlaylistMessage
5051
import org.oxycblt.auxio.playback.PlaybackDecision
5152
import org.oxycblt.auxio.playback.PlaybackViewModel
53+
import org.oxycblt.auxio.ui.FadingToolbarOffsetListener
5254
import org.oxycblt.auxio.util.collect
5355
import org.oxycblt.auxio.util.collectImmediately
5456
import org.oxycblt.auxio.util.context
@@ -72,7 +74,6 @@ import timber.log.Timber as L
7274
* @author Alexander Capehart (OxygenCobalt)
7375
*
7476
* TODO: Better keyboard management
75-
* TODO: Multi-filtering with chips
7677
*/
7778
@AndroidEntryPoint
7879
class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
@@ -118,10 +119,15 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
118119

119120
// --- UI SETUP ---
120121

122+
binding.searchAppbar.addOnOffsetChangedListener(
123+
FadingToolbarOffsetListener(
124+
binding.searchToolbar,
125+
binding.searchContent
126+
)
127+
)
128+
121129
binding.searchNormalToolbar.apply {
122130
// Initialize the current filtering mode.
123-
menu.findItem(searchModel.getFilterOptionId()).isChecked = true
124-
125131
setNavigationOnClickListener {
126132
// Keyboard is no longer needed.
127133
hideKeyboard()
@@ -145,6 +151,28 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
145151
}
146152
}
147153

154+
val filters = searchModel.filters
155+
binding.searchFilterSongs.isChecked = MusicType.SONGS in filters
156+
binding.searchFilterAlbums.isChecked = MusicType.ALBUMS in filters
157+
binding.searchFilterArtists.isChecked = MusicType.ARTISTS in filters
158+
binding.searchFilterGenres.isChecked = MusicType.GENRES in filters
159+
binding.searchFilterPlaylists.isChecked = MusicType.PLAYLISTS in filters
160+
binding.searchFilters.apply {
161+
setOnCheckedStateChangeListener { group, ids ->
162+
val types = ids.mapNotNullTo(mutableSetOf()) {
163+
when (it) {
164+
R.id.search_filter_songs -> MusicType.SONGS
165+
R.id.search_filter_albums -> MusicType.ALBUMS
166+
R.id.search_filter_artists -> MusicType.ARTISTS
167+
R.id.search_filter_genres -> MusicType.GENRES
168+
R.id.search_filter_playlists -> MusicType.PLAYLISTS
169+
else -> null
170+
}
171+
}
172+
searchModel.filters = types
173+
}
174+
}
175+
148176
binding.searchRecycler.apply {
149177
adapter = searchAdapter
150178
(layoutManager as GridLayoutManager).setFullWidthLookup {
@@ -179,24 +207,6 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
179207
binding.searchRecycler.adapter = null
180208
}
181209

182-
override fun onMenuItemClick(item: MenuItem): Boolean {
183-
if (super.onMenuItemClick(item)) {
184-
return true
185-
}
186-
187-
// Ignore junk sub-menu click events
188-
if (item.itemId != R.id.submenu_filtering) {
189-
// Is a change in filter mode and not just a junk submenu click, update
190-
// the filtering within SearchViewModel.
191-
L.d("Filter mode selected")
192-
item.isChecked = true
193-
searchModel.setFilterOptionId(item.itemId)
194-
return true
195-
}
196-
197-
return false
198-
}
199-
200210
override fun onRealClick(item: Music) {
201211
when (item) {
202212
is Song -> playbackModel.play(item, searchModel.playWith)
@@ -228,6 +238,12 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
228238
// that doesn't seem possible.
229239
L.d("Update finished, scrolling to top")
230240
binding.searchRecycler.scrollToPosition(0)
241+
if (results.isEmpty()) {
242+
// Expand the appbar when we no longer have results.
243+
// This way the user can't softlock themselves by scrolling then
244+
// switching to an empty-result query.
245+
binding.searchAppbar.expandWithScrollingRecycler()
246+
}
231247
}
232248
}
233249

@@ -299,9 +315,11 @@ class SearchFragment : ListFragment<Music, FragmentSearchBinding>() {
299315
if (selected.isNotEmpty()) {
300316
binding.searchSelectionToolbar.title = getString(R.string.fmt_selected, selected.size)
301317
if (binding.searchToolbar.setVisible(R.id.search_selection_toolbar)) {
302-
// New selection started, show the keyboard to make selection easier.
303-
L.d("Significant selection occurred, hiding keyboard")
318+
// New selection started, show the keyboard to make selection easier and
319+
// restore the appbar as well
320+
L.d("Significant selection occurred, hiding keyboard & expanding AppBar")
304321
hideKeyboard()
322+
binding.searchAppbar.expandWithScrollingRecycler()
305323
}
306324
} else {
307325
binding.searchToolbar.setVisible(R.id.search_normal_toolbar)

app/src/main/java/org/oxycblt/auxio/search/SearchSettings.kt

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,27 +32,26 @@ import org.oxycblt.auxio.settings.Settings
3232
* @author Alexander Capehart (OxygenCobalt)
3333
*/
3434
interface SearchSettings : Settings<Nothing> {
35-
/** The type of Music the search view is should filter to. */
36-
var filterTo: MusicType?
35+
/** The type of Music the search view should filter to. */
36+
var filters: Set<MusicType>
3737
}
3838

3939
class SearchSettingsImpl @Inject constructor(@ApplicationContext context: Context) :
4040
Settings.Impl<Nothing>(context), SearchSettings {
41-
override var filterTo: MusicType?
41+
override var filters: Set<MusicType>
4242
get() =
43-
MusicType.fromIntCode(
44-
sharedPreferences.getInt(
45-
getString(R.string.set_key_search_filter_to),
46-
Int.MIN_VALUE,
47-
)
48-
)
43+
sharedPreferences.getStringSet(
44+
getString(R.string.set_key_search_filters),
45+
null
46+
)?.mapNotNull {
47+
it.toIntOrNull()?.let(MusicType::fromIntCode)
48+
}?.toSet() ?: setOf()
4949
set(value) {
5050
sharedPreferences.edit {
51-
putInt(
52-
getString(R.string.set_key_search_filter_to),
53-
value?.intCode ?: Int.MIN_VALUE,
51+
putStringSet(
52+
getString(R.string.set_key_search_filters),
53+
value.map { it.intCode.toString() }.toSet()
5454
)
55-
apply()
5655
}
5756
}
5857
}

app/src/main/java/org/oxycblt/auxio/search/SearchViewModel.kt

Lines changed: 27 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import androidx.annotation.IdRes
2222
import androidx.lifecycle.ViewModel
2323
import androidx.lifecycle.viewModelScope
2424
import dagger.hilt.android.lifecycle.HiltViewModel
25-
import javax.inject.Inject
2625
import kotlinx.coroutines.Job
2726
import kotlinx.coroutines.flow.MutableStateFlow
2827
import kotlinx.coroutines.flow.StateFlow
@@ -34,11 +33,13 @@ import org.oxycblt.auxio.list.Item
3433
import org.oxycblt.auxio.list.PlainDivider
3534
import org.oxycblt.auxio.list.sort.Sort
3635
import org.oxycblt.auxio.music.MusicRepository
36+
import org.oxycblt.auxio.music.MusicSettings
3737
import org.oxycblt.auxio.music.MusicType
3838
import org.oxycblt.auxio.playback.PlaySong
3939
import org.oxycblt.auxio.playback.PlaybackSettings
4040
import org.oxycblt.musikr.Library
4141
import org.oxycblt.musikr.Song
42+
import javax.inject.Inject
4243
import timber.log.Timber as L
4344

4445
/**
@@ -110,29 +111,25 @@ constructor(
110111
}
111112

112113
private suspend fun searchImpl(library: Library, query: String): List<Item> {
113-
val filter = searchSettings.filterTo
114-
115-
val items =
116-
if (filter == null) {
117-
// A nulled filter type means to not filter anything.
118-
L.d("No filter specified, using entire library")
119-
SearchEngine.Items(
120-
library.songs,
121-
library.albums,
122-
library.artists,
123-
library.genres,
124-
library.playlists,
125-
)
126-
} else {
127-
L.d("Filter specified, reducing library")
128-
SearchEngine.Items(
129-
songs = if (filter == MusicType.SONGS) library.songs else null,
130-
albums = if (filter == MusicType.ALBUMS) library.albums else null,
131-
artists = if (filter == MusicType.ARTISTS) library.artists else null,
132-
genres = if (filter == MusicType.GENRES) library.genres else null,
133-
playlists = if (filter == MusicType.PLAYLISTS) library.playlists else null,
134-
)
135-
}
114+
val filters = searchSettings.filters
115+
116+
val items = if (!filters.isEmpty()) {
117+
SearchEngine.Items(
118+
songs = if (MusicType.SONGS in filters) library.songs else null,
119+
albums = if (MusicType.ALBUMS in filters) library.albums else null,
120+
artists = if (MusicType.ARTISTS in filters) library.artists else null,
121+
genres = if (MusicType.GENRES in filters) library.genres else null,
122+
playlists = if (MusicType.PLAYLISTS in filters) library.playlists else null,
123+
)
124+
} else {
125+
SearchEngine.Items(
126+
songs = library.songs,
127+
albums = library.albums,
128+
artists = library.artists,
129+
genres = library.genres,
130+
playlists = library.playlists
131+
)
132+
}
136133

137134
val results = searchEngine.search(items, query)
138135

@@ -186,45 +183,14 @@ constructor(
186183
}
187184
}
188185

189-
/**
190-
* Returns the ID of the filter option to currently highlight.
191-
*
192-
* @return A menu item ID of the filtering option selected.
193-
*/
194-
@IdRes
195-
fun getFilterOptionId() =
196-
when (searchSettings.filterTo) {
197-
MusicType.SONGS -> R.id.option_filter_songs
198-
MusicType.ALBUMS -> R.id.option_filter_albums
199-
MusicType.ARTISTS -> R.id.option_filter_artists
200-
MusicType.GENRES -> R.id.option_filter_genres
201-
MusicType.PLAYLISTS -> R.id.option_filter_playlists
202-
// Null maps to filtering nothing.
203-
null -> R.id.option_filter_all
186+
var filters: Set<MusicType>
187+
get() = searchSettings.filters
188+
set(value) {
189+
// TODO: make this consistent in convention
190+
searchSettings.filters = value
191+
search(lastQuery)
204192
}
205193

206-
/**
207-
* Update the filter type with the newly-selected filter option.
208-
*
209-
* @return A menu item ID of the new filtering option selected.
210-
*/
211-
fun setFilterOptionId(@IdRes id: Int) {
212-
val newFilter =
213-
when (id) {
214-
R.id.option_filter_songs -> MusicType.SONGS
215-
R.id.option_filter_albums -> MusicType.ALBUMS
216-
R.id.option_filter_artists -> MusicType.ARTISTS
217-
R.id.option_filter_genres -> MusicType.GENRES
218-
R.id.option_filter_playlists -> MusicType.PLAYLISTS
219-
// Null maps to filtering nothing.
220-
R.id.option_filter_all -> null
221-
else -> error("Invalid option ID provided")
222-
}
223-
L.d("Updating filter type to $newFilter")
224-
searchSettings.filterTo = newFilter
225-
search(lastQuery)
226-
}
227-
228194
private companion object {
229195
val SORT = Sort(Sort.Mode.ByName, Sort.Direction.ASCENDING)
230196
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package org.oxycblt.auxio.ui
2+
3+
import android.view.View
4+
import androidx.core.view.updatePadding
5+
import com.google.android.material.appbar.AppBarLayout
6+
import kotlin.math.abs
7+
8+
class FadingToolbarOffsetListener(
9+
private val toolbar: View,
10+
private val content: View
11+
) : AppBarLayout.OnOffsetChangedListener {
12+
override fun onOffsetChanged(appBarLayout: AppBarLayout, verticalOffset: Int) {
13+
val range = appBarLayout.totalScrollRange
14+
// Fade out the toolbar as the AppBarLayout collapses. To prevent status bar overlap,
15+
// the alpha transition is shifted such that the Toolbar becomes fully transparent
16+
// when the AppBarLayout is only at half-collapsed.
17+
toolbar.alpha = 1f - (abs(verticalOffset.toFloat()) / (range.toFloat() / 2))
18+
content.updatePadding(bottom = range + verticalOffset)
19+
}
20+
}

0 commit comments

Comments
 (0)