diff --git a/UIViews/build.gradle.kts b/UIViews/build.gradle.kts index 5208e6d62..2aee4e85c 100644 --- a/UIViews/build.gradle.kts +++ b/UIViews/build.gradle.kts @@ -132,13 +132,15 @@ dependencies { implementation(libs.material.kolor) - implementation("com.vanniktech:blurhash:0.4.0-SNAPSHOT") + implementation(libs.blurhash) ksp(libs.roomCompiler) - implementation("androidx.biometric:biometric:1.4.0-alpha02") + implementation(libs.biometric) implementation(projects.gemini) + implementation(libs.reorderable) + //TODO: Use this to check recomposition count on every screen //implementation("io.github.theapache64:rebugger:1.0.0-rc03") } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt index da0ff4a48..1fff2d238 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/BaseMainActivity.kt @@ -128,6 +128,7 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.programmersbox.extensionloader.SourceRepository import com.programmersbox.favoritesdatabase.ItemDatabase +import com.programmersbox.favoritesdatabase.SourceOrder import com.programmersbox.gemini.GeminiRecommendationScreen import com.programmersbox.helpfulutils.notificationManager import com.programmersbox.sharedutils.AppLogo @@ -152,6 +153,7 @@ import com.programmersbox.uiviews.settings.MoreSettingsScreen import com.programmersbox.uiviews.settings.NotificationSettings import com.programmersbox.uiviews.settings.PlaySettings import com.programmersbox.uiviews.settings.SettingScreen +import com.programmersbox.uiviews.settings.SourceOrderScreen import com.programmersbox.uiviews.utils.ChromeCustomTabsNavigator import com.programmersbox.uiviews.utils.LocalNavHostPadding import com.programmersbox.uiviews.utils.LocalWindowSizeClass @@ -228,6 +230,21 @@ abstract class BaseMainActivity : AppCompatActivity() { } .launchIn(lifecycleScope) + sourceRepository.sources + .onEach { + val itemDao = itemDatabase.itemDao() + it.forEachIndexed { index, sourceInformation -> + itemDao.insertSourceOrder( + SourceOrder( + source = sourceInformation.packageName, + name = sourceInformation.apiService.serviceName, + order = index + ) + ) + } + } + .launchIn(lifecycleScope) + setContent { navController = rememberNavController( remember { ChromeCustomTabsNavigator(this) } @@ -754,10 +771,16 @@ abstract class BaseMainActivity : AppCompatActivity() { otherClick = { navController.navigate(Screen.OtherSettings) }, moreInfoClick = { navController.navigate(Screen.MoreInfoSettings) }, moreSettingsClick = { navController.navigate(Screen.MoreSettings) }, - geminiClick = { navController.navigate(Screen.GeminiScreen) } + geminiClick = { navController.navigate(Screen.GeminiScreen) }, + sourcesOrderClick = { navController.navigate(Screen.OrderScreen) } ) } + composable( + enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, + exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) }, + ) { SourceOrderScreen() } + composable( enterTransition = { slideIntoContainer(AnimatedContentTransitionScope.SlideDirection.Start) }, exitTransition = { slideOutOfContainer(AnimatedContentTransitionScope.SlideDirection.End) }, diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt index b4d722b35..9c70e9d64 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsUtils.kt @@ -495,10 +495,7 @@ fun DetailFloatingActionButtonMenu( ) FloatingActionButtonMenuItem( - onClick = { - fabMenuExpanded = false - onFavoriteClick(isFavorite) - }, + onClick = { onFavoriteClick(isFavorite) }, icon = { Icon( if (isFavorite) Icons.Default.Favorite else Icons.Default.FavoriteBorder, @@ -510,10 +507,7 @@ fun DetailFloatingActionButtonMenu( if (isFavorite && LocalContext.current.shouldCheckFlow.collectAsStateWithLifecycle(initialValue = true).value) { FloatingActionButtonMenuItem( - onClick = { - fabMenuExpanded = false - notifyAction() - }, + onClick = notifyAction, icon = { Icon( if (canNotify) Icons.Default.NotificationsActive else Icons.Default.NotificationsOff, diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt index 86da1cc3e..c6d5fb03f 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/details/DetailsView.kt @@ -79,7 +79,6 @@ import com.programmersbox.uiviews.utils.LocalSettingsHandling import com.programmersbox.uiviews.utils.NotificationLogo import com.programmersbox.uiviews.utils.components.OtakuScaffold import com.programmersbox.uiviews.utils.components.ToolTipWrapper -import com.programmersbox.uiviews.utils.components.minus import com.programmersbox.uiviews.utils.isScrollingUp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze @@ -344,7 +343,7 @@ fun DetailsView( .nestedScroll(collapsableBehavior.nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection) ) { p -> - val modifiedPaddingValues = p - LocalNavHostPadding.current + val modifiedPaddingValues = p// - LocalNavHostPadding.current var descriptionVisibility by remember { mutableStateOf(false) } val listOfChapters = remember(reverseChapters) { info.chapters.let { if (reverseChapters) it.reversed() else it } diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt index 23aa2cdae..f5a0c1718 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/recent/RecentViewModel.kt @@ -20,6 +20,7 @@ import com.programmersbox.sharedutils.FirebaseDb import com.programmersbox.uiviews.CurrentSourceRepository import com.programmersbox.uiviews.utils.DefaultToastItems import com.programmersbox.uiviews.utils.ToastItems +import com.programmersbox.uiviews.utils.combineSources import com.programmersbox.uiviews.utils.dispatchIo import com.programmersbox.uiviews.utils.recordFirebaseException import kotlinx.coroutines.Dispatchers @@ -61,7 +62,7 @@ class RecentViewModel( val sources = mutableStateListOf() init { - sourceRepository.sources + combineSources(sourceRepository, dao) .onEach { sources.clear() sources.addAll(it) diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt index 59a3c8c04..8ed73d72f 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SettingsFragment.kt @@ -24,6 +24,7 @@ import androidx.compose.material.icons.filled.Notifications import androidx.compose.material.icons.filled.OpenInBrowser import androidx.compose.material.icons.filled.PhoneAndroid import androidx.compose.material.icons.filled.PlayCircleOutline +import androidx.compose.material.icons.filled.Reorder import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Source @@ -56,6 +57,7 @@ import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.LocalLifecycleOwner +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.SubcomposeAsyncImage import coil.compose.SubcomposeAsyncImageContent @@ -88,6 +90,7 @@ import com.programmersbox.uiviews.utils.components.OtakuScaffold import com.programmersbox.uiviews.utils.currentService import com.programmersbox.uiviews.utils.showSourceChooser import com.programmersbox.uiviews.utils.showTranslationScreen +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch import org.koin.compose.koinInject import java.util.Locale @@ -128,6 +131,7 @@ fun SettingScreen( moreInfoClick: () -> Unit = {}, moreSettingsClick: () -> Unit = {}, geminiClick: () -> Unit = {}, + sourcesOrderClick: () -> Unit = {}, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) @@ -165,7 +169,8 @@ fun SettingScreen( otherClick = otherClick, moreInfoClick = moreInfoClick, moreSettingsClick = moreSettingsClick, - geminiClick = geminiClick + geminiClick = geminiClick, + sourcesOrderClick = sourcesOrderClick ) } } @@ -302,6 +307,7 @@ private fun SettingsScreen( moreInfoClick: () -> Unit, moreSettingsClick: () -> Unit, geminiClick: () -> Unit, + sourcesOrderClick: () -> Unit, ) { val uriHandler = LocalUriHandler.current val source by LocalCurrentSource.current.asFlow().collectAsState(initial = null) @@ -391,6 +397,16 @@ private fun SettingsScreen( ) { showSourceChooser = true } ) + PreferenceSetting( + settingTitle = { Text("Sources Order") }, + settingIcon = { Icon(Icons.Default.Reorder, null, modifier = Modifier.fillMaxSize()) }, + modifier = Modifier.clickable( + indication = ripple(), + interactionSource = null, + onClick = sourcesOrderClick + ) + ) + PreferenceSetting( settingTitle = { Text(stringResource(R.string.view_extensions)) }, settingIcon = { Icon(Icons.Default.Extension, null, modifier = Modifier.fillMaxSize()) }, @@ -506,7 +522,8 @@ private fun SettingsPreview() { otherClick = {}, moreInfoClick = {}, moreSettingsClick = {}, - geminiClick = {} + geminiClick = {}, + sourcesOrderClick = {} ) } } @@ -588,11 +605,23 @@ fun SourceChooserScreen( val context = LocalContext.current val sourceRepository = LocalSourcesRepository.current val currentSourceRepository = LocalCurrentSource.current + val itemDao = LocalItemDao.current ListBottomScreen( includeInsetPadding = true, title = stringResource(R.string.chooseASource), - list = sourceRepository.list.filterNot { it.apiService.notWorking }, + list = remember { + combine( + sourceRepository.sources, + itemDao.getSourceOrder() + ) { list, order -> + list + .filterNot { it.apiService.notWorking } + .sortedBy { order.find { o -> o.source == it.packageName }?.order ?: 0 } + } + } + .collectAsStateWithLifecycle(emptyList()) + .value, onClick = { service -> onChosen() scope.launch { diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/settings/SourceOrderScreen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SourceOrderScreen.kt new file mode 100644 index 000000000..e9afc27ce --- /dev/null +++ b/UIViews/src/main/java/com/programmersbox/uiviews/settings/SourceOrderScreen.kt @@ -0,0 +1,193 @@ +package com.programmersbox.uiviews.settings + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.rounded.DragHandle +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ElevatedSplitButton +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SplitButtonDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.toMutableStateList +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.favoritesdatabase.ItemDao +import com.programmersbox.favoritesdatabase.SourceOrder +import com.programmersbox.uiviews.utils.BackButton +import com.programmersbox.uiviews.utils.LocalItemDao +import com.programmersbox.uiviews.utils.LocalNavHostPadding +import com.programmersbox.uiviews.utils.LocalSourcesRepository +import com.programmersbox.uiviews.utils.components.plus +import sh.calvin.reorderable.ReorderableItem +import sh.calvin.reorderable.rememberReorderableLazyListState + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SourceOrderScreen( + sourceRepository: SourceRepository = LocalSourcesRepository.current, + itemDao: ItemDao = LocalItemDao.current, +) { + val sourcesInOrder by sourceRepository + .sources + .collectAsStateWithLifecycle(emptyList()) + + //var showSourceChooser by showSourceChooser() + + val sourceOrder by itemDao + .getSourceOrder() + .collectAsStateWithLifecycle(emptyList()) + + val modifiedOrder = remember(sourceOrder.isNotEmpty()) { + sourceOrder + .sortedBy { it.order } + .toMutableStateList() + } + + LaunchedEffect(sourcesInOrder) { + sourcesInOrder.forEachIndexed { index, sourceInformation -> + itemDao.insertSourceOrder( + SourceOrder( + source = sourceInformation.packageName, + name = sourceInformation.apiService.serviceName, + order = index + ) + ) + } + } + + val haptic = LocalHapticFeedback.current + + val lazyListState = rememberLazyListState() + val reorderableLazyListState = rememberReorderableLazyListState(lazyListState) { from, to -> + runCatching { + val tmp = modifiedOrder[to.index].copy(order = from.index) + modifiedOrder[to.index] = modifiedOrder[from.index].copy(order = to.index) + modifiedOrder[from.index] = tmp + } + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + } + + LaunchedEffect(reorderableLazyListState.isAnyItemDragging) { + if (!reorderableLazyListState.isAnyItemDragging) { + modifiedOrder.forEach { itemDao.updateSourceOrder(it) } + } + } + + var checked by remember { mutableStateOf(false) } + + Scaffold( + topBar = { + TopAppBar( + navigationIcon = { BackButton() }, + title = { Text("Source Order") }, + actions = { + ElevatedSplitButton( + onLeadingButtonClick = { + //showSourceChooser = true + }, + checked = checked, + onTrailingButtonClick = { checked = !checked }, + leadingContent = { + Icon( + Icons.Filled.Edit, + modifier = Modifier.size(SplitButtonDefaults.LeadingIconSize), + contentDescription = "Localized description" + ) + Spacer(Modifier.size(ButtonDefaults.IconSpacing)) + Text("Custom") + }, + trailingContent = { + val rotation: Float by animateFloatAsState( + targetValue = if (checked) 180f else 0f, + label = "Trailing Icon Rotation" + ) + Icon( + Icons.Filled.KeyboardArrowDown, + modifier = + Modifier + .size(SplitButtonDefaults.TrailingIconSize) + .graphicsLayer { + this.rotationZ = rotation + }, + contentDescription = "Localized description" + ) + } + ) + } + ) + } + ) { padding -> + LazyColumn( + state = lazyListState, + verticalArrangement = Arrangement.spacedBy(4.dp), + contentPadding = padding + LocalNavHostPadding.current, + modifier = Modifier.fillMaxSize() + ) { + items( + items = modifiedOrder, + key = { it.source } + ) { item -> + ReorderableItem( + state = reorderableLazyListState, + key = item.source, + ) { + val interactionSource = remember { MutableInteractionSource() } + + OutlinedCard( + onClick = {}, + interactionSource = interactionSource, + ) { + ListItem( + leadingContent = { Text(item.order.toString()) }, + headlineContent = { Text(item.name) }, + trailingContent = { + IconButton( + modifier = Modifier.longPressDraggableHandle( + onDragStarted = { + haptic.performHapticFeedback(HapticFeedbackType.LongPress) + }, + onDragStopped = { + haptic.performHapticFeedback(HapticFeedbackType.TextHandleMove) + }, + interactionSource = interactionSource, + ), + onClick = {}, + ) { + Icon(Icons.Rounded.DragHandle, contentDescription = "Reorder") + } + } + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt index 6eb999dfd..954a003fa 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Screen.kt @@ -117,6 +117,9 @@ sealed class Screen(val route: String) { @Serializable data object GeminiScreen : Screen("gemini") + + @Serializable + data object OrderScreen : Screen("order") } fun NavController.navigateToDetails1(model: ItemModel) = navigate( diff --git a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Utils.kt b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Utils.kt index 7285611f7..8f53a34c2 100644 --- a/UIViews/src/main/java/com/programmersbox/uiviews/utils/Utils.kt +++ b/UIViews/src/main/java/com/programmersbox/uiviews/utils/Utils.kt @@ -2,6 +2,9 @@ package com.programmersbox.uiviews.utils import com.google.firebase.crashlytics.ktx.crashlytics import com.google.firebase.ktx.Firebase +import com.programmersbox.extensionloader.SourceRepository +import com.programmersbox.favoritesdatabase.ItemDao +import kotlinx.coroutines.flow.combine import kotlin.experimental.ExperimentalTypeInference @OptIn(ExperimentalTypeInference::class) @@ -14,4 +17,14 @@ fun recordFirebaseException(throwable: Throwable) = runCatching { fun logFirebaseMessage(message: String) = runCatching { Firebase.crashlytics.log(message) +} + +fun combineSources( + sourceRepository: SourceRepository, + dao: ItemDao, +) = combine( + sourceRepository.sources, + dao.getSourceOrder() +) { list, order -> + list.sortedBy { order.find { o -> o.source == it.packageName }?.order ?: 0 } } \ No newline at end of file diff --git a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDao.kt b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDao.kt index 5ee819e3d..7e72a8ec1 100644 --- a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDao.kt +++ b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDao.kt @@ -78,4 +78,24 @@ interface ItemDao { @Query("SELECT COUNT(id) FROM Notifications") fun getAllNotificationCount(): Flow + @Query("SELECT * FROM SourceOrder ORDER BY `order` ASC") + fun getSourceOrder(): Flow> + + @Insert(onConflict = OnConflictStrategy.IGNORE) + suspend fun insertSourceOrder(sourceOrder: SourceOrder) + + @Query("SELECT * FROM SourceOrder ORDER BY `order` ASC") + suspend fun getSourceOrderSync(): List + + @Query("DELETE FROM SourceOrder") + suspend fun deleteAllSourceOrder() + + @Query("DELETE FROM SourceOrder where source = :source") + suspend fun deleteSourceOrder(source: String) + + @Query("UPDATE SourceOrder SET `order` = :order WHERE source = :source") + suspend fun updateSourceOrder(source: String, order: Int) + + @Update + suspend fun updateSourceOrder(sourceOrder: SourceOrder) } \ No newline at end of file diff --git a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDatabase.kt b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDatabase.kt index c860a6632..6cccf9524 100644 --- a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDatabase.kt +++ b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemDatabase.kt @@ -10,12 +10,16 @@ import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase @Database( - entities = [DbModel::class, ChapterWatched::class, NotificationItem::class], - version = 3, + entities = [DbModel::class, ChapterWatched::class, NotificationItem::class, SourceOrder::class], + version = 4, autoMigrations = [ AutoMigration( from = 2, to = 3 + ), + AutoMigration( + from = 3, + to = 4 ) ] ) diff --git a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemModels.kt b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemModels.kt index d3cead859..932590992 100644 --- a/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemModels.kt +++ b/favoritesdatabase/src/main/java/com/programmersbox/favoritesdatabase/ItemModels.kt @@ -52,5 +52,16 @@ data class NotificationItem( @ColumnInfo(name = "source") val source: String, @ColumnInfo(name = "contentTitle") - val contentTitle: String + val contentTitle: String, +) + +@Entity("SourceOrder") +data class SourceOrder( + @PrimaryKey + @ColumnInfo(name = "source") + val source: String, + @ColumnInfo(name = "name") + val name: String, + @ColumnInfo(name = "order") + val order: Int, ) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 507ea0885..567e7e925 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,4 +1,6 @@ [versions] +biometricVersion = "1.4.0-alpha02" +blurhash = "0.4.0-SNAPSHOT" generativeai = "0.9.0" gradle = "8.7.0" @@ -19,6 +21,7 @@ glideVersion = "4.16.0" pagecurl = "1.5.1" pagingVersion = "3.3.2" protobufGradlePlugin = "0.9.4" +reorderable = "2.4.0-beta01" roomVersion = "2.7.0-alpha09" navVersion = "2.8.2" koinVersion = "4.0.0" @@ -73,6 +76,8 @@ google-android-libraries-mapsplatform-secrets-gradle-plugin = { id = "com.google [libraries] androidx-baselineprofile-gradle-plugin = { module = "androidx.baselineprofile:androidx.baselineprofile.gradle.plugin", version.ref = "androidx-baselineprofile" } +biometric = { module = "androidx.biometric:biometric", version.ref = "biometricVersion" } +blurhash = { module = "com.vanniktech:blurhash", version.ref = "blurhash" } compose-collapsable = { module = "me.tatarka.compose.collapsable:compose-collapsable", version.ref = "composeCollapsable" } dragselect = { module = "com.dragselectcompose:dragselect", version.ref = "dragselect" } easylauncher = { module = "com.project.starter:easylauncher", version.ref = "easylauncher" } @@ -112,6 +117,7 @@ composeMaterialIconsCore = { group = "androidx.compose.material", name = "materi composeMaterialIconsExtended = { group = "androidx.compose.material", name = "material-icons-extended" } composeViewBinding = { group = "androidx.compose.ui", name = "ui-viewbinding" } protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobufGradlePlugin" } +reorderable = { module = "sh.calvin.reorderable:reorderable", version.ref = "reorderable" } tv-material = { module = "androidx.tv:tv-material", version = "1.0.0" } tv-foundation = { module = "androidx.tv:tv-foundation", version = "1.0.0-alpha11" } uiUtil = { group = "androidx.compose.ui", name = "ui-util" }