mirror of https://github.com/tasks/tasks
Compare commits
No commits in common. 'main' and '14.6.2' have entirely different histories.
@ -1 +1 @@
|
||||
3.4.7
|
||||
3.3.6
|
||||
|
||||
@ -1,25 +0,0 @@
|
||||
package org.tasks.data
|
||||
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import org.tasks.data.dao.CaldavDao
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.injection.InjectingTestCase
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class CaldavDaoExtensionsTest : InjectingTestCase() {
|
||||
@Inject lateinit var caldavDao: CaldavDao
|
||||
|
||||
@Test
|
||||
fun getLocalListCreatesAccountIfNeeded() = runBlocking {
|
||||
withTimeout(5000L) {
|
||||
assertTrue(caldavDao.getAccounts().isEmpty())
|
||||
caldavDao.getLocalList()
|
||||
assertTrue(caldavDao.getAccounts(CaldavAccount.TYPE_LOCAL).isNotEmpty())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"redirect_uri" : "msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D",
|
||||
"account_mode" : "MULTIPLE",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "verbose",
|
||||
"logcat_enabled": true,
|
||||
"pii_enabled": true
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<manifest>
|
||||
|
||||
<application tools:ignore="MissingApplicationIcon">
|
||||
<activity
|
||||
android:name=".auth.MicrosoftAuthenticationActivity"
|
||||
android:theme="@style/TranslucentDialog"/>
|
||||
<activity
|
||||
android:name="net.openid.appauth.RedirectUriReceiverActivity"
|
||||
android:exported="true"
|
||||
tools:node="merge">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<category android:name="android.intent.category.DEFAULT"/>
|
||||
<category android:name="android.intent.category.BROWSABLE"/>
|
||||
<data
|
||||
android:host="${applicationId}"
|
||||
android:scheme="msauth" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
<application/>
|
||||
|
||||
</manifest>
|
||||
|
||||
@ -1,38 +0,0 @@
|
||||
package org.tasks.auth
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
import net.openid.appauth.AuthorizationServiceConfiguration
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
data class IdentityProvider(
|
||||
val name: String,
|
||||
val discoveryEndpoint: Uri,
|
||||
val clientId: String,
|
||||
val redirectUri: Uri,
|
||||
val scope: String
|
||||
) {
|
||||
suspend fun retrieveConfig(): AuthorizationServiceConfiguration {
|
||||
return suspendCoroutine { cont ->
|
||||
AuthorizationServiceConfiguration.fetchFromUrl(discoveryEndpoint) { serviceConfiguration, ex ->
|
||||
cont.resumeWith(
|
||||
when {
|
||||
ex != null -> Result.failure(ex)
|
||||
serviceConfiguration != null -> Result.success(serviceConfiguration)
|
||||
else -> Result.failure(IllegalStateException())
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val MICROSOFT = IdentityProvider(
|
||||
"Microsoft",
|
||||
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
|
||||
"9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
|
||||
"user.read Tasks.ReadWrite openid offline_access email"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import net.openid.appauth.AuthState
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.security.KeyStoreEncryption
|
||||
import javax.inject.Inject
|
||||
|
||||
class MicrosoftTokenProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val encryption: KeyStoreEncryption,
|
||||
) {
|
||||
suspend fun getToken(account: CaldavAccount): String {
|
||||
val authState = encryption.decrypt(account.password)?.let { AuthState.jsonDeserialize(it) }
|
||||
?: throw RuntimeException("Missing credentials")
|
||||
if (authState.needsTokenRefresh) {
|
||||
val (token, ex) = context.requestTokenRefresh(authState)
|
||||
authState.update(token, ex)
|
||||
if (authState.isAuthorized) {
|
||||
account.password = encryption.encrypt(authState.jsonSerializeString())
|
||||
}
|
||||
}
|
||||
if (!authState.isAuthorized) {
|
||||
throw RuntimeException("Needs authentication")
|
||||
}
|
||||
return authState.accessToken!!
|
||||
}
|
||||
}
|
||||
@ -1,100 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.app.Activity
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.microsoft.identity.client.AcquireTokenParameters
|
||||
import com.microsoft.identity.client.AuthenticationCallback
|
||||
import com.microsoft.identity.client.IAuthenticationResult
|
||||
import com.microsoft.identity.client.Prompt
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import com.microsoft.identity.client.exception.MsalException
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Constants
|
||||
import org.tasks.analytics.Firebase
|
||||
import org.tasks.data.UUIDHelper
|
||||
import org.tasks.data.dao.CaldavDao
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_MICROSOFT
|
||||
import org.tasks.extensions.Context.toast
|
||||
import org.tasks.jobs.WorkManager
|
||||
import org.tasks.sync.SyncAdapters
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class MicrosoftSignInViewModel @Inject constructor(
|
||||
private val caldavDao: CaldavDao,
|
||||
private val firebase: Firebase,
|
||||
private val syncAdapters: SyncAdapters,
|
||||
private val workManager: WorkManager,
|
||||
) : ViewModel() {
|
||||
fun signIn(activity: Activity) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
|
||||
activity,
|
||||
R.raw.microsoft_config
|
||||
)
|
||||
|
||||
val parameters = AcquireTokenParameters.Builder()
|
||||
.startAuthorizationFromActivity(activity)
|
||||
.withScopes(scopes)
|
||||
.withPrompt(Prompt.SELECT_ACCOUNT)
|
||||
.withCallback(object : AuthenticationCallback {
|
||||
override fun onSuccess(authenticationResult: IAuthenticationResult) {
|
||||
val email = authenticationResult.account.claims?.get("preferred_username") as? String
|
||||
if (email == null) {
|
||||
Timber.e("No email found")
|
||||
return
|
||||
}
|
||||
Timber.d("Successfully signed in")
|
||||
viewModelScope.launch {
|
||||
caldavDao
|
||||
.getAccount(TYPE_MICROSOFT, email)
|
||||
?.let {
|
||||
caldavDao.update(
|
||||
it.copy(error = null)
|
||||
)
|
||||
}
|
||||
?: caldavDao
|
||||
.insert(
|
||||
CaldavAccount(
|
||||
uuid = UUIDHelper.newUUID(),
|
||||
name = email,
|
||||
username = email,
|
||||
accountType = TYPE_MICROSOFT,
|
||||
)
|
||||
)
|
||||
.also {
|
||||
firebase.logEvent(
|
||||
R.string.event_sync_add_account,
|
||||
R.string.param_type to Constants.SYNC_TYPE_MICROSOFT
|
||||
)
|
||||
}
|
||||
syncAdapters.sync(true)
|
||||
workManager.updateBackgroundSync()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(exception: MsalException?) {
|
||||
Timber.e(exception)
|
||||
activity.toast(exception?.message ?: exception?.javaClass?.simpleName ?: "Sign in failed")
|
||||
}
|
||||
|
||||
override fun onCancel() {
|
||||
Timber.d("onCancel")
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
app.acquireToken(parameters)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val scopes = listOf("https://graph.microsoft.com/.default")
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package org.tasks.sync.microsoft
|
||||
|
||||
import android.content.Context
|
||||
import com.microsoft.identity.client.AcquireTokenSilentParameters
|
||||
import com.microsoft.identity.client.PublicClientApplication
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import org.tasks.R
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
|
||||
class MicrosoftTokenProvider @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
) {
|
||||
fun getToken(account: CaldavAccount): String {
|
||||
val app = PublicClientApplication.createMultipleAccountPublicClientApplication(
|
||||
context,
|
||||
R.raw.microsoft_config
|
||||
)
|
||||
|
||||
val result = try {
|
||||
val msalAccount = app.accounts.firstOrNull { it.username == account.username }
|
||||
?: throw RuntimeException("No matching account found")
|
||||
|
||||
val parameters = AcquireTokenSilentParameters.Builder()
|
||||
.withScopes(MicrosoftSignInViewModel.scopes)
|
||||
.forAccount(msalAccount)
|
||||
.fromAuthority(msalAccount.authority)
|
||||
.forceRefresh(true)
|
||||
.build()
|
||||
|
||||
app.acquireTokenSilent(parameters)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
throw RuntimeException("Authentication failed: ${e.message}")
|
||||
}
|
||||
return result.accessToken
|
||||
}
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"client_id" : "9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"authorization_user_agent" : "DEFAULT",
|
||||
"redirect_uri" : "msauth://org.tasks/sEe08kX5nGJi4miFX3VkNXICC%2FY%3D",
|
||||
"account_mode" : "MULTIPLE",
|
||||
"authorities" : [
|
||||
{
|
||||
"type": "AAD",
|
||||
"audience": {
|
||||
"type": "AzureADandPersonalMicrosoftAccount",
|
||||
"tenant_id": "common"
|
||||
}
|
||||
}
|
||||
],
|
||||
"logging": {
|
||||
"level": "info",
|
||||
"logcat_enabled": true,
|
||||
"pii_enabled": false
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<string name="google_oauth_scheme">com.googleusercontent.apps.363426363175-jdrijf7hql9030klgjcjlpi6k5spviif</string>
|
||||
<string name="microsoft_oauth_path">/sEe08kX5nGJi4miFX3VkNXICC/Y=</string>
|
||||
</resources>
|
||||
@ -0,0 +1,22 @@
|
||||
package org.tasks.auth
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.core.net.toUri
|
||||
|
||||
data class IdentityProvider(
|
||||
val name: String,
|
||||
val discoveryEndpoint: Uri,
|
||||
val clientId: String,
|
||||
val redirectUri: Uri,
|
||||
val scope: String
|
||||
) {
|
||||
companion object {
|
||||
val MICROSOFT = IdentityProvider(
|
||||
"Microsoft",
|
||||
"https://login.microsoftonline.com/consumers/v2.0/.well-known/openid-configuration".toUri(),
|
||||
"9d4babd5-e7ba-4286-ba4b-17274495a901",
|
||||
"msauth://org.tasks/8wnYBRqh5nnQgFzbIXfxXSs41xE%3D".toUri(),
|
||||
"user.read Tasks.ReadWrite openid offline_access email"
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,16 @@
|
||||
package org.tasks.caldav
|
||||
|
||||
import android.content.Context
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class FileStorage(
|
||||
rootPath: String
|
||||
class FileStorage @Inject constructor(
|
||||
@ApplicationContext context: Context
|
||||
) {
|
||||
val root = File(rootPath, "vtodo")
|
||||
val root = File(context.filesDir, "vtodo")
|
||||
|
||||
@Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS")
|
||||
fun getFile(vararg segments: String?): File? =
|
||||
@ -1,90 +0,0 @@
|
||||
package org.tasks.caldav
|
||||
|
||||
import android.app.Activity
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.analytics.Constants
|
||||
import org.tasks.data.UUIDHelper
|
||||
import org.tasks.data.entity.CaldavAccount
|
||||
|
||||
@AndroidEntryPoint
|
||||
class LocalAccountSettingsActivity : BaseCaldavAccountSettingsActivity(), Toolbar.OnMenuItemClickListener {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding.userLayout.visibility = View.GONE
|
||||
binding.passwordLayout.visibility = View.GONE
|
||||
binding.urlLayout.visibility = View.GONE
|
||||
binding.serverSelector.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun hasChanges() = newName != caldavAccount!!.name
|
||||
|
||||
override fun save() = lifecycleScope.launch {
|
||||
if (newName.isBlank()) {
|
||||
binding.nameLayout.error = getString(R.string.name_cannot_be_empty)
|
||||
return@launch
|
||||
}
|
||||
updateAccount()
|
||||
}
|
||||
|
||||
private suspend fun addAccount() {
|
||||
caldavDao.insert(
|
||||
CaldavAccount(
|
||||
name = newName,
|
||||
uuid = UUIDHelper.newUUID(),
|
||||
)
|
||||
)
|
||||
firebase.logEvent(
|
||||
R.string.event_sync_add_account,
|
||||
R.string.param_type to Constants.SYNC_TYPE_LOCAL
|
||||
)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
override suspend fun updateAccount() {
|
||||
caldavAccount!!.name = newName
|
||||
caldavDao.update(caldavAccount!!)
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
}
|
||||
|
||||
override suspend fun addAccount(url: String, username: String, password: String) {
|
||||
addAccount()
|
||||
}
|
||||
|
||||
override suspend fun updateAccount(url: String, username: String, password: String) {
|
||||
updateAccount()
|
||||
}
|
||||
|
||||
override suspend fun removeAccountPrompt() {
|
||||
val countTasks = caldavAccount?.uuid?.let { caldavDao.countTasks(it) } ?: 0
|
||||
val countString = resources.getQuantityString(R.plurals.task_count, countTasks, countTasks)
|
||||
dialogBuilder
|
||||
.newDialog()
|
||||
.setTitle(
|
||||
R.string.delete_tag_confirmation,
|
||||
caldavAccount?.name?.takeIf { it.isNotBlank() } ?: getString(R.string.local_lists)
|
||||
)
|
||||
.apply {
|
||||
if (countTasks > 0) {
|
||||
setMessage(R.string.delete_tasks_warning, countString)
|
||||
} else {
|
||||
setMessage(R.string.logout_warning)
|
||||
}
|
||||
}
|
||||
.setPositiveButton(R.string.delete) { _, _ -> lifecycleScope.launch { removeAccount() } }
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
override val newPassword: String? = null
|
||||
|
||||
override val helpUrl = R.string.url_caldav
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package org.tasks.caldav.property
|
||||
|
||||
import at.bitfire.dav4jvm.Property
|
||||
import at.bitfire.dav4jvm.PropertyFactory
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import org.xmlpull.v1.XmlPullParser
|
||||
import timber.log.Timber
|
||||
|
||||
data class CalendarIcon(
|
||||
val icon: String,
|
||||
): Property {
|
||||
companion object Companion {
|
||||
@JvmField
|
||||
val NAME = Property.Name(PropertyUtils.NS_TASKS, "x-calendar-icon")
|
||||
}
|
||||
|
||||
object Factory: PropertyFactory {
|
||||
|
||||
override fun getName() = NAME
|
||||
|
||||
override fun create(parser: XmlPullParser): CalendarIcon? {
|
||||
XmlUtils.readText(parser)?.takeIf { it.isNotBlank() }?.let {
|
||||
try {
|
||||
return CalendarIcon(it)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
Timber.e(e, "Couldn't parse icon: $it")
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
package org.tasks.caldav.property
|
||||
|
||||
import at.bitfire.dav4jvm.PropertyFactory
|
||||
import at.bitfire.dav4jvm.PropertyRegistry
|
||||
|
||||
object PropertyUtils {
|
||||
const val NS_TASKS = "http://org.tasks/ns/"
|
||||
const val NS_OWNCLOUD = "http://owncloud.org/ns"
|
||||
|
||||
fun PropertyRegistry.register(vararg factories: PropertyFactory) = register(factories.toList())
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
package org.tasks.compose
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object HomeDestination
|
||||
|
||||
@Serializable
|
||||
data class AddAccountDestination(val showImport: Boolean)
|
||||
@ -1,348 +0,0 @@
|
||||
package org.tasks.compose.accounts
|
||||
|
||||
import androidx.activity.compose.BackHandler
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||
import androidx.compose.material.icons.outlined.Backup
|
||||
import androidx.compose.material.icons.outlined.CloudOff
|
||||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedCard
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.text.withStyle
|
||||
import androidx.compose.ui.tooling.preview.PreviewFontScale
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.tasks.R
|
||||
import org.tasks.sync.AddAccountDialog.Platform
|
||||
import org.tasks.themes.TasksTheme
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun AddAccountScreen(
|
||||
gettingStarted: Boolean,
|
||||
hasTasksAccount: Boolean,
|
||||
hasPro: Boolean,
|
||||
onBack: () -> Unit,
|
||||
signIn: (Platform) -> Unit,
|
||||
openUrl: (Platform) -> Unit,
|
||||
onImportBackup: () -> Unit,
|
||||
) {
|
||||
BackHandler {
|
||||
if (!gettingStarted) {
|
||||
onBack()
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
colors = TopAppBarDefaults.topAppBarColors(),
|
||||
navigationIcon = {
|
||||
if (!gettingStarted) {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||
contentDescription = stringResource(R.string.back),
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(
|
||||
text = if (gettingStarted) {
|
||||
stringResource(R.string.sign_in)
|
||||
} else {
|
||||
stringResource(R.string.add_account)
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(vertical = 16.dp)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(rememberScrollState()),
|
||||
) {
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
maxItemsInEachRow = 5
|
||||
) {
|
||||
if (gettingStarted) {
|
||||
ActionCard(
|
||||
title = R.string.backup_BAc_import,
|
||||
icon = Icons.Outlined.Backup,
|
||||
onClick = onImportBackup,
|
||||
isOutlined = true
|
||||
)
|
||||
|
||||
ActionCard(
|
||||
title = R.string.continue_without_sync,
|
||||
icon = Icons.Outlined.CloudOff,
|
||||
onClick = { signIn(Platform.LOCAL) },
|
||||
isOutlined = true
|
||||
)
|
||||
}
|
||||
if (!hasTasksAccount) {
|
||||
AccountTypeCard(
|
||||
title = R.string.tasks_org,
|
||||
cost = R.string.cost_more_money,
|
||||
icon = R.drawable.ic_round_icon,
|
||||
onClick = { signIn(Platform.TASKS_ORG) }
|
||||
)
|
||||
}
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.microsoft,
|
||||
cost = if (hasPro) null else R.string.cost_free,
|
||||
icon = R.drawable.ic_microsoft_tasks,
|
||||
onClick = { signIn(Platform.MICROSOFT) }
|
||||
)
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.gtasks_GPr_header,
|
||||
cost = if (hasPro) null else R.string.cost_free,
|
||||
icon = R.drawable.ic_google,
|
||||
onClick = { signIn(Platform.GOOGLE_TASKS) }
|
||||
)
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.davx5,
|
||||
cost = if (hasPro) null else R.string.cost_money,
|
||||
icon = R.drawable.ic_davx5_icon_green_bg,
|
||||
onClick = { openUrl(Platform.DAVX5) }
|
||||
)
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.caldav,
|
||||
cost = if (hasPro) null else R.string.cost_money,
|
||||
icon = R.drawable.ic_webdav_logo,
|
||||
tint = MaterialTheme.colorScheme.onSurface.copy(alpha = .8f),
|
||||
onClick = { signIn(Platform.CALDAV) }
|
||||
)
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.etesync,
|
||||
cost = if (hasPro) null else R.string.cost_money,
|
||||
icon = R.drawable.ic_etesync,
|
||||
onClick = { signIn(Platform.ETESYNC) }
|
||||
)
|
||||
|
||||
AccountTypeCard(
|
||||
title = R.string.decsync,
|
||||
cost = if (hasPro) null else R.string.cost_money,
|
||||
icon = R.drawable.ic_decsync,
|
||||
onClick = { openUrl(Platform.DECSYNC_CC) }
|
||||
)
|
||||
|
||||
if (gettingStarted) {
|
||||
ActionCard(
|
||||
title = R.string.help_me_choose,
|
||||
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||
onClick = { openUrl(Platform.LOCAL) },
|
||||
isOutlined = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AccountTypeCard(
|
||||
@StringRes title: Int,
|
||||
@StringRes cost: Int? = null,
|
||||
@DrawableRes icon: Int,
|
||||
tint: Color? = null,
|
||||
onClick: () -> Unit,
|
||||
) {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.width(108.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(12.dp).fillMaxWidth(),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = icon),
|
||||
contentDescription = stringResource(id = title),
|
||||
tint = tint ?: Color.Unspecified,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = buildAnnotatedString {
|
||||
append(stringResource(id = title))
|
||||
cost?.let {
|
||||
append("\n")
|
||||
withStyle(
|
||||
style = SpanStyle(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
fontSize = MaterialTheme.typography.labelSmall.fontSize
|
||||
)
|
||||
) {
|
||||
append(stringResource(id = it))
|
||||
}
|
||||
}
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
minLines = 3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionCard(
|
||||
@StringRes title: Int,
|
||||
icon: ImageVector,
|
||||
onClick: () -> Unit,
|
||||
isOutlined: Boolean = false
|
||||
) {
|
||||
if (isOutlined) {
|
||||
OutlinedCard(
|
||||
modifier = Modifier
|
||||
.width(108.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
onClick = onClick
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(id = title),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
minLines = 3,
|
||||
maxLines = 3,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.width(150.dp),
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp),
|
||||
onClick = onClick
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(id = title),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
modifier = Modifier.size(40.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(id = title),
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@PreviewScreenSizes
|
||||
@PreviewFontScale
|
||||
@Composable
|
||||
fun GettingStartedPreview() {
|
||||
TasksTheme {
|
||||
AddAccountScreen(
|
||||
gettingStarted = true,
|
||||
hasTasksAccount = false,
|
||||
hasPro = false,
|
||||
onBack = {},
|
||||
signIn = {},
|
||||
openUrl = {},
|
||||
onImportBackup = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@PreviewLightDark
|
||||
@Composable
|
||||
fun AddAccountPreview() {
|
||||
TasksTheme {
|
||||
AddAccountScreen(
|
||||
gettingStarted = false,
|
||||
hasTasksAccount = false,
|
||||
hasPro = false,
|
||||
onBack = {},
|
||||
signIn = {},
|
||||
openUrl = {},
|
||||
onImportBackup = {},
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,32 +0,0 @@
|
||||
package org.tasks.compose.accounts
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.data.dao.CaldavDao
|
||||
import org.tasks.data.newLocalAccount
|
||||
import org.tasks.extensions.Context.openUri
|
||||
import org.tasks.sync.AddAccountDialog
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class AddAccountViewModel @Inject constructor(
|
||||
private val caldavDao: CaldavDao,
|
||||
) : ViewModel() {
|
||||
fun createLocalAccount() = viewModelScope.launch {
|
||||
caldavDao.newLocalAccount()
|
||||
}
|
||||
|
||||
fun openUrl(context: Context, platform: AddAccountDialog.Platform) {
|
||||
val url = when (platform) {
|
||||
AddAccountDialog.Platform.DAVX5 -> R.string.url_davx5
|
||||
AddAccountDialog.Platform.DECSYNC_CC -> R.string.url_decsync
|
||||
AddAccountDialog.Platform.LOCAL -> R.string.help_url_sync
|
||||
else -> return
|
||||
}
|
||||
context.openUri(context.getString(url))
|
||||
}
|
||||
}
|
||||
@ -1,316 +0,0 @@
|
||||
package org.tasks.compose.home
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity.RESULT_OK
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.ime
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.systemBars
|
||||
import androidx.compose.foundation.layout.windowInsetsBottomHeight
|
||||
import androidx.compose.foundation.layout.windowInsetsTopHeight
|
||||
import androidx.compose.material3.DrawerState
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.ModalDrawerSheet
|
||||
import androidx.compose.material3.ModalNavigationDrawer
|
||||
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffold
|
||||
import androidx.compose.material3.adaptive.layout.ListDetailPaneScaffoldRole
|
||||
import androidx.compose.material3.adaptive.layout.PaneAdaptedValue
|
||||
import androidx.compose.material3.adaptive.navigation.ThreePaneScaffoldNavigator
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.IntentCompat.getParcelableExtra
|
||||
import androidx.fragment.compose.AndroidFragment
|
||||
import androidx.fragment.compose.rememberFragmentState
|
||||
import androidx.hilt.navigation.compose.hiltViewModel
|
||||
import com.todoroo.astrid.activity.MainActivity.Companion.OPEN_FILTER
|
||||
import com.todoroo.astrid.activity.MainActivityViewModel
|
||||
import com.todoroo.astrid.activity.TaskEditFragment
|
||||
import com.todoroo.astrid.activity.TaskEditFragment.Companion.EXTRA_TASK
|
||||
import com.todoroo.astrid.activity.TaskListFragment
|
||||
import com.todoroo.astrid.activity.TaskListFragment.Companion.EXTRA_FILTER
|
||||
import kotlinx.coroutines.launch
|
||||
import org.tasks.R
|
||||
import org.tasks.TasksApplication
|
||||
import org.tasks.activities.TagSettingsActivity
|
||||
import org.tasks.billing.PurchaseActivity
|
||||
import org.tasks.caldav.BaseCaldavCalendarSettingsActivity.Companion.EXTRA_CALDAV_ACCOUNT
|
||||
import org.tasks.compose.drawer.DrawerAction
|
||||
import org.tasks.compose.drawer.DrawerItem
|
||||
import org.tasks.compose.drawer.MenuSearchBar
|
||||
import org.tasks.compose.drawer.TaskListDrawer
|
||||
import org.tasks.data.listSettingsClass
|
||||
import org.tasks.extensions.Context.openUri
|
||||
import org.tasks.filters.Filter
|
||||
import org.tasks.filters.FilterProvider
|
||||
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_LIST
|
||||
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_PLACE
|
||||
import org.tasks.filters.FilterProvider.Companion.REQUEST_NEW_TAGS
|
||||
import org.tasks.filters.NavigationDrawerSubheader
|
||||
import org.tasks.kmp.org.tasks.compose.TouchSlopMultiplier
|
||||
import org.tasks.kmp.org.tasks.compose.rememberImeState
|
||||
import org.tasks.location.LocationPickerActivity
|
||||
import org.tasks.preferences.HelpAndFeedback
|
||||
import org.tasks.preferences.MainPreferences
|
||||
import timber.log.Timber
|
||||
|
||||
@OptIn(ExperimentalMaterial3AdaptiveApi::class)
|
||||
@Composable
|
||||
fun HomeScreen(
|
||||
viewModel: MainActivityViewModel = hiltViewModel(LocalActivity.current as ComponentActivity),
|
||||
state: MainActivityViewModel.State,
|
||||
drawerState: DrawerState,
|
||||
showNewFilterDialog: () -> Unit,
|
||||
navigator: ThreePaneScaffoldNavigator<Any>,
|
||||
) {
|
||||
val currentWindowInsets = WindowInsets.systemBars.asPaddingValues()
|
||||
val windowInsets = remember { mutableStateOf(currentWindowInsets) }
|
||||
val keyboard = LocalSoftwareKeyboardController.current
|
||||
val newList =
|
||||
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data
|
||||
?.let { getParcelableExtra(it, OPEN_FILTER, Filter::class.java) }
|
||||
?.let { viewModel.setFilter(it) }
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(currentWindowInsets) {
|
||||
Timber.d("insets: $currentWindowInsets")
|
||||
if (currentWindowInsets.calculateTopPadding() != 0.dp || currentWindowInsets.calculateBottomPadding() != 0.dp) {
|
||||
windowInsets.value = currentWindowInsets
|
||||
}
|
||||
}
|
||||
val isListVisible =
|
||||
navigator.scaffoldValue[ListDetailPaneScaffoldRole.List] == PaneAdaptedValue.Expanded
|
||||
val isDetailVisible =
|
||||
navigator.scaffoldValue[ListDetailPaneScaffoldRole.Detail] == PaneAdaptedValue.Expanded
|
||||
|
||||
TouchSlopMultiplier {
|
||||
ModalNavigationDrawer(
|
||||
drawerState = drawerState,
|
||||
gesturesEnabled = isListVisible,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet(
|
||||
drawerState = drawerState,
|
||||
windowInsets = WindowInsets(0, 0, 0, 0),
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scope = rememberCoroutineScope()
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
TaskListDrawer(
|
||||
arrangement = if (state.menuQuery.isBlank()) Arrangement.Top else Arrangement.Bottom,
|
||||
filters = if (state.menuQuery.isNotEmpty()) state.searchItems else state.drawerItems,
|
||||
onClick = {
|
||||
when (it) {
|
||||
is DrawerItem.Filter -> {
|
||||
viewModel.setFilter(it.filter)
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
keyboard?.hide()
|
||||
}
|
||||
}
|
||||
|
||||
is DrawerItem.Header -> {
|
||||
viewModel.toggleCollapsed(it.header)
|
||||
}
|
||||
}
|
||||
},
|
||||
onAddClick = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
when (it.header.addIntentRc) {
|
||||
FilterProvider.REQUEST_NEW_FILTER ->
|
||||
showNewFilterDialog()
|
||||
|
||||
REQUEST_NEW_PLACE ->
|
||||
newList.launch(Intent(context, LocationPickerActivity::class.java))
|
||||
|
||||
REQUEST_NEW_TAGS ->
|
||||
newList.launch(Intent(context, TagSettingsActivity::class.java))
|
||||
|
||||
REQUEST_NEW_LIST ->
|
||||
when (it.header.subheaderType) {
|
||||
NavigationDrawerSubheader.SubheaderType.CALDAV,
|
||||
NavigationDrawerSubheader.SubheaderType.TASKS ->
|
||||
viewModel
|
||||
.getAccount(it.header.id.toLong())
|
||||
?.let {
|
||||
newList.launch(
|
||||
Intent(context, it.listSettingsClass())
|
||||
.putExtra(EXTRA_CALDAV_ACCOUNT, it)
|
||||
)
|
||||
}
|
||||
|
||||
else -> {}
|
||||
}
|
||||
|
||||
else -> Timber.e("Unhandled request code: $it")
|
||||
}
|
||||
}
|
||||
},
|
||||
onErrorClick = {
|
||||
context.startActivity(Intent(context, MainPreferences::class.java))
|
||||
},
|
||||
searchBar = {
|
||||
MenuSearchBar(
|
||||
begForMoney = state.begForMoney,
|
||||
onDrawerAction = {
|
||||
scope.launch {
|
||||
drawerState.close()
|
||||
when (it) {
|
||||
DrawerAction.PURCHASE ->
|
||||
if (TasksApplication.IS_GENERIC)
|
||||
context.openUri(R.string.url_donate)
|
||||
else
|
||||
context.startActivity(
|
||||
Intent(
|
||||
context,
|
||||
PurchaseActivity::class.java
|
||||
)
|
||||
)
|
||||
|
||||
DrawerAction.HELP_AND_FEEDBACK ->
|
||||
context.startActivity(
|
||||
Intent(
|
||||
context,
|
||||
HelpAndFeedback::class.java
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
query = state.menuQuery,
|
||||
onQueryChange = { viewModel.queryMenu(it) },
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
SystemBarScrim(
|
||||
modifier = Modifier
|
||||
.windowInsetsTopHeight(WindowInsets.systemBars)
|
||||
.align(Alignment.TopCenter)
|
||||
)
|
||||
SystemBarScrim(
|
||||
modifier = Modifier
|
||||
.windowInsetsBottomHeight(WindowInsets.systemBars)
|
||||
.align(Alignment.BottomCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val scope = rememberCoroutineScope()
|
||||
ListDetailPaneScaffold(
|
||||
directive = navigator.scaffoldDirective,
|
||||
value = navigator.scaffoldValue,
|
||||
listPane = {
|
||||
key (state.filter) {
|
||||
val fragment = remember { mutableStateOf<TaskListFragment?>(null) }
|
||||
val keyboardOpen = rememberImeState()
|
||||
AndroidFragment<TaskListFragment>(
|
||||
fragmentState = rememberFragmentState(),
|
||||
arguments = remember(state.filter) {
|
||||
Bundle()
|
||||
.apply { putParcelable(EXTRA_FILTER, state.filter) }
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.imePadding(),
|
||||
) { tlf ->
|
||||
fragment.value = tlf
|
||||
tlf.applyInsets(windowInsets.value)
|
||||
tlf.setNavigationClickListener {
|
||||
scope.launch { drawerState.open() }
|
||||
}
|
||||
}
|
||||
LaunchedEffect(fragment, windowInsets, keyboardOpen.value) {
|
||||
fragment.value?.applyInsets(
|
||||
if (keyboardOpen.value) {
|
||||
PaddingValues(
|
||||
top = windowInsets.value.calculateTopPadding(),
|
||||
)
|
||||
} else {
|
||||
windowInsets.value
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
detailPane = {
|
||||
val direction = LocalLayoutDirection.current
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(
|
||||
top = windowInsets.value.calculateTopPadding(),
|
||||
start = windowInsets.value.calculateStartPadding(direction),
|
||||
end = windowInsets.value.calculateEndPadding(direction),
|
||||
bottom = if (rememberImeState().value)
|
||||
WindowInsets.ime.asPaddingValues().calculateBottomPadding()
|
||||
else
|
||||
windowInsets.value.calculateBottomPadding()
|
||||
),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (state.task == null) {
|
||||
if (isListVisible && isDetailVisible) {
|
||||
Icon(
|
||||
painter = painterResource(org.tasks.kmp.R.drawable.ic_launcher_no_shadow_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(192.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
key(state.task) {
|
||||
AndroidFragment<TaskEditFragment>(
|
||||
fragmentState = rememberFragmentState(),
|
||||
arguments = remember(state.task) {
|
||||
Bundle()
|
||||
.apply { putParcelable(EXTRA_TASK, state.task) }
|
||||
},
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
SystemBarScrim(
|
||||
modifier = Modifier
|
||||
.windowInsetsTopHeight(WindowInsets.systemBars)
|
||||
.align(Alignment.TopCenter),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue