Skip to content

Commit 791d0e9

Browse files
dmitthedazedDmytro
andauthored
feat: add grid view to assistant selector, toggle for hiding the title in bottom sheet (#82)
Co-authored-by: Dmytro <dimka@example.com>
1 parent 5e74b31 commit 791d0e9

19 files changed

Lines changed: 763 additions & 11 deletions

app/src/main/java/com/wstxda/switchai/fragment/SettingsFragment.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,10 @@ class SettingsFragment : PreferenceFragmentCompat() {
6868
findPreference<MultiSelectListPreference>(Constants.ASSISTANT_MANAGER_MANUAL_PREF_KEY)?.isVisible =
6969
!isDynamic
7070
}
71+
assistantSelectorViewModel.isGridViewEnabled.observe(this) { isGrid ->
72+
findPreference<ListPreference>(Constants.ASSISTANT_GRID_COLUMNS_PREF_KEY)?.isVisible = isGrid
73+
findPreference<ListPreference>(Constants.ASSISTANT_GRID_COLUMNS_LAND_PREF_KEY)?.isVisible = isGrid
74+
}
7175
}
7276

7377
override fun onResume() {

app/src/main/java/com/wstxda/switchai/ui/adapter/AssistantSelectorAdapter.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import android.view.ViewGroup
55
import androidx.recyclerview.widget.ListAdapter
66
import androidx.recyclerview.widget.RecyclerView
77
import com.wstxda.switchai.databinding.ListItemAssistantCategoryBinding
8+
import com.wstxda.switchai.databinding.ListItemAssistantGridViewBinding
89
import com.wstxda.switchai.databinding.ListItemAssistantViewBinding
910
import com.wstxda.switchai.databinding.ListItemReorderTipBinding
1011
import com.wstxda.switchai.ui.viewholder.AssistantSelectorCategoryViewHolder
12+
import com.wstxda.switchai.ui.viewholder.AssistantSelectorGridItemViewHolder
1113
import com.wstxda.switchai.ui.viewholder.AssistantSelectorItemViewHolder
1214
import com.wstxda.switchai.ui.viewholder.ReorderTipViewHolder
1315
import com.wstxda.switchai.utils.Constants
@@ -17,13 +19,15 @@ class AssistantSelectorAdapter(
1719
private val onAssistantClicked: (String) -> Unit,
1820
private val onPinClicked: (String) -> Unit,
1921
private val onDismissTipClicked: () -> Unit,
22+
val isGridMode: Boolean = false,
2023
) : ListAdapter<AssistantSelectorRecyclerView, RecyclerView.ViewHolder>(
2124
AssistantSelectorDiffCallback()
2225
) {
2326

2427
override fun getItemViewType(position: Int) = when (getItem(position)) {
2528
is AssistantSelectorRecyclerView.CategoryHeader -> Constants.VIEW_TYPE_CATEGORY_HEADER
26-
is AssistantSelectorRecyclerView.AssistantSelector -> Constants.VIEW_TYPE_ASSISTANT_ITEM
29+
is AssistantSelectorRecyclerView.AssistantSelector ->
30+
if (isGridMode) Constants.VIEW_TYPE_ASSISTANT_GRID_ITEM else Constants.VIEW_TYPE_ASSISTANT_ITEM
2731
is AssistantSelectorRecyclerView.ReorderTip -> Constants.VIEW_TYPE_REORDER_TIP
2832
}
2933

@@ -42,6 +46,13 @@ class AssistantSelectorAdapter(
4246
AssistantSelectorItemViewHolder(binding)
4347
}
4448

49+
Constants.VIEW_TYPE_ASSISTANT_GRID_ITEM -> {
50+
val binding = ListItemAssistantGridViewBinding.inflate(
51+
LayoutInflater.from(parent.context), parent, false
52+
)
53+
AssistantSelectorGridItemViewHolder(binding)
54+
}
55+
4556
Constants.VIEW_TYPE_REORDER_TIP -> {
4657
val binding = ListItemReorderTipBinding.inflate(
4758
LayoutInflater.from(parent.context), parent, false
@@ -55,15 +66,17 @@ class AssistantSelectorAdapter(
5566
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, pos: Int) {
5667
when (val item = getItem(pos)) {
5768
is AssistantSelectorRecyclerView.CategoryHeader -> (holder as AssistantSelectorCategoryViewHolder).bind(
58-
item
69+
item, isGridMode
5970
)
6071

61-
is AssistantSelectorRecyclerView.AssistantSelector -> (holder as AssistantSelectorItemViewHolder).bind(
62-
item, onAssistantClicked, onPinClicked
63-
)
72+
is AssistantSelectorRecyclerView.AssistantSelector -> when (holder) {
73+
is AssistantSelectorItemViewHolder -> holder.bind(item, onAssistantClicked, onPinClicked)
74+
is AssistantSelectorGridItemViewHolder -> holder.bind(item, onAssistantClicked, onPinClicked)
75+
else -> Unit
76+
}
6477

6578
is AssistantSelectorRecyclerView.ReorderTip -> (holder as ReorderTipViewHolder).bind(
66-
onDismissTipClicked
79+
onDismissTipClicked, isGridMode
6780
)
6881
}
6982
}

app/src/main/java/com/wstxda/switchai/ui/component/AssistantSelectorBottomSheet.kt

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import androidx.core.view.isInvisible
1111
import androidx.core.view.isVisible
1212
import androidx.core.widget.doOnTextChanged
1313
import androidx.fragment.app.viewModels
14+
import androidx.recyclerview.widget.GridLayoutManager
1415
import androidx.recyclerview.widget.ItemTouchHelper
1516
import androidx.recyclerview.widget.LinearLayoutManager
1617
import androidx.recyclerview.widget.RecyclerView
@@ -42,12 +43,19 @@ class AssistantSelectorBottomSheet : BaseBottomSheet<FragmentAssistantDialogBind
4243

4344
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
4445
super.onViewCreated(view, savedInstanceState)
46+
setupTitle()
4547
setupRecyclerView()
4648
setupObservers()
4749
setupSearch()
4850
setupReorder()
4951
}
5052

53+
private fun setupTitle() {
54+
val isTitleVisible =
55+
preferenceHelper.getBoolean(Constants.ASSISTANT_SELECTOR_TITLE_PREF_KEY, true)
56+
titleTextView.isVisible = isTitleVisible
57+
}
58+
5159
private fun setupSearch() {
5260
val isSearchBarEnabled =
5361
preferenceHelper.getBoolean(Constants.ASSISTANT_SEARCH_BAR_PREF_KEY, true)
@@ -62,6 +70,7 @@ class AssistantSelectorBottomSheet : BaseBottomSheet<FragmentAssistantDialogBind
6270
}
6371

6472
private fun setupRecyclerView() {
73+
val isGridMode = preferenceHelper.getBoolean(Constants.ASSISTANT_GRID_VIEW_PREF_KEY, false)
6574
assistantSelectorAdapter = AssistantSelectorAdapter(onAssistantClicked = { assistantKey ->
6675
openAssistant(assistantKey)
6776
viewModel.updateRecentlyUsedAssistants(assistantKey)
@@ -70,9 +79,51 @@ class AssistantSelectorBottomSheet : BaseBottomSheet<FragmentAssistantDialogBind
7079
viewModel.togglePinAssistant(assistantKey)
7180
}, onDismissTipClicked = {
7281
viewModel.dismissReorderTip()
73-
})
82+
}, isGridMode = isGridMode)
7483
binding.assistantsRecyclerView.apply {
75-
layoutManager = LinearLayoutManager(context)
84+
if (isGridMode) {
85+
val isLandscape = resources.configuration.orientation ==
86+
android.content.res.Configuration.ORIENTATION_LANDSCAPE
87+
val userPref = if (isLandscape) {
88+
preferenceHelper.getString(
89+
Constants.ASSISTANT_GRID_COLUMNS_LAND_PREF_KEY, "0"
90+
)?.toIntOrNull() ?: 0
91+
} else {
92+
preferenceHelper.getString(
93+
Constants.ASSISTANT_GRID_COLUMNS_PREF_KEY, "0"
94+
)?.toIntOrNull() ?: 0
95+
}
96+
val autoSpan = run {
97+
val isLowDensity = resources.displayMetrics.densityDpi <=
98+
android.util.DisplayMetrics.DENSITY_MEDIUM
99+
when {
100+
isLandscape && isLowDensity -> 3
101+
isLandscape -> 4
102+
isLowDensity -> 2
103+
else -> 3
104+
}
105+
}
106+
val spanCount = if (userPref > 0) userPref else autoSpan
107+
val gridLayoutManager = GridLayoutManager(context, spanCount)
108+
gridLayoutManager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
109+
override fun getSpanSize(position: Int): Int {
110+
return when (assistantSelectorAdapter.getItemViewType(position)) {
111+
Constants.VIEW_TYPE_ASSISTANT_GRID_ITEM -> 1
112+
else -> spanCount
113+
}
114+
}
115+
}
116+
val tv = android.util.TypedValue()
117+
context.theme.resolveAttribute(android.R.attr.dialogPreferredPadding, tv, true)
118+
val dialogPadding = android.util.TypedValue.complexToDimensionPixelSize(tv.data, resources.displayMetrics)
119+
val itemMargin = (3 * resources.displayMetrics.density).toInt()
120+
val horizontalPadding = dialogPadding - itemMargin
121+
setPadding(horizontalPadding, 0, horizontalPadding, paddingBottom)
122+
scrollBarStyle = View.SCROLLBARS_OUTSIDE_INSET
123+
layoutManager = gridLayoutManager
124+
} else {
125+
layoutManager = LinearLayoutManager(context)
126+
}
76127
adapter = assistantSelectorAdapter
77128
}
78129
}
@@ -120,7 +171,9 @@ class AssistantSelectorBottomSheet : BaseBottomSheet<FragmentAssistantDialogBind
120171
private class PinnedItemReorderCallback(
121172
private val adapter: AssistantSelectorAdapter,
122173
private val onReorderFinished: (List<AssistantItem>) -> Unit
123-
) : ItemTouchHelper.SimpleCallback(ItemTouchHelper.UP or ItemTouchHelper.DOWN, 0) {
174+
) : ItemTouchHelper.SimpleCallback(
175+
ItemTouchHelper.UP or ItemTouchHelper.DOWN or ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT, 0
176+
) {
124177

125178
override fun getDragDirs(
126179
recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder

app/src/main/java/com/wstxda/switchai/ui/viewholder/AssistantSelectorCategoryViewHolder.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,13 @@ class AssistantSelectorCategoryViewHolder(
88
private val binding: ListItemAssistantCategoryBinding
99
) : RecyclerView.ViewHolder(binding.root) {
1010

11-
fun bind(categoryHeader: AssistantSelectorRecyclerView.CategoryHeader) {
11+
fun bind(categoryHeader: AssistantSelectorRecyclerView.CategoryHeader, isGridMode: Boolean = false) {
1212
binding.categoryTitleTextView.text = categoryHeader.title
1313
binding.categoryCountChip.text = categoryHeader.count.toString()
14+
val density = binding.root.resources.displayMetrics.density
15+
val startPadding = ((if (isGridMode) 8 else 40) * density).toInt()
16+
val endPadding = ((if (isGridMode) 8 else 28) * density).toInt()
17+
val topPadding = (12 * density).toInt()
18+
binding.root.setPadding(startPadding, topPadding, endPadding, 0)
1419
}
1520
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.wstxda.switchai.ui.viewholder
2+
3+
import android.view.View
4+
import androidx.recyclerview.widget.RecyclerView
5+
import com.wstxda.switchai.R
6+
import com.wstxda.switchai.databinding.ListItemAssistantGridViewBinding
7+
import com.wstxda.switchai.ui.adapter.AssistantSelectorRecyclerView
8+
import com.wstxda.switchai.ui.utils.VibrationService.buttonVibration
9+
10+
class AssistantSelectorGridItemViewHolder(
11+
private val binding: ListItemAssistantGridViewBinding,
12+
) : RecyclerView.ViewHolder(binding.root) {
13+
14+
fun bind(
15+
wrapper: AssistantSelectorRecyclerView.AssistantSelector,
16+
onAssistantClicked: (String) -> Unit,
17+
onPinClicked: (String) -> Unit,
18+
) {
19+
val item = wrapper.assistantItem
20+
binding.assistantCheckedTextView.text = item.name
21+
22+
val isItemEnabled = item.isInstalled
23+
binding.pinButton.visibility = if (isItemEnabled) View.VISIBLE else View.GONE
24+
25+
binding.assistantIcon.setImageResource(
26+
if (item.iconRes != 0) item.iconRes else R.drawable.ic_assistant
27+
)
28+
29+
binding.pinButton.setIconResource(
30+
if (item.isPinned) R.drawable.ic_pin_filled else R.drawable.ic_pin_outline
31+
)
32+
33+
binding.pinButton.setOnClickListener {
34+
onPinClicked(item.key)
35+
it.context.buttonVibration()
36+
}
37+
38+
itemView.setOnClickListener {
39+
onAssistantClicked(item.key)
40+
}
41+
}
42+
}

app/src/main/java/com/wstxda/switchai/ui/viewholder/ReorderTipViewHolder.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,15 @@ class ReorderTipViewHolder(
77
private val binding: ListItemReorderTipBinding
88
) : RecyclerView.ViewHolder(binding.root) {
99

10-
fun bind(onDismissClicked: () -> Unit) {
10+
fun bind(onDismissClicked: () -> Unit, isGridMode: Boolean = false) {
1111
binding.dismissButton.setOnClickListener {
1212
onDismissClicked()
1313
}
14+
if (isGridMode) {
15+
val lp = binding.root.layoutParams as? android.view.ViewGroup.MarginLayoutParams
16+
lp?.marginStart = 0
17+
lp?.marginEnd = 0
18+
binding.root.layoutParams = lp
19+
}
1420
}
1521
}

app/src/main/java/com/wstxda/switchai/utils/Constants.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ object Constants {
88
const val DIGITAL_ASSISTANT_SELECT_PREF_KEY = "digital_assistant_select"
99
const val ASSISTANT_SELECTOR_DIALOG_PREF_KEY = "assistant_selector_dialog"
1010
const val ASSISTANT_SEARCH_BAR_PREF_KEY = "assistant_search_bar"
11+
const val ASSISTANT_GRID_VIEW_PREF_KEY = "assistant_grid_view"
12+
const val ASSISTANT_GRID_COLUMNS_PREF_KEY = "assistant_grid_columns"
13+
const val ASSISTANT_GRID_COLUMNS_LAND_PREF_KEY = "assistant_grid_columns_land"
14+
const val ASSISTANT_SELECTOR_TITLE_PREF_KEY = "assistant_selector_title"
1115
const val ASSISTANT_MANAGER_MANUAL_PREF_KEY = "assistant_manager_manual"
1216
const val ASSISTANT_MANAGER_DYNAMIC_PREF_KEY = "assistant_manager_dynamic"
1317
const val OPEN_ASSISTANT_TILE_PREF_KEY = "open_assistant_tile"
@@ -51,6 +55,8 @@ object Constants {
5155
const val VIEW_TYPE_CATEGORY_HEADER = 0
5256
const val VIEW_TYPE_ASSISTANT_ITEM = 1
5357
const val VIEW_TYPE_REORDER_TIP = 2
58+
const val VIEW_TYPE_ASSISTANT_GRID_ITEM = 3
59+
const val GRID_SPAN_COUNT = 3
5460
// Maximum number of recently used assistants
5561
const val CAT_MAX_RECENTLY_USED = 3
5662
// Category for AssistantSelectorBottomSheet

app/src/main/java/com/wstxda/switchai/viewmodel/AssistantSelectorViewModel.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
4444
private val _isDynamicModeEnabled = MutableLiveData<Boolean>()
4545
val isDynamicModeEnabled: LiveData<Boolean> = _isDynamicModeEnabled
4646

47+
private val _isGridViewEnabled = MutableLiveData<Boolean>()
48+
val isGridViewEnabled: LiveData<Boolean> = _isGridViewEnabled
49+
4750
private val pinnedAssistantKeys = mutableListOf<String>()
4851
private val recentlyUsedAssistants = mutableListOf<Pair<String, Long>>()
4952

@@ -77,9 +80,15 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
7780
Constants.ASSISTANT_MANAGER_DYNAMIC_PREF_KEY, false
7881
)
7982

83+
private val isGridView: Boolean
84+
get() = defaultSharedPreferences.getBoolean(
85+
Constants.ASSISTANT_GRID_VIEW_PREF_KEY, false
86+
)
87+
8088
init {
8189
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(this)
8290
_isDynamicModeEnabled.value = isDynamicMode
91+
_isGridViewEnabled.value = isGridView
8392

8493
val intentFilter = IntentFilter().apply {
8594
addAction(Intent.ACTION_PACKAGE_ADDED)
@@ -347,6 +356,7 @@ class AssistantSelectorViewModel(application: Application) : AndroidViewModel(ap
347356
_isDynamicModeEnabled.value = isDynamicMode
348357
loadAssistants()
349358
}
359+
Constants.ASSISTANT_GRID_VIEW_PREF_KEY -> _isGridViewEnabled.value = isGridView
350360
}
351361
}
352362

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
2+
android:width="24dp"
3+
android:height="24dp"
4+
android:viewportWidth="24"
5+
android:viewportHeight="24">
6+
<path
7+
android:fillColor="@android:color/white"
8+
android:pathData="M3,3h7v7H3zM14,3h7v7h-7zM3,14h7v7H3zM14,14h7v7h-7z" />
9+
</vector>
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
2+
xmlns:app="http://schemas.android.com/apk/res-auto"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
style="?attr/materialCardViewFilledStyle"
5+
android:layout_width="match_parent"
6+
android:layout_height="wrap_content"
7+
android:layout_margin="2dp"
8+
android:clickable="true"
9+
android:focusable="false"
10+
app:cardBackgroundColor="?attr/colorSecondaryContainer"
11+
app:cardCornerRadius="16dp">
12+
13+
<FrameLayout
14+
android:layout_width="match_parent"
15+
android:layout_height="wrap_content">
16+
17+
<LinearLayout
18+
android:layout_width="match_parent"
19+
android:layout_height="wrap_content"
20+
android:gravity="center_horizontal"
21+
android:orientation="vertical"
22+
android:paddingTop="10dp"
23+
android:paddingBottom="8dp"
24+
android:paddingHorizontal="2dp">
25+
26+
<com.google.android.material.imageview.ShapeableImageView
27+
android:id="@+id/assistant_icon"
28+
android:layout_width="32dp"
29+
android:layout_height="40dp"
30+
android:tint="?attr/colorSecondary"
31+
tools:src="@drawable/ic_assistant_chatgpt" />
32+
33+
<com.google.android.material.textview.MaterialTextView
34+
android:id="@+id/assistant_checked_text_view"
35+
android:layout_width="match_parent"
36+
android:layout_height="wrap_content"
37+
android:layout_marginTop="4dp"
38+
android:ellipsize="end"
39+
android:maxLines="2"
40+
android:textAlignment="center"
41+
android:textAppearance="?attr/textAppearanceLabelSmall"
42+
android:textColor="?attr/colorOnSecondaryContainer"
43+
tools:text="ChatGPT" />
44+
45+
</LinearLayout>
46+
47+
<FrameLayout
48+
android:id="@+id/pin_button_container"
49+
android:layout_width="32dp"
50+
android:layout_height="32dp"
51+
android:layout_gravity="top|end"
52+
android:layout_margin="2dp">
53+
54+
<com.google.android.material.button.MaterialButton
55+
android:id="@+id/pin_button"
56+
style="@style/Widget.Material3Expressive.Button.IconButton.Tonal"
57+
android:layout_width="26dp"
58+
android:layout_height="26dp"
59+
android:layout_gravity="center"
60+
android:gravity="center"
61+
android:insetLeft="0dp"
62+
android:insetTop="0dp"
63+
android:insetRight="0dp"
64+
android:insetBottom="0dp"
65+
app:backgroundTint="?attr/colorPrimaryInverse"
66+
app:iconGravity="textStart"
67+
app:iconPadding="0dp"
68+
app:iconSize="14dp"
69+
app:iconTint="?attr/colorOnPrimaryContainer"
70+
tools:icon="@drawable/ic_pin_filled" />
71+
72+
</FrameLayout>
73+
74+
</FrameLayout>
75+
76+
</com.google.android.material.card.MaterialCardView>

0 commit comments

Comments
 (0)