Skip to content

Commit 4269932

Browse files
authored
Merge branch 'MM2-0:main' into main
2 parents 7369574 + 35739bd commit 4269932

File tree

38 files changed

+1094
-557
lines changed

38 files changed

+1094
-557
lines changed

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/app/build.gradle.kts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ android {
3131
minSdk = libs.versions.minSdk.get().toInt()
3232
targetSdk = libs.versions.targetSdk.get().toInt()
3333
@SuppressLint("HighAppVersionCode")
34-
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2024090300
35-
versionName = "1.33.1"
34+
versionCode = System.getenv("VERSION_CODE_OVERRIDE")?.toIntOrNull() ?: 2024120500
35+
versionName = "1.34.0"
3636
signingConfig = signingConfigs.getByName("debug")
3737
}
3838

app/ui/src/main/java/de/mm20/launcher2/ui/assistant/AssistantScaffold.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ fun AssistantScaffold(
149149
val density = LocalDensity.current
150150
val maxSearchBarOffset = with(density) { 128.dp.toPx() }
151151
var searchBarOffset by remember {
152-
mutableStateOf(0f)
152+
mutableFloatStateOf(0f)
153153
}
154154

155155
val nestedScrollConnection = remember {
@@ -161,7 +161,7 @@ fun AssistantScaffold(
161161
}
162162
}
163163
}
164-
val actions by searchVM.searchActionResults
164+
val actions = searchVM.searchActionResults
165165
val webSearchPadding by animateDpAsState(
166166
if (actions.isEmpty()) 0.dp else 48.dp
167167
)
Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
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+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package de.mm20.launcher2.ui.common
2+
3+
import androidx.compose.runtime.mutableStateOf
4+
import androidx.lifecycle.ViewModel
5+
import de.mm20.launcher2.icons.CustomIconWithPreview
6+
import de.mm20.launcher2.icons.IconPack
7+
import de.mm20.launcher2.icons.IconService
8+
import de.mm20.launcher2.search.SavableSearchable
9+
import kotlinx.coroutines.Job
10+
import kotlinx.coroutines.cancelAndJoin
11+
import kotlinx.coroutines.delay
12+
import kotlinx.coroutines.flow.flow
13+
import kotlinx.coroutines.launch
14+
import kotlinx.coroutines.withContext
15+
import org.koin.core.component.KoinComponent
16+
import org.koin.core.component.inject
17+
import kotlin.coroutines.coroutineContext
18+
19+
class IconPickerVM(
20+
private val searchable: SavableSearchable
21+
): KoinComponent {
22+
private val iconService: IconService by inject()
23+
24+
fun getDefaultIcon(size: Int) = flow {
25+
emit(iconService.getUncustomizedDefaultIcon(searchable, size))
26+
}
27+
28+
fun getIconSuggestions(size: Int) = flow {
29+
emit(iconService.getCustomIconSuggestions(searchable, size))
30+
}
31+
32+
val installedIconPacks = iconService.getInstalledIconPacks()
33+
34+
val iconSearchResults = mutableStateOf(emptyList<CustomIconWithPreview>())
35+
val isSearchingIcons = mutableStateOf(false)
36+
37+
38+
private var debounceSearchJob: Job? = null
39+
suspend fun searchIcon(query: String, iconPack: IconPack?) {
40+
debounceSearchJob?.cancelAndJoin()
41+
if (query.isBlank()) {
42+
iconSearchResults.value = emptyList()
43+
isSearchingIcons.value = false
44+
return
45+
}
46+
withContext(coroutineContext) {
47+
debounceSearchJob = launch {
48+
delay(500)
49+
isSearchingIcons.value = true
50+
iconSearchResults.value = emptyList()
51+
iconSearchResults.value = iconService.searchCustomIcons(query, iconPack)
52+
isSearchingIcons.value = false
53+
}
54+
}
55+
}
56+
}

0 commit comments

Comments
 (0)