From 32c82d74c3670f6e37683622af32f3c1c32a683b Mon Sep 17 00:00:00 2001 From: MM20 <15646950+MM2-0@users.noreply.github.com> Date: Tue, 26 Jul 2022 17:20:34 +0200 Subject: [PATCH] Implement basic icon picker --- .../customattrs/CustomAttributesRepository.kt | 19 ++- .../mm20/launcher2/database/CustomAttrsDao.kt | 11 +- i18n/src/main/res/values/strings.xml | 3 + icons/build.gradle.kts | 2 +- .../de/mm20/launcher2/icons/IconRepository.kt | 31 ++++- .../ui/launcher/search/apps/AppItem.kt | 16 +++ .../customattrs/CustomizeSearchableSheet.kt | 130 ++++++++++++++++++ .../customattrs/CustomizeSearchableSheetVM.kt | 43 ++++++ 8 files changed, 244 insertions(+), 11 deletions(-) create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt create mode 100644 ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt diff --git a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt index b0aa18fa..b4b3491a 100644 --- a/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt +++ b/customattrs/src/main/java/de/mm20/launcher2/customattrs/CustomAttributesRepository.kt @@ -2,21 +2,38 @@ package de.mm20.launcher2.customattrs import de.mm20.launcher2.database.AppDatabase import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch interface CustomAttributesRepository { fun getCustomIcon(searchable: Searchable): Flow + fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) } internal class CustomAttributesRepositoryImpl( private val appDatabase: AppDatabase, ) : CustomAttributesRepository { + private val scope = CoroutineScope(Job() + Dispatchers.Default) + override fun getCustomIcon(searchable: Searchable): Flow { val dao = appDatabase.customAttrsDao() - return dao.getCustomIcon(searchable.key) + return dao.getCustomAttribute(searchable.key, CustomAttributeType.Icon.value) .map { CustomAttribute.fromDatabaseEntity(it) as? CustomIcon } } + + override fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) { + val dao = appDatabase.customAttrsDao() + scope.launch { + dao.clearCustomAttribute(searchable.key, CustomAttributeType.Icon.value) + if (icon != null) { + dao.setCustomAttribute(icon.toDatabaseEntity(searchable.key)) + } + } + } } \ No newline at end of file diff --git a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt index 6c9def2d..419f661b 100644 --- a/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt +++ b/database/src/main/java/de/mm20/launcher2/database/CustomAttrsDao.kt @@ -1,12 +1,19 @@ package de.mm20.launcher2.database import androidx.room.Dao +import androidx.room.Insert import androidx.room.Query import de.mm20.launcher2.database.entities.CustomAttributeEntity import kotlinx.coroutines.flow.Flow @Dao interface CustomAttrsDao { - @Query("SELECT * FROM CustomAttributes WHERE type = 'icon' AND key = :key LIMIT 1") - fun getCustomIcon(key: String) : Flow + @Query("SELECT * FROM CustomAttributes WHERE type = :type AND key = :key LIMIT 1") + fun getCustomAttribute(key: String, type: String) : Flow + + @Query("DELETE FROM CustomAttributes WHERE type = :type AND key = :key") + fun clearCustomAttribute(key: String, type: String) + + @Insert + fun setCustomAttribute(entity: CustomAttributeEntity) } \ No newline at end of file diff --git a/i18n/src/main/res/values/strings.xml b/i18n/src/main/res/values/strings.xml index 8057ad62..7401ad9e 100644 --- a/i18n/src/main/res/values/strings.xml +++ b/i18n/src/main/res/values/strings.xml @@ -17,6 +17,7 @@ App info Launch + Customize Open Hide @@ -619,4 +620,6 @@ Created %1$s on %2$s with %3$s. Select what to restore. Existing data will be overwritten! The backup has been restored. + + Pick icon \ No newline at end of file diff --git a/icons/build.gradle.kts b/icons/build.gradle.kts index ea116fb4..a475d2be 100644 --- a/icons/build.gradle.kts +++ b/icons/build.gradle.kts @@ -53,6 +53,6 @@ dependencies { implementation(project(":search")) implementation(project(":applications")) implementation(project(":crashreporter")) - implementation(project(":customattrs")) + api(project(":customattrs")) } \ No newline at end of file diff --git a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt index e7e918e5..f9df413f 100644 --- a/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt +++ b/icons/src/main/java/de/mm20/launcher2/icons/IconRepository.kt @@ -175,15 +175,24 @@ class IconRepository( val defaultTransformedIcon = applyTransformations(rawIcon, defaultTransformations) - suggestions.add(CustomIconSuggestion( - defaultTransformedIcon, - null, - )) + suggestions.add( + CustomIconSuggestion( + defaultTransformedIcon, + null, + ) + ) if (rawIcon is StaticLauncherIcon && rawIcon.backgroundLayer is TransparentLayer) { val adaptifyOptions = listOf( + // Legacy icons that simply fill the entire canvas AdaptifiedLegacyIcon( - fgScale = 1.25f, + fgScale = 1f, + bgColor = 1 + ), + // 48x48 with 5px padding used to be the default icon size for icons generated by + // the Android Studio asset generator. Upscale these icons to remove that padding. + AdaptifiedLegacyIcon( + fgScale = 48f / 38f, bgColor = 1 ), AdaptifiedLegacyIcon( @@ -197,7 +206,8 @@ class IconRepository( ) suggestions.addAll( adaptifyOptions.mapNotNull { - val transformation = getTransformations(it)?.firstOrNull() ?: return@mapNotNull null + val transformation = + getTransformations(it)?.firstOrNull() ?: return@mapNotNull null CustomIconSuggestion( icon = transformation.transform(rawIcon), data = it, @@ -211,7 +221,10 @@ class IconRepository( } - private suspend fun applyTransformations(icon: LauncherIcon, transformations: List): LauncherIcon { + private suspend fun applyTransformations( + icon: LauncherIcon, + transformations: List + ): LauncherIcon { var icon = icon if (icon is StaticLauncherIcon) { for (transformation in transformations) { @@ -221,6 +234,10 @@ class IconRepository( return icon } + fun setCustomIcon(searchable: Searchable, icon: CustomIcon?) { + customAttributesRepository.setCustomIcon(searchable, icon) + } + } data class CustomIconSuggestion( diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt index 857b8712..4cd3719c 100644 --- a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/apps/AppItem.kt @@ -28,6 +28,7 @@ import de.mm20.launcher2.ui.R import de.mm20.launcher2.ui.component.* import de.mm20.launcher2.ui.ktx.toDp import de.mm20.launcher2.ui.ktx.toPixels +import de.mm20.launcher2.ui.launcher.search.common.customattrs.CustomizeSearchableSheet import de.mm20.launcher2.ui.locals.LocalFavoritesEnabled import de.mm20.launcher2.ui.locals.LocalGridIconSize import de.mm20.launcher2.ui.locals.LocalSnackbarHostState @@ -48,6 +49,8 @@ fun AppItem( val lifecycleOwner = LocalLifecycleOwner.current val snackbarHostState = LocalSnackbarHostState.current + var edit by remember { mutableStateOf(false) } + val scope = rememberCoroutineScope() Column( modifier = modifier @@ -217,6 +220,12 @@ fun AppItem( ) ) + toolbarActions.add(DefaultToolbarAction( + label = stringResource(R.string.menu_customize), + icon = Icons.Rounded.Edit, + action = { edit = true } + )) + val storeDetails = remember(app) { app.getStoreDetails(context) } val shareAction = if (storeDetails == null) { DefaultToolbarAction( @@ -308,6 +317,13 @@ fun AppItem( rightActions = toolbarActions ) } + + if (edit) { + CustomizeSearchableSheet( + searchable = app, + onDismiss = { edit = false } + ) + } } @OptIn(ExperimentalAnimationApi::class) diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt new file mode 100644 index 00000000..2a27e52e --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheet.kt @@ -0,0 +1,130 @@ +package de.mm20.launcher2.ui.launcher.search.common.customattrs + +import android.graphics.drawable.InsetDrawable +import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.badges.Badge +import de.mm20.launcher2.search.data.Searchable +import de.mm20.launcher2.ui.R +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.ShapedLauncherIcon +import de.mm20.launcher2.ui.ktx.toPixels +import de.mm20.launcher2.ui.locals.LocalGridColumns + +@Composable +fun CustomizeSearchableSheet( + searchable: Searchable, + onDismiss: () -> Unit, +) { + val viewModel: CustomizeSearchableSheetVM = + remember(searchable) { CustomizeSearchableSheetVM(searchable) } + val context = LocalContext.current + + val pickIcon by viewModel.isIconPickerOpen.observeAsState(false) + + BottomSheetDialog( + onDismissRequest = onDismiss, + title = { + Text(stringResource(if (pickIcon) R.string.icon_picker_title else R.string.menu_customize)) + }, + confirmButton = { + if (pickIcon) { + OutlinedButton(onClick = { viewModel.closeIconPicker() }) { + Text(stringResource(id = android.R.string.cancel)) + } + } else { + OutlinedButton(onClick = onDismiss) { + Text(stringResource(id = R.string.close)) + } + } + } + ) { + if (!pickIcon) { + Column( + modifier = Modifier + .padding(top = 8.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + + val iconSize = 64.dp + val iconSizePx = iconSize.toPixels() + val icon by remember { viewModel.getIcon(iconSizePx.toInt()) }.collectAsState(null) + val primaryColor = MaterialTheme.colorScheme.onSecondary + val badgeDrawable = remember { + InsetDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_edit), + 8 + ).also { + it.setTint(primaryColor.toArgb()) + } + } + + ShapedLauncherIcon( + size = iconSize, + icon = icon, + badge = Badge( + icon = badgeDrawable + ), + onClick = { + viewModel.openIconPicker() + } + ) + OutlinedTextField( + modifier = + Modifier + .fillMaxWidth() + .padding(top = 24.dp), + value = "", + onValueChange = {}, + placeholder = { + Text(searchable.label) + }, + ) + } + } else { + val iconSize = 48.dp + val iconSizePx = iconSize.toPixels() + val suggestions by + remember { viewModel.getIconSuggestions(iconSizePx.toInt()) } + .observeAsState(emptyList()) + LazyVerticalGrid(columns = GridCells.Fixed(LocalGridColumns.current)) { + items(suggestions) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.padding(vertical = 8.dp) + ) { + ShapedLauncherIcon( + size = iconSize, + icon = it.icon, + onClick = { + viewModel.pickIcon(it.data) + } + ) + } + } + } + } + } + +} \ No newline at end of file diff --git a/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt new file mode 100644 index 00000000..53e6e02e --- /dev/null +++ b/ui/src/main/java/de/mm20/launcher2/ui/launcher/search/common/customattrs/CustomizeSearchableSheetVM.kt @@ -0,0 +1,43 @@ +package de.mm20.launcher2.ui.launcher.search.common.customattrs + +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.liveData +import de.mm20.launcher2.customattrs.CustomIcon +import de.mm20.launcher2.icons.CustomIconSuggestion +import de.mm20.launcher2.icons.IconRepository +import de.mm20.launcher2.icons.LauncherIcon +import de.mm20.launcher2.search.data.Searchable +import kotlinx.coroutines.flow.Flow +import org.koin.androidx.compose.inject +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +class CustomizeSearchableSheetVM( + private val searchable: Searchable +) : KoinComponent { + private val iconRepository: IconRepository by inject() + + val isIconPickerOpen = MutableLiveData(false) + + fun getIcon(size: Int): Flow { + return iconRepository.getIcon(searchable, size) + } + + fun getIconSuggestions(size: Int) = liveData { + emit(iconRepository.getCustomIconSuggestions(searchable, size)) + } + + fun openIconPicker() { + isIconPickerOpen.value = true + } + + fun closeIconPicker() { + isIconPickerOpen.value = false + } + + fun pickIcon(icon: CustomIcon?) { + iconRepository.setCustomIcon(searchable, icon) + closeIconPicker() + } +} \ No newline at end of file