mirror of
https://github.com/bitfireAT/davx5-ose.git
synced 2026-01-21 13:17:56 -05:00
Compare commits
18 Commits
backups-sc
...
migrate-in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
958be01a79 | ||
|
|
597a3d293e | ||
|
|
0e59334e68 | ||
|
|
d8aad544ff | ||
|
|
5b935263d3 | ||
|
|
013cb915fd | ||
|
|
754c971fb9 | ||
|
|
282f1d1db6 | ||
|
|
52fde0c9f7 | ||
|
|
97478fb7a3 | ||
|
|
bdae74189b | ||
|
|
b2785bc296 | ||
|
|
0d9be98547 | ||
|
|
b8b38b600a | ||
|
|
a544e53267 | ||
|
|
a02559ca9a | ||
|
|
ddf881a504 | ||
|
|
777b419a60 |
@@ -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)
|
||||
|
||||
@@ -60,9 +60,8 @@
|
||||
<activity android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
tools:node="remove" tools:selector="net.openid.appauth"/>
|
||||
|
||||
<activity android:name=".ui.intro.IntroActivity" />
|
||||
<activity
|
||||
android:name=".ui.AccountsActivity"
|
||||
android:name=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
@@ -70,15 +69,18 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".ui.AboutActivity"
|
||||
android:label="@string/navigation_drawer_about"
|
||||
android:parentActivityName=".ui.AccountsActivity"/>
|
||||
<!--
|
||||
Left here for external apps.
|
||||
Automatically redirects to MainActivity.
|
||||
Should be removed in the future.
|
||||
-->
|
||||
<!--suppress DeprecatedClassUsageInspection -->
|
||||
<activity android:name=".ui.AccountsActivity" android:exported="true" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.AppSettingsActivity"
|
||||
android:label="@string/app_settings"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:parentActivityName=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES"/>
|
||||
@@ -106,7 +108,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".ui.setup.LoginActivity"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:parentActivityName=".ui.MainActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
@@ -134,7 +136,7 @@
|
||||
|
||||
<activity
|
||||
android:name=".ui.account.AccountActivity"
|
||||
android:parentActivityName=".ui.AccountsActivity"
|
||||
android:parentActivityName=".ui.MainActivity"
|
||||
android:exported="true">
|
||||
</activity>
|
||||
<activity
|
||||
@@ -156,7 +158,7 @@
|
||||
<activity
|
||||
android:name=".ui.webdav.WebdavMountsActivity"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".ui.AccountsActivity" />
|
||||
android:parentActivityName=".ui.MainActivity" />
|
||||
<activity
|
||||
android:name=".ui.webdav.AddWebdavMountActivity"
|
||||
android:parentActivityName=".ui.webdav.WebdavMountsActivity"
|
||||
|
||||
@@ -21,8 +21,9 @@ import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TextTable
|
||||
import at.bitfire.davdroid.db.migration.*
|
||||
import at.bitfire.davdroid.ui.AccountsActivity
|
||||
import at.bitfire.davdroid.db.migration.AutoMigration12
|
||||
import at.bitfire.davdroid.db.migration.AutoMigration16
|
||||
import at.bitfire.davdroid.ui.MainActivity
|
||||
import at.bitfire.davdroid.ui.NotificationRegistry
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
@@ -74,7 +75,7 @@ abstract class AppDatabase: RoomDatabase() {
|
||||
.addCallback(object: Callback() {
|
||||
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
|
||||
notificationRegistry.notifyIfPossible(NotificationRegistry.NOTIFY_DATABASE_CORRUPTED) {
|
||||
val launcherIntent = Intent(context, AccountsActivity::class.java)
|
||||
val launcherIntent = Intent(context, MainActivity::class.java)
|
||||
NotificationCompat.Builder(context, notificationRegistry.CHANNEL_GENERAL)
|
||||
.setSmallIcon(R.drawable.ic_warning_notify)
|
||||
.setContentTitle(context.getString(R.string.database_destructive_migration_title))
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
/*
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
*/
|
||||
|
||||
package at.bitfire.davdroid.ui
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
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.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
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.composable.PixelBoxes
|
||||
import com.mikepenz.aboutlibraries.Libs
|
||||
import com.mikepenz.aboutlibraries.ui.compose.m3.LibrariesContainer
|
||||
import com.mikepenz.aboutlibraries.util.withJson
|
||||
import dagger.BindsOptionalOf
|
||||
import dagger.Module
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.components.ActivityComponent
|
||||
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.time.LocalDateTime
|
||||
import java.time.ZoneOffset
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import java.util.LinkedList
|
||||
import java.util.Locale
|
||||
import java.util.Optional
|
||||
import java.util.logging.Level
|
||||
import java.util.logging.Logger
|
||||
import javax.inject.Inject
|
||||
import kotlin.jvm.optionals.getOrNull
|
||||
|
||||
@AndroidEntryPoint
|
||||
class AboutActivity: AppCompatActivity() {
|
||||
|
||||
val model by viewModels<Model>()
|
||||
|
||||
@Inject
|
||||
lateinit var licenseInfoProvider: Optional<AppLicenseInfoProvider>
|
||||
|
||||
|
||||
@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<String>
|
||||
)
|
||||
|
||||
val translations = MutableLiveData<List<Translation>>()
|
||||
|
||||
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<Translation>()
|
||||
for (langCode in jsonTranslations.keys()) {
|
||||
val jsonTranslators = jsonTranslations.getJSONArray(langCode)
|
||||
val translators = Array<String>(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<AboutActivity.Model.Translation>
|
||||
) {
|
||||
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"))
|
||||
))
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -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<Routes.Accounts>()
|
||||
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,
|
||||
|
||||
57
app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
Normal file
57
app/src/main/kotlin/at/bitfire/davdroid/ui/MainActivity.kt
Normal file
@@ -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<Routes.Accounts> { AccountsScreen(it, accountsDrawerHandler) }
|
||||
composable<Routes.Intro> { IntroScreen() }
|
||||
composable<Routes.About> { 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
|
||||
)
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<String>
|
||||
)
|
||||
|
||||
val translations = MutableLiveData<List<Translation>>()
|
||||
|
||||
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<Translation>()
|
||||
for (langCode in jsonTranslations.keys()) {
|
||||
val jsonTranslators = jsonTranslations.getJSONArray(langCode)
|
||||
val translators = Array<String>(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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
250
app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutScreen.kt
Normal file
250
app/src/main/kotlin/at/bitfire/davdroid/ui/about/AboutScreen.kt
Normal file
@@ -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<AboutModel.Translation>
|
||||
) {
|
||||
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"))
|
||||
))
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package at.bitfire.davdroid.ui.about
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
|
||||
interface AppLicenseInfoProvider {
|
||||
@Composable
|
||||
fun LicenseInfo()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<IntroModel>()
|
||||
|
||||
|
||||
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<Unit?, Boolean>() {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<IntroPage>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
package at.bitfire.davdroid.ui.navigation
|
||||
|
||||
import androidx.compose.runtime.compositionLocalOf
|
||||
import androidx.navigation.NavController
|
||||
|
||||
val LocalNavController = compositionLocalOf<NavController> { error("No NavController attached.") }
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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()) }
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
|
||||
Reference in New Issue
Block a user