feat: enhance tablet layout with adaptive components and improve UI responsiveness

This commit is contained in:
maxrave-dev 2025-10-01 23:40:15 +07:00
parent 7f72ba1322
commit d2c55fe434
No known key found for this signature in database
16 changed files with 204 additions and 59 deletions

View file

@ -1,3 +1,4 @@
import com.android.build.gradle.internal.tasks.CompileArtProfileTask
import java.util.Properties
@ -201,6 +202,7 @@ dependencies {
androidTestImplementation(composeBom)
implementation(libs.compose.material3.lib)
implementation(libs.compose.material3.sizeclass)
implementation(libs.compose.material3.adaptive)
implementation(libs.compose.ui)
implementation(libs.compose.material.ripple)
implementation(libs.compose.material.icons.core)
@ -302,11 +304,9 @@ dependencies {
}
/**
* Task to generate the aboutlibraries.json file
* Run with:
./gradlew :app:exportLibraryDefinitions --no-daemon --no-configuration-cache --no-build-cache
**/
aboutLibraries {
android.registerAndroidTasks = true
collect.configPath = file("../config")
export {
exportVariant = "fullRelease"
prettyPrint = true

View file

@ -68,7 +68,7 @@
<activity
android:name=".ui.MainActivity"
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
android:configChanges="keyboardHidden|screenSize|screenLayout|smallestScreenSize"
android:exported="true"
android:supportsPictureInPicture="true"
android:enableOnBackInvokedCallback="true"

View file

@ -35,14 +35,14 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.rounded.ArrowForwardIos
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@ -53,6 +53,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalResources
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.TextLinkStyles
@ -66,6 +67,7 @@ import androidx.core.os.LocaleListCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import androidx.window.core.layout.WindowWidthSizeClass
import com.kyant.backdrop.backdrops.layerBackdrop
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
import com.maxrave.common.FIRST_TIME_MIGRATION
@ -147,7 +149,6 @@ class MainActivity : AppCompatActivity() {
viewModel.setIntent(intent)
}
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@ExperimentalMaterial3Api
@ExperimentalFoundationApi
override fun onCreate(savedInstanceState: Bundle?) {
@ -238,7 +239,8 @@ class MainActivity : AppCompatActivity() {
viewModel.getLocation()
setContent {
val windowSize = calculateWindowSizeClass(activity = this)
val windowSize = currentWindowAdaptiveInfo().windowSizeClass
val resources = LocalResources.current
val navController = rememberNavController()
val sleepTimerState by viewModel.sleepTimerState.collectAsStateWithLifecycle()
@ -364,7 +366,8 @@ class MainActivity : AppCompatActivity() {
var isScrolledToTop by rememberSaveable {
mutableStateOf(false)
}
val isTablet = windowSize.widthSizeClass == WindowWidthSizeClass.Medium || windowSize.widthSizeClass == WindowWidthSizeClass.Expanded
val isTablet = windowSize.windowWidthSizeClass != WindowWidthSizeClass.COMPACT
val isTabletLandscape = isTablet && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
AppTheme {
Scaffold(
bottomBar = {
@ -426,7 +429,7 @@ class MainActivity : AppCompatActivity() {
Modifier
.fillMaxSize()
.then(
if (isLiquidGlassEnabled == TRUE) {
if (isLiquidGlassEnabled == TRUE && !isTablet) {
Modifier.layerBackdrop(backdrop)
} else {
Modifier
@ -446,22 +449,34 @@ class MainActivity : AppCompatActivity() {
Box(
Modifier.fillMaxSize().weight(1f),
) {
AppNavigationGraph(
innerPadding = innerPadding,
navController = navController,
hideNavBar = {
isNavBarVisible = false
},
showNavBar = {
isNavBarVisible = true
},
showNowPlayingSheet = {
isShowNowPlaylistScreen = true
},
onScrolling = {
isScrolledToTop = it
},
)
Box(
Modifier
.fillMaxSize()
.then(
if (isLiquidGlassEnabled == TRUE && isTablet) {
Modifier.layerBackdrop(backdrop)
} else {
Modifier
},
),
) {
AppNavigationGraph(
innerPadding = innerPadding,
navController = navController,
hideNavBar = {
isNavBarVisible = false
},
showNavBar = {
isNavBarVisible = true
},
showNowPlayingSheet = {
isShowNowPlaylistScreen = true
},
onScrolling = {
isScrolledToTop = it
},
)
}
this@Row.AnimatedVisibility(
modifier =
Modifier
@ -474,7 +489,7 @@ class MainActivity : AppCompatActivity() {
MiniPlayer(
Modifier
.height(56.dp)
.fillMaxWidth(0.5f)
.fillMaxWidth(0.8f)
.padding(
horizontal = 12.dp,
).padding(
@ -491,7 +506,7 @@ class MainActivity : AppCompatActivity() {
)
}
}
if (isTablet) {
if (isTablet && isTabletLandscape) {
AnimatedVisibility(
isShowNowPlaylistScreen,
enter = expandHorizontally() + fadeIn(),
@ -508,6 +523,7 @@ class MainActivity : AppCompatActivity() {
navController = navController,
sharedViewModel = viewModel,
isExpanded = true,
dismissIcon = Icons.AutoMirrored.Rounded.ArrowForwardIos,
onSwipeEnabledChange = {},
) {
isShowNowPlaylistScreen = false
@ -519,7 +535,7 @@ class MainActivity : AppCompatActivity() {
}
}
if (isShowNowPlaylistScreen && !isTablet) {
if (isShowNowPlaylistScreen && !isTabletLandscape) {
NowPlayingScreen(
navController = navController,
) {

View file

@ -326,6 +326,7 @@ fun HomeItemContentPlaylist(
modifier =
Modifier
.size(thumbSize)
.aspectRatio(1f)
.clip(
RoundedCornerShape(10.dp),
),

View file

@ -168,7 +168,7 @@ fun AppNavigationRail(
)
}
NavigationRail {
Spacer(Modifier.height(32.dp))
Spacer(Modifier.height(16.dp))
Box(Modifier.padding(horizontal = 16.dp)) {
Box(
Modifier
@ -228,6 +228,6 @@ fun AppNavigationRail(
},
)
}
Spacer(Modifier.height(64.dp))
Spacer(Modifier.height(32.dp))
}
}

View file

@ -98,7 +98,7 @@ fun CollapsingToolbarParallaxEffect(
TopAppBarDefaults.TopAppBarExpandedHeight + with(density) { WindowInsets.statusBars.getTop(this).toDp() * 2 }
val scroll: ScrollState = rememberScrollState(0)
val headerHeight = getScreenSizeInfo().hDP.dp * 2 / 6
val headerHeight = (getScreenSizeInfo().hDP.dp * 2 / 6).coerceAtLeast(200.dp)
val headerHeightPx = with(density) { headerHeight.toPx() }
val toolbarHeightPx = with(density) { toolbarHeight.toPx() }

View file

@ -6,9 +6,11 @@ import androidx.compose.foundation.MarqueeAnimationMode
import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -107,7 +109,8 @@ inline fun <reified T> GridLibraryPlaylist(
val list = (data as? LocalResource.Success)?.data ?: emptyList()
if (data is LocalResource.Success && list.isNotEmpty() || createNewPlaylist != null) {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 125.dp),
columns = GridCells.FixedSize(size = 132.dp),
horizontalArrangement = Arrangement.SpaceEvenly,
contentPadding = contentPadding,
state = state,
) {
@ -126,7 +129,8 @@ inline fun <reified T> GridLibraryPlaylist(
) {
Box(
Modifier
.size(125.dp)
.size(132.dp)
.aspectRatio(1f)
.clip(RoundedCornerShape(10.dp))
.angledGradientBackground(
colors =
@ -152,7 +156,7 @@ inline fun <reified T> GridLibraryPlaylist(
maxLines = 1,
modifier =
Modifier
.width(125.dp)
.width(132.dp)
.wrapContentHeight(align = Alignment.CenterVertically)
.padding(top = 8.dp)
.basicMarquee(
@ -214,7 +218,7 @@ inline fun <reified T> GridLibraryPlaylist(
}
},
data = item,
thumbSize = 125.dp,
thumbSize = 132.dp,
)
}

View file

@ -499,8 +499,6 @@ fun LiquidGlassAppBottomNavigationBar(
.height(56.dp)
.layoutId("miniPlayer"),
backdrop = backdrop,
layer = layer,
luminanceAnimation = luminanceAnimation.value,
onClick = {
onOpenNowPlaying()
},

View file

@ -1,5 +1,6 @@
package com.maxrave.simpmusic.ui.screen
import android.graphics.Bitmap
import androidx.compose.animation.Animatable
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.Crossfade
@ -59,8 +60,9 @@ import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.layer.GraphicsLayer
import androidx.compose.ui.graphics.rememberGraphicsLayer
import androidx.compose.ui.input.pointer.PointerInputChange
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
@ -68,6 +70,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.core.graphics.scale
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
@ -89,30 +92,72 @@ import com.maxrave.simpmusic.ui.theme.transparent
import com.maxrave.simpmusic.ui.theme.typo
import com.maxrave.simpmusic.viewModel.SharedViewModel
import com.maxrave.simpmusic.viewModel.UIEvent
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.compose.koinInject
import java.nio.IntBuffer
import kotlin.math.roundToInt
import kotlin.time.Duration.Companion.seconds
private const val TAG = "MiniPlayer"
@Composable
fun MiniPlayer(
modifier: Modifier,
backdrop: Backdrop,
layer: GraphicsLayer? = null,
luminanceAnimation: Float? = null,
sharedViewModel: SharedViewModel = koinInject(),
onClose: () -> Unit,
onClick: () -> Unit,
) {
val isLiquidGlassEnabled by sharedViewModel.getEnableLiquidGlass().collectAsStateWithLifecycle(DataStoreManager.FALSE)
val layer = rememberGraphicsLayer()
val luminanceAnimation = remember { Animatable(0f) }
val textColor by animateColorAsState(
targetValue = if (luminanceAnimation != null && luminanceAnimation > 0.6f) Color.Black else Color.White,
targetValue = if (luminanceAnimation.value > 0.6f) Color.Black else Color.White,
label = "MiniPlayerTextColor",
animationSpec = tween(500),
)
LaunchedEffect(layer, isLiquidGlassEnabled) {
val buffer = IntBuffer.allocate(25)
while (isActive && isLiquidGlassEnabled == DataStoreManager.TRUE) {
try {
withContext(Dispatchers.Main) {
val imageBitmap = layer.toImageBitmap()
val thumbnail =
imageBitmap
.asAndroidBitmap()
.scale(5, 5, false)
.copy(Bitmap.Config.ARGB_8888, false)
buffer.rewind()
thumbnail.copyPixelsToBuffer(buffer)
}
} catch (e: Exception) {
Logger.e(TAG, "Error getting pixels from layer: ${e.localizedMessage}")
}
val averageLuminance =
(0 until 25).sumOf { index ->
val color = buffer.get(index)
val r = (color shr 16 and 0xFF) / 255f
val g = (color shr 8 and 0xFF) / 255f
val b = (color and 0xFF) / 255f
0.2126 * r + 0.7152 * g + 0.0722 * b
} / 25
luminanceAnimation.animateTo(
averageLuminance.coerceAtMost(0.8).toFloat(),
tween(500),
)
delay(1.seconds)
}
}
val (songEntity, setSongEntity) =
remember {
mutableStateOf<SongEntity?>(null)
@ -220,8 +265,8 @@ fun MiniPlayer(
modifier =
modifier
.then(
if (isLiquidGlassEnabled == DataStoreManager.TRUE && layer != null && luminanceAnimation != null) {
Modifier.drawBackdropCustomShape(backdrop, layer, luminanceAnimation, RoundedCornerShape(16.dp))
if (isLiquidGlassEnabled == DataStoreManager.TRUE) {
Modifier.drawBackdropCustomShape(backdrop, layer, luminanceAnimation.value, RoundedCornerShape(16.dp))
} else {
Modifier
},

View file

@ -118,7 +118,6 @@ import com.maxrave.simpmusic.ui.navigation.destination.home.CreditDestination
import com.maxrave.simpmusic.ui.navigation.destination.login.LoginDestination
import com.maxrave.simpmusic.ui.navigation.destination.login.SpotifyLoginDestination
import com.maxrave.simpmusic.ui.theme.DarkColors
import com.maxrave.simpmusic.ui.theme.md_theme_dark_outline
import com.maxrave.simpmusic.ui.theme.md_theme_dark_primary
import com.maxrave.simpmusic.ui.theme.typo
import com.maxrave.simpmusic.ui.theme.white
@ -127,9 +126,10 @@ import com.maxrave.simpmusic.viewModel.SettingAlertState
import com.maxrave.simpmusic.viewModel.SettingBasicAlertState
import com.maxrave.simpmusic.viewModel.SettingsViewModel
import com.maxrave.simpmusic.viewModel.SharedViewModel
import com.mikepenz.aboutlibraries.entity.Library
import com.mikepenz.aboutlibraries.ui.compose.ChipColors
import com.mikepenz.aboutlibraries.ui.compose.LibraryDefaults
import com.mikepenz.aboutlibraries.ui.compose.android.rememberLibraries
import com.mikepenz.aboutlibraries.ui.compose.android.produceLibraries
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
import com.mikepenz.aboutlibraries.ui.compose.m3.libraryColors
import dev.chrisbanes.haze.hazeEffect
@ -137,6 +137,7 @@ import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.chrisbanes.haze.rememberHazeState
import kotlinx.collections.immutable.toImmutableList
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import org.koin.androidx.compose.koinViewModel
@ -1819,7 +1820,7 @@ fun SettingScreen(
}
if (showThirdPartyLibraries) {
val libraries by rememberLibraries(com.maxrave.simpmusic.R.raw.aboutlibraries)
val libraries by produceLibraries(com.maxrave.simpmusic.R.raw.aboutlibraries)
val lazyListState = rememberLazyListState()
val canScrollBackward by remember {
derivedStateOf {
@ -1849,17 +1850,25 @@ fun SettingScreen(
shape = RectangleShape,
) {
LibrariesContainer(
libraries,
libraries?.copy(
libraries =
libraries
?.libraries
?.distinctBy {
it.name
}?.toImmutableList() ?: emptyList<Library>().toImmutableList(),
),
Modifier.fillMaxSize(),
lazyListState = lazyListState,
showDescription = true,
contentPadding = innerPadding,
typography = typo,
colors =
LibraryDefaults.libraryColors(
licenseChipColors =
object : ChipColors {
override val containerColor: Color
get() = md_theme_dark_outline
get() = Color.DarkGray
override val contentColor: Color
get() = Color.White
},

View file

@ -55,6 +55,7 @@ import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.Done
import androidx.compose.material.icons.rounded.Forward5
import androidx.compose.material.icons.rounded.KeyboardArrowDown
import androidx.compose.material.icons.rounded.PauseCircle
import androidx.compose.material.icons.rounded.PlayCircle
import androidx.compose.material.icons.rounded.Repeat
@ -105,6 +106,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.onGloballyPositioned
@ -224,6 +226,7 @@ fun NowPlayingScreen(
sharedViewModel = sharedViewModel,
navController = navController,
isExpanded = sheetState.currentValue == SheetValue.Expanded,
dismissIcon = Icons.Rounded.KeyboardArrowDown,
onSwipeEnabledChange = {
swipeEnabled = it
},
@ -240,6 +243,7 @@ fun NowPlayingScreenContent(
sharedViewModel: SharedViewModel = koinInject(),
navController: NavController,
isExpanded: Boolean,
dismissIcon: ImageVector,
onSwipeEnabledChange: (Boolean) -> Unit = {},
onDismiss: () -> Unit = {},
) {
@ -702,7 +706,7 @@ fun NowPlayingScreenContent(
onDismiss()
}) {
Icon(
painter = painterResource(id = R.drawable.baseline_keyboard_arrow_down_24),
imageVector = dismissIcon,
contentDescription = "",
tint = Color.White,
)
@ -1086,7 +1090,7 @@ fun NowPlayingScreenContent(
top = 15.dp,
).padding(horizontal = 20.dp)
.isElementVisible {
shouldShowToolbar = !it && isExpanded
shouldShowToolbar = !it && isExpanded && mainScrollState.value > 0
},
) {
Box(
@ -1247,7 +1251,8 @@ fun NowPlayingScreenContent(
.size(42.dp)
.clip(
CircleShape,
).clickable {
).weight(1f)
.clickable {
sharedViewModel.onUIEvent(UIEvent.Shuffle)
},
contentAlignment = Alignment.Center,
@ -1277,7 +1282,8 @@ fun NowPlayingScreenContent(
.size(52.dp)
.clip(
CircleShape,
).clickable {
).weight(1f)
.clickable {
if (controllerState.isPreviousAvailable) {
sharedViewModel.onUIEvent(UIEvent.Previous)
}
@ -1298,7 +1304,8 @@ fun NowPlayingScreenContent(
.size(96.dp)
.clip(
CircleShape,
).clickable {
).weight(1f)
.clickable {
sharedViewModel.onUIEvent(UIEvent.PlayPause)
},
contentAlignment = Alignment.Center,
@ -1328,7 +1335,8 @@ fun NowPlayingScreenContent(
.size(52.dp)
.clip(
CircleShape,
).clickable {
).weight(1f)
.clickable {
if (controllerState.isNextAvailable) {
sharedViewModel.onUIEvent(UIEvent.Next)
}
@ -1348,7 +1356,8 @@ fun NowPlayingScreenContent(
.size(42.dp)
.clip(
CircleShape,
).clickable {
).weight(1f)
.clickable {
sharedViewModel.onUIEvent(UIEvent.Repeat)
},
contentAlignment = Alignment.Center,

View file

@ -0,0 +1,25 @@
{
"uniqueId": "com.zionhuang.music",
"funding": [
],
"developers": [
{
"name": "Zion Huang"
}
],
"artifactVersion": "",
"description": "A Material 3 YouTube Music client for Android",
"scm": {
"connection": "scm:git:https://github.com/z-huang/InnerTune.git",
"url": "https://github.com/z-huang/InnerTune.git"
},
"name": "InnerTune",
"website": "https://github.com/z-huang/InnerTune",
"licenses": [
"GPL-3.0"
],
"organization": {
"name": "Zion Huang"
}
}

View file

@ -0,0 +1,25 @@
{
"uniqueId": "com.yuliskov.smarttubetv",
"funding": [
],
"developers": [
{
"name": "Yuriy L"
}
],
"artifactVersion": "",
"description": "SmartTube is a free and open-source advanced media player for Android TVs and TV boxes. It allows you to play content from various public sources.",
"scm": {
"connection": "scm:git:https://github.com/yuliskov/SmartTube.git",
"url": "https://github.com/yuliskov/SmartTube.git"
},
"name": "SmartTube",
"website": "www.patreon.com/smarttube",
"licenses": [
"GNU"
],
"organization": {
"name": "Yuriy L"
}
}

6
config/licenses/gnu.json Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -39,7 +39,7 @@ datastore-preferences = "1.1.7"
lottie = "6.6.9"
paging = "3.3.6"
customactivityoncrash = "2.4.0"
aboutlibraries = "12.2.4"
aboutlibraries = "13.0.0-rc01"
balloon = "1.6.13"
ktor = "3.3.0"
brotli = "0.1.2"
@ -70,6 +70,7 @@ desugaring = { group = "com.android.tools", name = "desugar_jdk_libs_nio", versi
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material3-lib = { group = "androidx.compose.material3", name = "material3", version.ref = "material3-expressive" }
compose-material3-sizeclass = { group = "androidx.compose.material3", name = "material3-window-size-class" }
compose-material3-adaptive = { group = "androidx.compose.material3.adaptive", name = "adaptive" }
compose-ui = { group = "androidx.compose.ui", name = "ui" }
compose-material-ripple = { group = "androidx.compose.material", name = "material-ripple" }
compose-material-icons-core = { group = "androidx.compose.material", name = "material-icons-core" }
@ -180,6 +181,6 @@ navigation-safeargs = { id = "androidx.navigation.safeargs.kotlin", version.ref
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "serialization" }
kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlibraries" }
aboutlibraries = { id = "com.mikepenz.aboutlibraries.plugin.android", version.ref = "aboutlibraries" }
room = { id = "androidx.room", version.ref = "room" }
sentry-gradle = { id = "io.sentry.android.gradle", version.ref = "sentry-gradle" }