diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fbcf0e169..9d2db540d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,6 +8,7 @@ plugins {
alias(libs.plugins.compose.compiler)
alias(libs.plugins.hilt)
alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.ksp)
}
@@ -163,6 +164,7 @@ dependencies {
implementation(platform(libs.compose.bom))
implementation(libs.compose.material3)
implementation(libs.compose.materialIconsExtended)
+ implementation(libs.compose.navigation)
implementation(libs.compose.runtime.livedata)
debugImplementation(libs.compose.ui.tooling)
implementation(libs.compose.ui.toolingPreview)
@@ -189,6 +191,7 @@ dependencies {
@Suppress("RedundantSuppression")
implementation(libs.dnsjava)
implementation(libs.guava)
+ implementation(libs.kotlinx.serialization)
implementation(libs.mikepenz.aboutLibraries)
implementation(libs.nsk90.kstatemachine)
implementation(libs.okhttp.base)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 173b80fc0..487416721 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -60,9 +60,8 @@
-
@@ -70,15 +69,18 @@
-
+
+
+
@@ -106,7 +108,7 @@
@@ -134,7 +136,7 @@
+ android:parentActivityName=".ui.MainActivity" />
()
-
- @Inject
- lateinit var licenseInfoProvider: Optional
-
-
- @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- setContent {
- AppTheme {
- val uriHandler = LocalUriHandler.current
-
- Scaffold(
- topBar = {
- TopAppBar(
- navigationIcon = {
- IconButton(onClick = { onSupportNavigateUp() }) {
- Icon(
- Icons.AutoMirrored.Default.ArrowBack,
- contentDescription = stringResource(R.string.navigate_up)
- )
- }
- },
- title = {
- Text(stringResource(R.string.navigation_drawer_about))
- },
- actions = {
- IconButton(onClick = {
- uriHandler.openUri(Constants.HOMEPAGE_URL
- .buildUpon()
- .withStatParams("AboutActivity")
- .build().toString())
- }) {
- Icon(
- Icons.Default.Home,
- contentDescription = stringResource(R.string.navigation_drawer_website)
- )
- }
- }
- )
- }
- ) { paddingValues ->
- Column(Modifier.padding(paddingValues)) {
- val scope = rememberCoroutineScope()
- val state = rememberPagerState(pageCount = { 3 })
-
- TabRow(state.currentPage) {
- Tab(state.currentPage == 0, onClick = {
- scope.launch { state.scrollToPage(0) }
- }) {
- Text(
- stringResource(R.string.app_name),
- modifier = Modifier.padding(8.dp)
- )
- }
- Tab(state.currentPage == 1, onClick = {
- scope.launch { state.scrollToPage(1) }
- }) {
- Text(
- stringResource(R.string.about_translations),
- modifier = Modifier.padding(8.dp)
- )
- }
- Tab(state.currentPage == 2, onClick = {
- scope.launch { state.scrollToPage(2) }
- }) {
- Text(
- stringResource(R.string.about_libraries),
- modifier = Modifier.padding(8.dp)
- )
- }
- }
-
- HorizontalPager(
- state,
- modifier = Modifier
- .fillMaxWidth()
- .weight(1f),
- verticalAlignment = Alignment.Top
- ) { index ->
- when (index) {
- 0 -> AboutApp(licenseInfoProvider = licenseInfoProvider.getOrNull())
- 1 -> {
- val translations = model.translations.observeAsState(emptyList())
- TranslatorsGallery(translations.value)
- }
-
- 2 -> LibrariesContainer(Modifier.fillMaxSize(),
- itemContentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
- itemSpacing = 8.dp,
- librariesBlock = { ctx ->
- Libs.Builder()
- .withJson(ctx, R.raw.aboutlibraries)
- .build()
- })
- }
- }
- }
- }
- }
- }
- }
-
-
- @HiltViewModel
- class Model @Inject constructor(
- @ApplicationContext val context: Context,
- private val logger: Logger
- ): ViewModel() {
-
- data class Translation(
- val language: String,
- val translators: Set
- )
-
- val translations = MutableLiveData>()
-
- init {
- viewModelScope.launch(Dispatchers.IO) {
- loadTranslations()
- }
- }
-
- private fun loadTranslations() {
- try {
- context.resources.assets.open("translators.json").use { stream ->
- val jsonTranslations = JSONObject(stream.readBytes().decodeToString())
- val result = LinkedList()
- for (langCode in jsonTranslations.keys()) {
- val jsonTranslators = jsonTranslations.getJSONArray(langCode)
- val translators = Array(jsonTranslators.length()) { idx ->
- jsonTranslators.getString(idx)
- }
-
- val langTag = langCode.replace('_', '-')
- val language = Locale.forLanguageTag(langTag).displayName
- result += Translation(language, translators.toSet())
- }
-
- // sort translations by localized language name
- val collator = Collator.getInstance()
- result.sortWith { o1, o2 ->
- collator.compare(o1.language, o2.language)
- }
-
- translations.postValue(result)
- }
- } catch (e: Exception) {
- logger.log(Level.WARNING, "Couldn't load translators", e)
- }
- }
-
- }
-
-
- interface AppLicenseInfoProvider {
- @Composable
- fun LicenseInfo()
- }
-
- @Module
- @InstallIn(ActivityComponent::class)
- interface AppLicenseInfoProviderModule {
- @BindsOptionalOf
- fun appLicenseInfoProvider(): AppLicenseInfoProvider
- }
-
-}
-
-
-@Composable
-fun AboutApp(licenseInfoProvider: AboutActivity.AppLicenseInfoProvider? = null) {
- Column(
- modifier = Modifier
- .padding(8.dp)
- .fillMaxWidth()
- .verticalScroll(rememberScrollState())) {
- Image(
- UiUtils.adaptiveIconPainterResource(R.mipmap.ic_launcher),
- contentDescription = stringResource(R.string.app_name),
- modifier = Modifier
- .size(128.dp)
- .align(Alignment.CenterHorizontally)
- )
- Text(
- stringResource(R.string.app_name),
- style = MaterialTheme.typography.headlineMedium,
- textAlign = TextAlign.Center,
- modifier = Modifier
- .fillMaxWidth()
- .padding(8.dp)
- )
-
- Text(
- stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
- style = MaterialTheme.typography.bodyLarge,
- textAlign = TextAlign.Center,
- modifier = Modifier.fillMaxWidth()
- )
-
- Text(
- stringResource(R.string.about_copyright),
- style = MaterialTheme.typography.bodyLarge,
- textAlign = TextAlign.Center,
- modifier = Modifier
- .fillMaxWidth()
- .padding(vertical = 8.dp)
- )
-
- Text(
- stringResource(R.string.about_license_info_no_warranty),
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier
- .fillMaxWidth()
- .padding(top = 8.dp)
- )
-
- PixelBoxes(
- arrayOf(Color(0xFFFCF434), Color.White, Color(0xFF9C59D1), Color.Black),
- modifier = Modifier
- .align(Alignment.CenterHorizontally)
- .padding(16.dp)
- )
-
- licenseInfoProvider?.LicenseInfo()
- }
-}
-
-@Composable
-@Preview
-fun AboutApp_Preview() {
- AboutApp(licenseInfoProvider = object : AboutActivity.AppLicenseInfoProvider {
- @Composable
- override fun LicenseInfo() {
- Text("Some flavored License Info")
- }
- })
-}
-
-
-@Composable
-fun TranslatorsGallery(
- translations: List
-) {
- val collator = Collator.getInstance()
- LazyColumn(Modifier.padding(8.dp)) {
- items(translations) { translation ->
- Text(
- translation.language,
- style = MaterialTheme.typography.headlineMedium,
- modifier = Modifier.padding(vertical = 4.dp)
- )
- Text(
- translation.translators
- .sortedWith { a, b -> collator.compare(a, b) }
- .joinToString(" · "),
- style = MaterialTheme.typography.bodyLarge,
- modifier = Modifier.padding(bottom = 16.dp)
- )
- }
- }
-}
-
-@Composable
-@Preview
-fun TranslatorsGallery_Sample() {
- TranslatorsGallery(listOf(
- AboutActivity.Model.Translation("Some Language", setOf("User 1", "User 2")),
- AboutActivity.Model.Translation("Another Language", setOf("User 3", "User 4"))
- ))
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt
index ad6752eb2..1f2973961 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsActivity.kt
@@ -6,53 +6,17 @@ package at.bitfire.davdroid.ui
import android.content.Intent
import android.os.Bundle
-import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
-import at.bitfire.davdroid.ui.account.AccountActivity
-import at.bitfire.davdroid.ui.intro.IntroActivity
-import at.bitfire.davdroid.ui.setup.LoginActivity
-import dagger.hilt.android.AndroidEntryPoint
-import javax.inject.Inject
-
-@AndroidEntryPoint
+@Deprecated("Automatically redirects to MainActivity. Should be removed in the future.")
class AccountsActivity: AppCompatActivity() {
-
- @Inject
- lateinit var accountsDrawerHandler: AccountsDrawerHandler
-
- private val introActivityLauncher = registerForActivityResult(IntroActivity.Contract) { cancelled ->
- if (cancelled)
- finish()
- }
-
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
- // handle "Sync all" intent from launcher shortcut
- val syncAccounts = intent.action == Intent.ACTION_SYNC
-
- setContent {
- AccountsScreen(
- initialSyncAccounts = syncAccounts,
- onShowAppIntro = {
- introActivityLauncher.launch(null)
- },
- accountsDrawerHandler = accountsDrawerHandler,
- onAddAccount = {
- startActivity(Intent(this, LoginActivity::class.java))
- },
- onShowAccount = { account ->
- val intent = Intent(this, AccountActivity::class.java)
- intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
- startActivity(intent)
- },
- onManagePermissions = {
- startActivity(Intent(this, PermissionsActivity::class.java))
- }
- )
- }
+ startActivity(
+ Intent(this, MainActivity::class.java).apply {
+ action = intent.action
+ }
+ )
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
index 4a081dcfb..5c529b0a5 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
@@ -53,6 +53,8 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
+import at.bitfire.davdroid.ui.composition.LocalNavController
+import at.bitfire.davdroid.ui.navigation.Routes
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
import kotlinx.coroutines.launch
import java.net.URI
@@ -104,6 +106,7 @@ abstract class AccountsDrawerHandler {
open fun ImportantEntries(
snackbarHostState: SnackbarHostState
) {
+ val navController = LocalNavController.current
val context = LocalContext.current
val isBeta =
LocalInspectionMode.current ||
@@ -116,7 +119,7 @@ abstract class AccountsDrawerHandler {
icon = Icons.Default.Info,
title = stringResource(R.string.navigation_drawer_about),
onClick = {
- context.startActivity(Intent(context, AboutActivity::class.java))
+ navController.navigate(Routes.About)
}
)
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
index e0ec50d83..d65b4d971 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsScreen.kt
@@ -6,6 +6,7 @@ package at.bitfire.davdroid.ui
import android.Manifest
import android.accounts.Account
+import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Build
@@ -71,11 +72,18 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavBackStackEntry
+import androidx.navigation.toRoute
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
+import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.account.AccountProgress
import at.bitfire.davdroid.ui.composable.ActionCard
import at.bitfire.davdroid.ui.composable.ProgressBar
+import at.bitfire.davdroid.ui.composition.LocalNavController
+import at.bitfire.davdroid.ui.intro.INTRO_CANCELLED
+import at.bitfire.davdroid.ui.navigation.Routes
+import at.bitfire.davdroid.ui.setup.LoginActivity
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberPermissionState
@@ -84,9 +92,49 @@ import kotlinx.coroutines.launch
@Composable
fun AccountsScreen(
+ backStackEntry: NavBackStackEntry,
+ accountsDrawerHandler: AccountsDrawerHandler
+) {
+ val route = backStackEntry.toRoute()
+ val navController = LocalNavController.current
+ val context = LocalContext.current
+ val activity = context as? Activity
+
+ LaunchedEffect(Unit) {
+ navController.currentBackStackEntry
+ ?.savedStateHandle
+ ?.getStateFlow(INTRO_CANCELLED, false)
+ ?.collect { cancelled ->
+ if (cancelled) activity?.finish()
+ }
+ }
+
+ AccountsScreen(
+ accountsDrawerHandler = accountsDrawerHandler,
+ initialSyncAccounts = route.syncAccounts,
+ onShowAppIntro = { navController.navigate(Routes.Intro) },
+ onAddAccount = {
+ // eventually this will become a navigation
+ context.startActivity(Intent(context, LoginActivity::class.java))
+ },
+ onShowAccount = { account ->
+ // eventually this will become a navigation
+ val intent = Intent(context, AccountActivity::class.java)
+ intent.putExtra(AccountActivity.EXTRA_ACCOUNT, account)
+ context.startActivity(intent)
+ },
+ onManagePermissions = {
+ // eventually this will become a navigation
+ context.startActivity(Intent(context, PermissionsActivity::class.java))
+ }
+ )
+}
+
+@Composable
+fun AccountsScreen(
+ accountsDrawerHandler: AccountsDrawerHandler,
initialSyncAccounts: Boolean,
onShowAppIntro: () -> Unit,
- accountsDrawerHandler: AccountsDrawerHandler,
onAddAccount: () -> Unit,
onShowAccount: (Account) -> Unit,
onManagePermissions: () -> Unit,
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
new file mode 100644
index 000000000..e7ac32eaa
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
+ */
+
+package at.bitfire.davdroid.ui
+
+import android.content.Intent
+import android.os.Bundle
+import androidx.activity.compose.setContent
+import androidx.appcompat.app.AppCompatActivity
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
+import at.bitfire.davdroid.ui.about.AboutScreen
+import at.bitfire.davdroid.ui.intro.IntroScreen
+import at.bitfire.davdroid.ui.navigation.LocalNavController
+import at.bitfire.davdroid.ui.navigation.Routes
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+
+@AndroidEntryPoint
+class MainActivity: AppCompatActivity() {
+
+ @Inject
+ lateinit var accountsDrawerHandler: AccountsDrawerHandler
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ setContent {
+ val navController = rememberNavController()
+
+ CompositionLocalProvider(LocalNavController provides navController) {
+ NavHost(
+ navController = navController,
+ startDestination = accountsFromIntent()
+ ) {
+ composable { AccountsScreen(it, accountsDrawerHandler) }
+ composable { IntroScreen() }
+ composable { AboutScreen() }
+ }
+ }
+ }
+ }
+
+ /**
+ * Initializes the accounts route from the current intent data.
+ * Checks whether the action is [Intent.ACTION_SYNC].
+ */
+ private fun accountsFromIntent() = Routes.Accounts(
+ // handle "Sync all" intent from launcher shortcut
+ syncAccounts = intent.action == Intent.ACTION_SYNC
+ )
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt
index 6ac2d82eb..c2b341486 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/PermissionsScreen.kt
@@ -34,9 +34,9 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.composable.CardWithImage
@@ -80,7 +80,7 @@ fun PermissionsScreen(
@Composable
fun PermissionsScreen(
modifier: Modifier = Modifier,
- model: PermissionsModel = viewModel()
+ model: PermissionsModel = hiltViewModel()
) {
// check permissions when the lifecycle owner (for instance Activity) is resumed
val lifecycle = LocalLifecycleOwner.current.lifecycle
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt
index da537402f..356493b1a 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksScreen.kt
@@ -40,8 +40,8 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.text.HtmlCompat
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
@@ -77,7 +77,7 @@ fun TasksScreen(onNavUp: () -> Unit) {
@Composable
fun TasksCard(
- model: TasksModel = viewModel()
+ model: TasksModel = hiltViewModel()
) {
val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
index 4b1038a79..72d97128a 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/UiUtils.kt
@@ -86,7 +86,7 @@ object UiUtils {
ShortcutInfo.Builder(context, SHORTCUT_SYNC_ALL)
.setIcon(Icon.createWithResource(context, R.drawable.ic_sync_shortcut))
.setShortLabel(context.getString(R.string.accounts_sync_all))
- .setIntent(Intent(Intent.ACTION_SYNC, null, context, AccountsActivity::class.java))
+ .setIntent(Intent(Intent.ACTION_SYNC, null, context, MainActivity::class.java))
.build()
)
} catch(e: Exception) {
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutModel.kt
new file mode 100644
index 000000000..96050f42c
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutModel.kt
@@ -0,0 +1,68 @@
+package at.bitfire.davdroid.ui.about
+
+import android.content.Context
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import dagger.hilt.android.lifecycle.HiltViewModel
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.json.JSONObject
+import java.text.Collator
+import java.util.LinkedList
+import java.util.Locale
+import java.util.logging.Level
+import java.util.logging.Logger
+import javax.inject.Inject
+
+@HiltViewModel
+class AboutModel @Inject constructor(
+ @ApplicationContext val context: Context,
+ private val logger: Logger,
+ val licenseInfoProvider: AppLicenseInfoProvider?
+): ViewModel() {
+
+ data class Translation(
+ val language: String,
+ val translators: Set
+ )
+
+ val translations = MutableLiveData>()
+
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ loadTranslations()
+ }
+ }
+
+ private fun loadTranslations() {
+ try {
+ context.resources.assets.open("translators.json").use { stream ->
+ val jsonTranslations = JSONObject(stream.readBytes().decodeToString())
+ val result = LinkedList()
+ for (langCode in jsonTranslations.keys()) {
+ val jsonTranslators = jsonTranslations.getJSONArray(langCode)
+ val translators = Array(jsonTranslators.length()) { idx ->
+ jsonTranslators.getString(idx)
+ }
+
+ val langTag = langCode.replace('_', '-')
+ val language = Locale.forLanguageTag(langTag).displayName
+ result += Translation(language, translators.toSet())
+ }
+
+ // sort translations by localized language name
+ val collator = Collator.getInstance()
+ result.sortWith { o1, o2 ->
+ collator.compare(o1.language, o2.language)
+ }
+
+ translations.postValue(result)
+ }
+ } catch (e: Exception) {
+ logger.log(Level.WARNING, "Couldn't load translators", e)
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutScreen.kt
new file mode 100644
index 000000000..72f1649c9
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutScreen.kt
@@ -0,0 +1,250 @@
+package at.bitfire.davdroid.ui.about
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.pager.HorizontalPager
+import androidx.compose.foundation.pager.rememberPagerState
+import androidx.compose.foundation.rememberScrollState
+import androidx.compose.foundation.verticalScroll
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.filled.Home
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Scaffold
+import androidx.compose.material3.Tab
+import androidx.compose.material3.TabRow
+import androidx.compose.material3.Text
+import androidx.compose.material3.TopAppBar
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalUriHandler
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.tooling.preview.Preview
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import at.bitfire.davdroid.BuildConfig
+import at.bitfire.davdroid.Constants
+import at.bitfire.davdroid.Constants.withStatParams
+import at.bitfire.davdroid.R
+import at.bitfire.davdroid.ui.UiUtils
+import at.bitfire.davdroid.ui.composable.PixelBoxes
+import at.bitfire.davdroid.ui.composition.LocalNavController
+import com.mikepenz.aboutlibraries.Libs
+import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
+import com.mikepenz.aboutlibraries.util.withJson
+import kotlinx.coroutines.launch
+import java.text.Collator
+
+@Composable
+@OptIn(ExperimentalMaterial3Api::class)
+fun AboutScreen(model: AboutModel = hiltViewModel()) {
+ val uriHandler = LocalUriHandler.current
+ val navController = LocalNavController.current
+
+ Scaffold(
+ topBar = {
+ TopAppBar(
+ navigationIcon = {
+ IconButton(onClick = { navController.popBackStack() }) {
+ Icon(
+ Icons.AutoMirrored.Default.ArrowBack,
+ contentDescription = stringResource(R.string.navigate_up)
+ )
+ }
+ },
+ title = {
+ Text(stringResource(R.string.navigation_drawer_about))
+ },
+ actions = {
+ IconButton(onClick = {
+ uriHandler.openUri(
+ Constants.HOMEPAGE_URL
+ .buildUpon()
+ .withStatParams("AboutActivity")
+ .build().toString())
+ }) {
+ Icon(
+ Icons.Default.Home,
+ contentDescription = stringResource(R.string.navigation_drawer_website)
+ )
+ }
+ }
+ )
+ }
+ ) { paddingValues ->
+ Column(Modifier.padding(paddingValues)) {
+ val scope = rememberCoroutineScope()
+ val state = rememberPagerState(pageCount = { 3 })
+
+ TabRow(state.currentPage) {
+ Tab(state.currentPage == 0, onClick = {
+ scope.launch { state.scrollToPage(0) }
+ }) {
+ Text(
+ stringResource(R.string.app_name),
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ Tab(state.currentPage == 1, onClick = {
+ scope.launch { state.scrollToPage(1) }
+ }) {
+ Text(
+ stringResource(R.string.about_translations),
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ Tab(state.currentPage == 2, onClick = {
+ scope.launch { state.scrollToPage(2) }
+ }) {
+ Text(
+ stringResource(R.string.about_libraries),
+ modifier = Modifier.padding(8.dp)
+ )
+ }
+ }
+
+ HorizontalPager(
+ state,
+ modifier = Modifier
+ .fillMaxWidth()
+ .weight(1f),
+ verticalAlignment = Alignment.Top
+ ) { index ->
+ when (index) {
+ 0 -> AboutApp(licenseInfoProvider = model.licenseInfoProvider)
+ 1 -> {
+ val translations = model.translations.observeAsState(emptyList())
+ TranslatorsGallery(translations.value)
+ }
+
+ 2 -> LibrariesContainer(
+ Modifier.fillMaxSize(),
+ itemContentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
+ itemSpacing = 8.dp,
+ librariesBlock = { ctx ->
+ Libs.Builder()
+ .withJson(ctx, R.raw.aboutlibraries)
+ .build()
+ })
+ }
+ }
+ }
+ }
+}
+
+@Composable
+fun AboutApp(licenseInfoProvider: AppLicenseInfoProvider? = null) {
+ Column(
+ modifier = Modifier
+ .padding(8.dp)
+ .fillMaxWidth()
+ .verticalScroll(rememberScrollState())) {
+ Image(
+ UiUtils.adaptiveIconPainterResource(R.mipmap.ic_launcher),
+ contentDescription = stringResource(R.string.app_name),
+ modifier = Modifier
+ .size(128.dp)
+ .align(Alignment.CenterHorizontally)
+ )
+ Text(
+ stringResource(R.string.app_name),
+ style = MaterialTheme.typography.headlineMedium,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(8.dp)
+ )
+
+ Text(
+ stringResource(R.string.about_version, BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ modifier = Modifier.fillMaxWidth()
+ )
+
+ Text(
+ stringResource(R.string.about_copyright),
+ style = MaterialTheme.typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(vertical = 8.dp)
+ )
+
+ Text(
+ stringResource(R.string.about_license_info_no_warranty),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 8.dp)
+ )
+
+ PixelBoxes(
+ arrayOf(Color(0xFFFCF434), Color.White, Color(0xFF9C59D1), Color.Black),
+ modifier = Modifier
+ .align(Alignment.CenterHorizontally)
+ .padding(16.dp)
+ )
+
+ licenseInfoProvider?.LicenseInfo()
+ }
+}
+
+@Composable
+@Preview
+fun AboutApp_Preview() {
+ AboutApp(licenseInfoProvider = object : AppLicenseInfoProvider {
+ @Composable
+ override fun LicenseInfo() {
+ Text("Some flavored License Info")
+ }
+ })
+}
+
+
+@Composable
+fun TranslatorsGallery(
+ translations: List
+) {
+ val collator = Collator.getInstance()
+ LazyColumn(Modifier.padding(8.dp)) {
+ items(translations) { translation ->
+ Text(
+ translation.language,
+ style = MaterialTheme.typography.headlineMedium,
+ modifier = Modifier.padding(vertical = 4.dp)
+ )
+ Text(
+ translation.translators
+ .sortedWith { a, b -> collator.compare(a, b) }
+ .joinToString(" · "),
+ style = MaterialTheme.typography.bodyLarge,
+ modifier = Modifier.padding(bottom = 16.dp)
+ )
+ }
+ }
+}
+
+@Composable
+@Preview
+fun TranslatorsGallery_Sample() {
+ TranslatorsGallery(listOf(
+ AboutModel.Translation("Some Language", setOf("User 1", "User 2")),
+ AboutModel.Translation("Another Language", setOf("User 3", "User 4"))
+ ))
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProvider.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProvider.kt
new file mode 100644
index 000000000..9380c5a62
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProvider.kt
@@ -0,0 +1,8 @@
+package at.bitfire.davdroid.ui.about
+
+import androidx.compose.runtime.Composable
+
+interface AppLicenseInfoProvider {
+ @Composable
+ fun LicenseInfo()
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProviderModule.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProviderModule.kt
new file mode 100644
index 000000000..4bb35c8b0
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/about/AppLicenseInfoProviderModule.kt
@@ -0,0 +1,13 @@
+package at.bitfire.davdroid.ui.about
+
+import dagger.BindsOptionalOf
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+interface AppLicenseInfoProviderModule {
+ @BindsOptionalOf
+ fun appLicenseInfoProvider(): AppLicenseInfoProvider
+}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt
index 39f4c5efa..90db413c4 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/BatteryOptimizationsPageContent.kt
@@ -30,8 +30,8 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
@@ -41,7 +41,7 @@ import java.util.Locale
@Composable
fun BatteryOptimizationsPageContent(
- model: BatteryOptimizationsPageModel = viewModel()
+ model: BatteryOptimizationsPageModel = hiltViewModel()
) {
val ignoreBatteryOptimizationsResultLauncher = rememberLauncherForActivityResult(
BatteryOptimizationsPage.IgnoreBatteryOptimizationsContract
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt
deleted file mode 100644
index 97d919f73..000000000
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroActivity.kt
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
- */
-
-package at.bitfire.davdroid.ui.intro
-
-import android.app.Activity
-import android.content.Context
-import android.content.Intent
-import android.os.Bundle
-import androidx.activity.compose.BackHandler
-import androidx.activity.compose.setContent
-import androidx.activity.result.contract.ActivityResultContract
-import androidx.activity.viewModels
-import androidx.appcompat.app.AppCompatActivity
-import androidx.compose.foundation.pager.rememberPagerState
-import androidx.compose.runtime.rememberCoroutineScope
-import at.bitfire.davdroid.ui.AppTheme
-import dagger.hilt.android.AndroidEntryPoint
-import kotlinx.coroutines.launch
-
-@AndroidEntryPoint
-class IntroActivity : AppCompatActivity() {
-
- val model by viewModels()
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- val pages = model.pages
-
- setContent {
- AppTheme {
- val scope = rememberCoroutineScope()
- val pagerState = rememberPagerState { pages.size }
-
- BackHandler {
- if (pagerState.settledPage == 0) {
- setResult(Activity.RESULT_CANCELED)
- finish()
- } else scope.launch {
- pagerState.animateScrollToPage(pagerState.settledPage - 1)
- }
- }
-
- IntroScreen(
- pages = pages,
- pagerState = pagerState,
- onDonePressed = {
- setResult(Activity.RESULT_OK)
- finish()
- }
- )
- }
- }
- }
-
-
- /**
- * For launching the [IntroActivity]. Result is `true` when the user cancelled the intro.
- */
- object Contract: ActivityResultContract() {
- override fun createIntent(context: Context, input: Unit?): Intent =
- Intent(context, IntroActivity::class.java)
-
- override fun parseResult(resultCode: Int, intent: Intent?): Boolean {
- return resultCode == Activity.RESULT_CANCELED
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt
index ec159b2e7..190c78180 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/IntroScreen.kt
@@ -4,6 +4,7 @@
package at.bitfire.davdroid.ui.intro
+import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.Canvas
@@ -53,11 +54,42 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.M3ColorScheme
+import at.bitfire.davdroid.ui.composition.LocalNavController
import kotlinx.coroutines.launch
+const val INTRO_CANCELLED = "cancelled"
+
+@Composable
+fun IntroScreen(
+ model: IntroModel = hiltViewModel()
+) {
+ val navController = LocalNavController.current
+
+ val scope = rememberCoroutineScope()
+ val pagerState = rememberPagerState { model.pages.size }
+
+ BackHandler {
+ if (pagerState.settledPage == 0) {
+ navController.previousBackStackEntry
+ ?.savedStateHandle
+ ?.set(INTRO_CANCELLED, true)
+ navController.popBackStack()
+ } else scope.launch {
+ pagerState.animateScrollToPage(pagerState.settledPage - 1)
+ }
+ }
+
+ IntroScreen(
+ pages = model.pages,
+ pagerState = pagerState,
+ onDonePressed = { navController.popBackStack() }
+ )
+}
+
@Composable
fun IntroScreen(
pages: List,
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt
index 642830595..5d76b520d 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/intro/OpenSourcePage.kt
@@ -26,9 +26,9 @@ import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.Constants
import at.bitfire.davdroid.Constants.withStatParams
import at.bitfire.davdroid.R
@@ -54,7 +54,7 @@ class OpenSourcePage @Inject constructor(
}
@Composable
- private fun Page(model: Model = viewModel()) {
+ private fun Page(model: Model = hiltViewModel()) {
val dontShow by model.dontShow.collectAsStateWithLifecycle(false)
OpenSourcePage(
dontShow = dontShow,
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/NavController.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/NavController.kt
new file mode 100644
index 000000000..71991c542
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/NavController.kt
@@ -0,0 +1,6 @@
+package at.bitfire.davdroid.ui.navigation
+
+import androidx.compose.runtime.compositionLocalOf
+import androidx.navigation.NavController
+
+val LocalNavController = compositionLocalOf { error("No NavController attached.") }
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Routes.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Routes.kt
new file mode 100644
index 000000000..b217cd463
--- /dev/null
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/navigation/Routes.kt
@@ -0,0 +1,14 @@
+package at.bitfire.davdroid.ui.navigation
+
+import kotlinx.serialization.Serializable
+
+object Routes {
+ @Serializable
+ data class Accounts(val syncAccounts: Boolean)
+
+ @Serializable
+ data object Intro
+
+ @Serializable
+ data object About
+}
diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
index 3efcf73f0..28e1c6a19 100644
--- a/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
+++ b/app/src/ose/kotlin/at/bitfire/davdroid/OseFlavorModule.kt
@@ -4,10 +4,10 @@
package at.bitfire.davdroid
-import at.bitfire.davdroid.ui.AboutActivity
import at.bitfire.davdroid.ui.AccountsDrawerHandler
import at.bitfire.davdroid.ui.OpenSourceLicenseInfoProvider
import at.bitfire.davdroid.ui.OseAccountsDrawerHandler
+import at.bitfire.davdroid.ui.about.AppLicenseInfoProvider
import at.bitfire.davdroid.ui.intro.IntroPageFactory
import at.bitfire.davdroid.ui.setup.LoginTypesProvider
import at.bitfire.davdroid.ui.setup.StandardLoginTypesProvider
@@ -34,7 +34,7 @@ interface OseFlavorModules {
@InstallIn(ViewModelComponent::class)
interface ForViewModels {
@Binds
- fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AboutActivity.AppLicenseInfoProvider
+ fun appLicenseInfoProvider(impl: OpenSourceLicenseInfoProvider): AppLicenseInfoProvider
@Binds
fun loginTypesProvider(impl: StandardLoginTypesProvider): LoginTypesProvider
diff --git a/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt b/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt
index a076d63c7..065fc12a2 100644
--- a/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt
+++ b/app/src/ose/kotlin/at/bitfire/davdroid/ui/OpenSourceLicenseInfoProvider.kt
@@ -14,17 +14,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.core.text.HtmlCompat
+import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
-import androidx.lifecycle.viewmodel.compose.viewModel
import at.bitfire.davdroid.ui.UiUtils.toAnnotatedString
+import at.bitfire.davdroid.ui.about.AppLicenseInfoProvider
import com.google.common.io.CharStreams
import dagger.hilt.android.lifecycle.HiltViewModel
-import javax.inject.Inject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import javax.inject.Inject
-class OpenSourceLicenseInfoProvider @Inject constructor(): AboutActivity.AppLicenseInfoProvider {
+class OpenSourceLicenseInfoProvider @Inject constructor(): AppLicenseInfoProvider {
@Composable
override fun LicenseInfo() {
@@ -33,7 +34,7 @@ class OpenSourceLicenseInfoProvider @Inject constructor(): AboutActivity.AppLice
@Composable
fun LicenseInfoGpl(
- model: Model = viewModel()
+ model: Model = hiltViewModel()
) {
model.gpl?.let { OpenSourceLicenseInfo(it.toAnnotatedString()) }
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index b2f9722c5..fa4f092b5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -24,6 +24,7 @@ bitfire-ical4android = "12df9bfddb"
bitfire-vcard4android = "ae5d609f92"
compose-accompanist = "0.37.0"
compose-bom = "2024.12.01"
+compose-navigation = "2.8.5"
dnsjava = "3.6.0"
glance = "1.1.1"
guava = "33.4.0-android"
@@ -31,6 +32,7 @@ hilt = "2.55"
# keep in sync with ksp version
kotlin = "2.1.0"
kotlinx-coroutines = "1.10.1"
+kotlinx-serialization = "1.7.3"
# see https://github.com/google/ksp/releases for version numbers
ksp = "2.1.0-1.0.29"
mikepenz-aboutLibraries = "11.4.0"
@@ -72,6 +74,7 @@ compose-accompanist-permissions = { module = "com.google.accompanist:accompanist
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
compose-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-materialIconsExtended = { module = "androidx.compose.material:material-icons-extended" }
+compose-navigation = { module = "androidx.navigation:navigation-compose", version.ref = "compose-navigation" }
compose-runtime-livedata = { module = "androidx.compose.runtime:runtime-livedata" }
compose-ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
compose-ui-toolingPreview = { module = "androidx.compose.ui:ui-tooling-preview" }
@@ -85,6 +88,7 @@ hilt-android-testing = { module = "com.google.dagger:hilt-android-testing", vers
junit = { module = "junit:junit", version = "4.13.2" }
kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
mikepenz-aboutLibraries = { module = "com.mikepenz:aboutlibraries-compose-m3", version.ref = "mikepenz-aboutLibraries" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
@@ -106,5 +110,6 @@ android-application = { id = "com.android.application", version.ref = "android-a
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
+kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
mikepenz-aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "mikepenz-aboutLibraries" }