1
+ package de.mm20.launcher2.ui.common
2
+
3
+ import android.content.pm.PackageManager
4
+ import androidx.compose.animation.animateContentSize
5
+ import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.PaddingValues
7
+ import androidx.compose.foundation.layout.fillMaxSize
8
+ import androidx.compose.foundation.layout.padding
9
+ import androidx.compose.foundation.layout.size
10
+ import androidx.compose.foundation.layout.wrapContentWidth
11
+ import androidx.compose.foundation.lazy.grid.GridCells
12
+ import androidx.compose.foundation.lazy.grid.GridItemSpan
13
+ import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
14
+ import androidx.compose.foundation.lazy.grid.items
15
+ import androidx.compose.material.icons.Icons
16
+ import androidx.compose.material.icons.rounded.ArrowDropDown
17
+ import androidx.compose.material.icons.rounded.FilterAlt
18
+ import androidx.compose.material.icons.rounded.Search
19
+ import androidx.compose.material3.Button
20
+ import androidx.compose.material3.ButtonDefaults
21
+ import androidx.compose.material3.CircularProgressIndicator
22
+ import androidx.compose.material3.DropdownMenu
23
+ import androidx.compose.material3.DropdownMenuItem
24
+ import androidx.compose.material3.Icon
25
+ import androidx.compose.material3.OutlinedTextField
26
+ import androidx.compose.material3.SearchBar
27
+ import androidx.compose.material3.SearchBarDefaults
28
+ import androidx.compose.material3.Text
29
+ import androidx.compose.runtime.Composable
30
+ import androidx.compose.runtime.collectAsState
31
+ import androidx.compose.runtime.getValue
32
+ import androidx.compose.runtime.mutableStateOf
33
+ import androidx.compose.runtime.remember
34
+ import androidx.compose.runtime.rememberCoroutineScope
35
+ import androidx.compose.runtime.setValue
36
+ import androidx.compose.ui.Alignment
37
+ import androidx.compose.ui.Modifier
38
+ import androidx.compose.ui.platform.LocalContext
39
+ import androidx.compose.ui.res.stringResource
40
+ import androidx.compose.ui.unit.dp
41
+ import coil.compose.AsyncImage
42
+ import de.mm20.launcher2.data.customattrs.CustomIcon
43
+ import de.mm20.launcher2.icons.IconPack
44
+ import de.mm20.launcher2.search.SavableSearchable
45
+ import de.mm20.launcher2.ui.R
46
+ import de.mm20.launcher2.ui.ktx.toPixels
47
+ import de.mm20.launcher2.ui.launcher.sheets.IconPreview
48
+ import de.mm20.launcher2.ui.launcher.sheets.Separator
49
+ import de.mm20.launcher2.ui.locals.LocalGridSettings
50
+ import kotlinx.coroutines.launch
51
+
52
+ @Composable
53
+ fun IconPicker (
54
+ searchable : SavableSearchable ,
55
+ onSelect : (CustomIcon ? ) -> Unit ,
56
+ contentPadding : PaddingValues = PaddingValues (0.dp)
57
+ ) {
58
+ val iconSize = 48 .dp
59
+ val iconSizePx = iconSize.toPixels()
60
+
61
+ val context = LocalContext .current
62
+
63
+ val scope = rememberCoroutineScope()
64
+
65
+ val viewModel: IconPickerVM =
66
+ remember(searchable.key) { IconPickerVM (searchable) }
67
+
68
+ val suggestions by remember { viewModel.getIconSuggestions(iconSizePx.toInt()) }
69
+ .collectAsState(emptyList())
70
+
71
+ val defaultIcon by remember {
72
+ viewModel.getDefaultIcon(iconSizePx.toInt())
73
+ }.collectAsState(null )
74
+
75
+ var query by remember { mutableStateOf(" " ) }
76
+ var filterIconPack by remember { mutableStateOf<IconPack ?>(null ) }
77
+ val isSearching by viewModel.isSearchingIcons
78
+ val iconResults by viewModel.iconSearchResults
79
+
80
+ var showIconPackFilter by remember { mutableStateOf(false ) }
81
+ val installedIconPacks by viewModel.installedIconPacks.collectAsState(null )
82
+ val noPacksInstalled = installedIconPacks?.isEmpty() == true
83
+
84
+ val columns = LocalGridSettings .current.columnCount
85
+
86
+ LazyVerticalGrid (
87
+ modifier = Modifier .fillMaxSize(),
88
+ columns = GridCells .Fixed (columns),
89
+ contentPadding = contentPadding,
90
+ ) {
91
+
92
+ item(span = { GridItemSpan (columns) }) {
93
+ SearchBar (
94
+ modifier = Modifier .padding(bottom = 16 .dp),
95
+ expanded = false ,
96
+ onExpandedChange = {},
97
+ inputField = {
98
+ SearchBarDefaults .InputField (
99
+ enabled = ! noPacksInstalled,
100
+ leadingIcon = {
101
+ Icon (
102
+ imageVector = Icons .Rounded .Search ,
103
+ contentDescription = null
104
+ )
105
+ },
106
+ onSearch = {},
107
+ expanded = false ,
108
+ onExpandedChange = {},
109
+ placeholder = {
110
+ Text (
111
+ stringResource(
112
+ if (noPacksInstalled) R .string.icon_picker_no_packs_installed else R .string.icon_picker_search_icon
113
+ )
114
+ )
115
+ },
116
+ query = query,
117
+ onQueryChange = {
118
+ query = it
119
+ scope.launch {
120
+ viewModel.searchIcon(query, filterIconPack)
121
+ }
122
+ },
123
+ )
124
+ }
125
+ ) {
126
+
127
+ }
128
+ }
129
+
130
+ if (query.isEmpty()) {
131
+ if (defaultIcon != null ) {
132
+ item(span = { GridItemSpan (columns) }) {
133
+ Separator (stringResource(R .string.icon_picker_default_icon))
134
+ }
135
+ item {
136
+ IconPreview (item = defaultIcon, iconSize = iconSize, onClick = {
137
+ onSelect(null )
138
+ })
139
+ }
140
+ }
141
+ item(span = { GridItemSpan (columns) }) {
142
+ Separator (stringResource(R .string.icon_picker_suggestions))
143
+ }
144
+
145
+ if (suggestions.isNotEmpty()) {
146
+ items(suggestions) {
147
+ IconPreview (
148
+ it,
149
+ iconSize,
150
+ onClick = { onSelect(it.customIcon) }
151
+ )
152
+ }
153
+ }
154
+ } else {
155
+
156
+ if (! installedIconPacks.isNullOrEmpty()) {
157
+ item(
158
+ span = { GridItemSpan (columns) },
159
+ ) {
160
+ Button (
161
+ onClick = { showIconPackFilter = ! showIconPackFilter },
162
+ modifier = Modifier
163
+ .wrapContentWidth(align = Alignment .CenterHorizontally )
164
+ .padding(bottom = 16 .dp),
165
+ contentPadding = PaddingValues (
166
+ horizontal = 16 .dp,
167
+ vertical = 8 .dp
168
+ )
169
+ ) {
170
+ if (filterIconPack == null ) {
171
+ Icon (
172
+ modifier = Modifier
173
+ .padding(end = ButtonDefaults .IconSpacing )
174
+ .size(ButtonDefaults .IconSize ),
175
+ imageVector = Icons .Rounded .FilterAlt ,
176
+ contentDescription = null
177
+ )
178
+ } else {
179
+ val icon = remember(filterIconPack?.packageName) {
180
+ try {
181
+ filterIconPack?.packageName?.let { pkg ->
182
+ context.packageManager.getApplicationIcon(pkg)
183
+ }
184
+ } catch (e: PackageManager .NameNotFoundException ) {
185
+ null
186
+ }
187
+ }
188
+ AsyncImage (
189
+ modifier = Modifier
190
+ .padding(end = ButtonDefaults .IconSpacing )
191
+ .size(ButtonDefaults .IconSize ),
192
+ model = icon,
193
+ contentDescription = null
194
+ )
195
+ }
196
+ DropdownMenu (
197
+ expanded = showIconPackFilter,
198
+ onDismissRequest = { showIconPackFilter = false }) {
199
+ DropdownMenuItem (
200
+ text = { Text (stringResource(id = R .string.icon_picker_filter_all_packs)) },
201
+ onClick = {
202
+ showIconPackFilter = false
203
+ filterIconPack = null
204
+ scope.launch {
205
+ viewModel.searchIcon(query, filterIconPack)
206
+ }
207
+ }
208
+ )
209
+ installedIconPacks?.forEach { iconPack ->
210
+ DropdownMenuItem (
211
+ onClick = {
212
+ showIconPackFilter = false
213
+ filterIconPack = iconPack
214
+ scope.launch {
215
+ viewModel.searchIcon(query, filterIconPack)
216
+ }
217
+ },
218
+ text = {
219
+ Text (iconPack.name)
220
+ })
221
+ }
222
+ }
223
+ Text (
224
+ text = filterIconPack?.name
225
+ ? : stringResource(id = R .string.icon_picker_filter_all_packs),
226
+ modifier = Modifier .animateContentSize()
227
+ )
228
+ Icon (
229
+ Icons .Rounded .ArrowDropDown ,
230
+ modifier = Modifier
231
+ .padding(start = ButtonDefaults .IconSpacing )
232
+ .size(ButtonDefaults .IconSize ),
233
+ contentDescription = null
234
+ )
235
+ }
236
+ }
237
+ }
238
+
239
+ items(iconResults) {
240
+ IconPreview (
241
+ it,
242
+ iconSize,
243
+ onClick = { onSelect(it.customIcon) }
244
+ )
245
+ }
246
+
247
+ if (isSearching) {
248
+ item(span = { GridItemSpan (columns) }) {
249
+ Box (
250
+ contentAlignment = Alignment .Center
251
+ ) {
252
+ CircularProgressIndicator (
253
+ modifier = Modifier
254
+ .padding(12 .dp)
255
+ .size(24 .dp)
256
+ )
257
+ }
258
+ }
259
+ }
260
+ }
261
+
262
+ }
263
+ }
0 commit comments