Prompt for sync at install time

pull/3559/head
Alex Baker 7 months ago
parent 267ebfe86e
commit 0dea530c50

@ -9,8 +9,8 @@ import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Test
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.CaldavDao.Companion.LOCAL
import org.tasks.data.dao.DeletionDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.data.entity.CaldavTask
import org.tasks.date.DateTimeUtils.newDateTime
@ -62,7 +62,8 @@ class DeletionDaoTests : InjectingTestCase() {
fun purgeDeletedLocalTask() = runBlocking {
val task = newTask(with(DELETION_TIME, newDateTime()))
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()
@ -74,7 +75,8 @@ class DeletionDaoTests : InjectingTestCase() {
fun dontPurgeActiveTasks() = runBlocking {
val task = newTask()
taskDao.createNew(task)
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL))
caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL))
caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd"))
caldavDao.insert(CaldavTask(task = task.id, calendar = "1234"))
deletionDao.purgeDeleted()

@ -418,6 +418,10 @@
android:name=".caldav.CaldavAccountSettingsActivity"
android:theme="@style/Tasks"/>
<activity
android:name=".caldav.LocalAccountSettingsActivity"
android:theme="@style/Tasks" />
<activity
android:name=".etebase.EtebaseAccountSettingsActivity"
android:theme="@style/Tasks" />

@ -11,8 +11,10 @@ import android.graphics.Color
import android.os.Bundle
import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
@ -31,13 +33,16 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch
@ -45,22 +50,36 @@ import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.auth.SignInActivity
import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.compose.AddAccountDestination
import org.tasks.compose.HomeDestination
import org.tasks.compose.accounts.AddAccountScreen
import org.tasks.compose.accounts.AddAccountViewModel
import org.tasks.compose.home.HomeScreen
import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task
import org.tasks.dialogs.ImportTasksDialog
import org.tasks.dialogs.NewFilterDialog
import org.tasks.etebase.EtebaseAccountSettingsActivity
import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.toast
import org.tasks.extensions.broughtToFront
import org.tasks.extensions.flagsToString
import org.tasks.extensions.isFromHistory
import org.tasks.files.FileHelper
import org.tasks.filters.Filter
import org.tasks.jobs.WorkManager
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.preferences.fragments.FRAG_TAG_IMPORT_TASKS
import org.tasks.sync.AddAccountDialog
import org.tasks.sync.SyncAdapters
import org.tasks.sync.microsoft.MicrosoftSignInViewModel
import org.tasks.themes.ColorProvider
import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
@ -82,6 +101,8 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0
@ -125,7 +146,7 @@ class MainActivity : AppCompatActivity() {
LaunchedEffect(hasAccount) {
Timber.d("hasAccount=$hasAccount")
if (hasAccount == false) {
// TODO: navigate to add account screen
navController.navigate(AddAccountDestination(showImport = true))
}
isReady = hasAccount != null
}
@ -133,7 +154,87 @@ class MainActivity : AppCompatActivity() {
navController = navController,
startDestination = HomeDestination,
) {
composable<AddAccountDestination> {
val route = it.toRoute<AddAccountDestination>()
LaunchedEffect(hasAccount) {
if (route.showImport && hasAccount == true) {
navController.popBackStack()
}
}
val addAccountViewModel: AddAccountViewModel = hiltViewModel()
val microsoftVM: MicrosoftSignInViewModel = hiltViewModel()
val syncLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
syncAdapters.sync(true)
workManager.updateBackgroundSync()
} else {
result.data
?.getStringExtra(GtasksLoginActivity.EXTRA_ERROR)
?.let { toast(it) }
}
}
val importBackupLauncher =
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
val uri = result.data?.data ?: return@rememberLauncherForActivityResult
ImportTasksDialog.newImportTasksDialog(uri)
.show(supportFragmentManager, FRAG_TAG_IMPORT_TASKS)
}
}
AddAccountScreen(
gettingStarted = route.showImport,
hasTasksAccount = inventory.hasTasksAccount,
hasPro = inventory.hasPro,
onBack = { navController.popBackStack() },
signIn = { platform ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform)
when (platform) {
AddAccountDialog.Platform.TASKS_ORG ->
syncLauncher.launch(
Intent(this@MainActivity, SignInActivity::class.java)
)
AddAccountDialog.Platform.GOOGLE_TASKS ->
syncLauncher.launch(
Intent(this@MainActivity, GtasksLoginActivity::class.java)
)
AddAccountDialog.Platform.MICROSOFT ->
microsoftVM.signIn(this@MainActivity)
AddAccountDialog.Platform.CALDAV ->
syncLauncher.launch(
Intent(this@MainActivity, CaldavAccountSettingsActivity::class.java)
)
AddAccountDialog.Platform.ETESYNC ->
syncLauncher.launch(
Intent(this@MainActivity, EtebaseAccountSettingsActivity::class.java)
)
AddAccountDialog.Platform.LOCAL ->
addAccountViewModel.createLocalAccount()
else -> throw IllegalArgumentException()
}
},
openUrl = { platform ->
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to platform.name)
addAccountViewModel.openUrl(this@MainActivity, platform)
},
onImportBackup = {
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to "import_backup")
importBackupLauncher.launch(
FileHelper.newFilePickerIntent(this@MainActivity, preferences.backupDirectory),
)
}
)
}
composable<HomeDestination> {
if (hasAccount != true) {
return@composable
}
val scope = rememberCoroutineScope()
val state = viewModel.state.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState(

@ -9,4 +9,5 @@ object Constants {
const val SYNC_TYPE_ETEBASE = "etebase"
const val SYNC_TYPE_DECSYNC = "decsync"
const val SYNC_TYPE_MICROSOFT = "microsoft"
const val SYNC_TYPE_LOCAL = "local"
}

@ -36,6 +36,7 @@ import org.tasks.compose.ServerSelector
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.entity.Task
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding
import org.tasks.dialogs.DialogBuilder
@ -116,7 +117,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT)
}
if (!inventory.hasPro) {
if (!inventory.hasPro && caldavAccount?.accountType != TYPE_LOCAL) {
newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) {
@ -308,7 +309,8 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
private fun newSnackbar(message: String?): Snackbar {
val snackbar = Snackbar.make(binding.rootLayout, message!!, 8000)
.setTextColor(getColor(R.color.snackbar_text_color))
.setBackgroundTint(getColor(R.color.dialog_background))
.setTextColor(getColor(R.color.text_primary))
.setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar
.view
@ -341,7 +343,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
}
}
private fun removeAccountPrompt() {
protected open suspend fun removeAccountPrompt() {
if (requestInProgress()) {
return
}
@ -378,7 +380,9 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.menu_help -> openUri(helpUrl)
R.id.remove -> removeAccountPrompt()
R.id.remove -> lifecycleScope.launch {
removeAccountPrompt()
}
}
return onOptionsItemSelected(item)
}

@ -0,0 +1,90 @@
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
}

@ -3,26 +3,20 @@ package org.tasks.caldav
import android.os.Bundle
import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.compose.DeleteButton
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme
@AndroidEntryPoint
class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val canDelete = runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 }
setContent {
TasksTheme {
BaseCaldavSettingsContent (
optionButton = { if (!isNew && canDelete) DeleteButton(caldavCalendar?.name ?: "") { delete() } }
optionButton = { if (!isNew) DeleteButton(caldavCalendar?.name ?: "") { delete() } }
)
}
}
@ -37,4 +31,4 @@ class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) =
onDeleted(true)
}
}

@ -16,7 +16,6 @@ import org.tasks.themes.TasksTheme
fun AddAccountDialog(
hasTasksAccount: Boolean,
hasPro: Boolean,
enableMicrosoftSync: Boolean = true,
selected: (Platform) -> Unit,
) {
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
@ -36,15 +35,13 @@ fun AddAccountDialog(
icon = R.drawable.ic_google,
onClick = { selected(Platform.GOOGLE_TASKS) }
)
if (enableMicrosoftSync) {
SyncAccount(
title = R.string.microsoft,
cost = if (hasPro) null else R.string.cost_free,
description = R.string.microsoft_selection_description,
icon = R.drawable.ic_microsoft_tasks,
onClick = { selected(Platform.MICROSOFT) }
)
}
SyncAccount(
title = R.string.microsoft,
cost = if (hasPro) null else R.string.cost_free,
description = R.string.microsoft_selection_description,
icon = R.drawable.ic_microsoft_tasks,
onClick = { selected(Platform.MICROSOFT) }
)
SyncAccount(
title = R.string.davx5,
cost = if (hasPro) null else R.string.cost_money,

@ -4,3 +4,6 @@ import kotlinx.serialization.Serializable
@Serializable
object HomeDestination
@Serializable
data class AddAccountDestination(val showImport: Boolean)

@ -0,0 +1,348 @@
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 = {},
)
}
}

@ -0,0 +1,32 @@
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))
}
}

@ -7,6 +7,7 @@ import org.tasks.activities.GoogleTaskListSettingsActivity
import org.tasks.caldav.BaseCaldavAccountSettingsActivity
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.caldav.CaldavCalendarSettingsActivity
import org.tasks.caldav.LocalAccountSettingsActivity
import org.tasks.caldav.LocalListSettingsActivity
import org.tasks.data.OpenTaskDao.Companion.isDavx5
import org.tasks.data.OpenTaskDao.Companion.isDavx5Managed
@ -29,6 +30,7 @@ val CaldavAccount.prefTitle: Int
uuid.isDecSync() -> R.string.decsync
isMicrosoft -> R.string.microsoft
isGoogleTasks -> R.string.gtasks_GPr_header
isLocalList -> R.string.local_lists
else -> 0
}
@ -42,6 +44,7 @@ val CaldavAccount.prefIcon: Int
uuid.isDecSync() -> R.drawable.ic_decsync
isMicrosoft -> R.drawable.ic_microsoft_tasks
isGoogleTasks -> R.drawable.ic_google
isLocalList -> R.drawable.ic_outline_cloud_off_24px
else -> 0
}
@ -66,6 +69,7 @@ val CaldavAccount.accountSettingsClass: Class<out BaseCaldavAccountSettingsActiv
isCaldavAccount -> CaldavAccountSettingsActivity::class.java
isEtebaseAccount -> EtebaseAccountSettingsActivity::class.java
isOpenTasks -> OpenTaskAccountSettingsActivity::class.java
isLocalList -> LocalAccountSettingsActivity::class.java
else -> throw IllegalArgumentException("Unexpected account type: $this")
}

@ -27,7 +27,4 @@ class PreferenceDrawerConfiguration(
override val recentlyModifiedFilter: Boolean
get() = preferences.getBoolean(R.string.p_show_recently_modified_filter, super.recentlyModifiedFilter)
override val localListsEnabled: Boolean
get() = preferences.getBoolean(R.string.p_lists_enabled, super.localListsEnabled)
}

@ -3,14 +3,14 @@ package org.tasks.jobs
import android.content.Context
import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters
import com.todoroo.astrid.service.TaskDeleter
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import org.tasks.R
import org.tasks.analytics.Firebase
import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.BaseWorker
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters
@HiltWorker
@ -20,14 +20,18 @@ class MigrateLocalWork @AssistedInject constructor(
firebase: Firebase,
private val clientProvider: CaldavClientProvider,
private val caldavDao: CaldavDao,
private val preferences: Preferences,
private val syncAdapters: SyncAdapters
private val syncAdapters: SyncAdapters,
private val taskDeleter: TaskDeleter,
) : BaseWorker(context, workerParams, firebase) {
override suspend fun run(): Result {
val uuid = inputData.getString(EXTRA_ACCOUNT) ?: return Result.failure()
val caldavAccount = caldavDao.getAccountByUuid(uuid) ?: return Result.failure()
val caldavClient = clientProvider.forAccount(caldavAccount)
caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).forEach {
val fromAccount = caldavDao
.getAccounts(CaldavAccount.TYPE_LOCAL)
.firstOrNull()
?: return Result.success()
caldavDao.getCalendarsByAccount(fromAccount.uuid!!).forEach {
caldavDao.update(
it.copy(
url = caldavClient.makeCollection(it.name!!, it.color),
@ -35,7 +39,7 @@ class MigrateLocalWork @AssistedInject constructor(
)
)
}
preferences.setBoolean(R.string.p_lists_enabled, false)
taskDeleter.delete(fromAccount)
syncAdapters.sync()
return Result.success()
}

@ -11,8 +11,6 @@ import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Task
import org.tasks.data.getLocalAccount
import org.tasks.data.getLocalList
import org.tasks.filters.CaldavFilter
import org.tasks.filters.CustomFilter
import org.tasks.filters.Filter
@ -92,7 +90,7 @@ class DefaultFilterProvider @Inject constructor(
?.let { caldavDao.getAccountByUuid(it) }
?.let { account -> CaldavFilter(calendar = list, account = account) }
}
?: CaldavFilter(calendar = caldavDao.getLocalList(), account = caldavDao.getLocalAccount())
?: throw IllegalStateException()
defaultList = filter
return filter
}

@ -17,7 +17,6 @@ import org.tasks.R
import org.tasks.backup.BackupConstants
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.googleapis.InvokerFactory
import org.tasks.gtasks.GoogleAccountManager
@ -38,7 +37,7 @@ class PreferencesViewModel @Inject constructor(
val lastDriveBackup = MutableLiveData<Long?>()
val lastAndroidBackup = MutableLiveData<Long>()
val caldavAccounts: Flow<List<CaldavAccount>>
get() = caldavDao.watchAccounts(exclude = listOf(TYPE_LOCAL))
get() = caldavDao.watchAccounts()
private fun isStale(timestamp: Long?) =
timestamp != null

@ -27,7 +27,7 @@ const val REQUEST_DRIVE_BACKUP = 12002
private const val REQUEST_PICKER = 10003
private const val REQUEST_BACKUP_NOW = 10004
private const val FRAG_TAG_EXPORT_TASKS = "frag_tag_export_tasks"
private const val FRAG_TAG_IMPORT_TASKS = "frag_tag_import_tasks"
const val FRAG_TAG_IMPORT_TASKS = "frag_tag_import_tasks"
@AndroidEntryPoint
class Backups : InjectingPreferenceFragment() {

@ -136,6 +136,8 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
)
Platform.DECSYNC_CC ->
context?.openUri(R.string.url_decsync)
Platform.LOCAL -> {}
}
}
}
@ -160,13 +162,6 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
})
}
preferenceScreen.removeAt(current, index - current)
if (caldavAccounts.isEmpty()) {
addAccount.setTitle(R.string.not_signed_in)
addAccount.setIcon(R.drawable.ic_outline_cloud_off_24px)
} else {
addAccount.setTitle(R.string.add_account)
addAccount.setIcon(R.drawable.ic_outline_add_24px)
}
tintIcons(addAccount, requireContext().getColor(R.color.icon_tint_with_alpha))
}
@ -210,7 +205,7 @@ class MainSettingsFragment : InjectingPreferenceFragment() {
pref.setTitle(account.prefTitle)
pref.summary = account.name
pref.setIcon(account.prefIcon)
if (account.isCaldavAccount) {
if (account.isCaldavAccount || account.isLocalList) {
tintIcons(pref, requireContext().getColor(R.color.icon_tint_with_alpha))
}
pref.setOnPreferenceClickListener {

@ -22,7 +22,6 @@ import org.tasks.auth.SignInActivity
import org.tasks.auth.SignInActivity.Platform
import org.tasks.billing.Inventory
import org.tasks.billing.Purchase
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired
import org.tasks.extensions.Context.openUri
@ -203,7 +202,8 @@ class TasksAccount : BaseAccountPreference() {
}
lifecycleScope.launch {
val listCount = caldavDao.listCount(CaldavDao.LOCAL)
val localAccount = caldavDao.getAccounts(CaldavAccount.TYPE_LOCAL).firstOrNull()
val listCount = localAccount?.uuid?.let { caldavDao.listCount(it) } ?: 0
val quantityString = resources.getQuantityString(R.plurals.list_count, listCount, listCount)
findPreference(R.string.migrate).isVisible = listCount > 0
findPreference(R.string.local_lists).summary =

@ -7,6 +7,7 @@ import androidx.fragment.app.DialogFragment
import androidx.fragment.app.setFragmentResult
import dagger.hilt.android.AndroidEntryPoint
import org.tasks.R
import org.tasks.compose.AddAccountDialog
import org.tasks.dialogs.DialogBuilder
import org.tasks.extensions.Context.openUri
import org.tasks.preferences.Preferences
@ -34,7 +35,8 @@ class AddAccountDialog : DialogFragment() {
DAVX5,
CALDAV,
ETESYNC,
DECSYNC_CC
DECSYNC_CC,
LOCAL,
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
@ -46,9 +48,8 @@ class AddAccountDialog : DialogFragment() {
theme = theme.themeBase.index,
primary = theme.themeColor.primaryColor,
) {
org.tasks.compose.AddAccountDialog(
AddAccountDialog(
hasTasksAccount = hasTasksAccount,
enableMicrosoftSync = preferences.getBoolean(R.string.p_microsoft_sync, false),
hasPro = hasPro,
selected = this::selected
)

@ -481,7 +481,6 @@
<string name="github_sponsor">راعي</string>
<string name="authentication_required">يستلزم التوثيق</string>
<string name="github_sponsors">رعاة GitHub</string>
<string name="not_signed_in">لم تسجل الدخول</string>
<string name="authorization_cancelled">تم إلغاء التخويل</string>
<string name="issue_tracker">تعقب المشكلة</string>
<string name="chat_libera">انضم إلى #tasks على Libera Chat</string>

@ -641,7 +641,6 @@
<string name="app_password_delete_confirmation">Всички приложения, които използват паролата ще бъдат отписани</string>
<string name="app_password_save">Използвайте тези данни за вход за настройка на друго приложение. Те дават пълен достъп до профила ви в Tasks.org за това не ги записвайте никъде и не ги споделяйте с никого!</string>
<string name="app_passwords_more_info">Синхронизирайте задачите и календарите си с други настолни и мобилни приложения. Докоснете за повече информация</string>
<string name="not_signed_in">Не сте вписани</string>
<string name="insufficient_subscription">Недостатъчно ниво на абонамент. За да бъде възстановена услугата надстройте своя абонамент.</string>
<string name="authorization_cancelled">Удостоверяването е спряно</string>
<string name="background_location">Местоположение във фонов режим</string>

@ -578,7 +578,6 @@
<string name="sign_in_with_google">Přihlášení přes Google</string>
<string name="github_sponsors">sponzoři na GitHubu</string>
<string name="google_play_subscribers">předplatitelé Google Play</string>
<string name="not_signed_in">Nepřihlášeni</string>
<string name="authorization_cancelled">Autorizace zrušena</string>
<string name="privacy">Soukromí</string>
<string name="open_source">Otevřený zdrojový kód</string>

@ -515,7 +515,6 @@
<string name="sign_in_with_google">Log ind med Google</string>
<string name="github_sponsors">GitHub-sponsorer</string>
<string name="google_play_subscribers">Google Play-abonnenter</string>
<string name="not_signed_in">Ikke logget ind</string>
<string name="authorization_cancelled">Autorisation annulleret</string>
<string name="follow_reddit">Følg r/tasks</string>
<string name="current_subscription">Nuværende abonnement: %s</string>

@ -543,7 +543,6 @@
<string name="sign_in_with_github">Mit GitHub anmelden</string>
<string name="github_sponsors">GitHub-Sponsoren</string>
<string name="google_play_subscribers">Google Play-Abonnenten</string>
<string name="not_signed_in">Nicht angemeldet</string>
<string name="authorization_cancelled">Autorisierung abgebrochen</string>
<string name="sign_in_with_google">Mit Google anmelden</string>
<string name="account_not_included">Nicht im „Wähle deinen Preis“-Abonnement enthalten</string>

@ -611,7 +611,6 @@
<string name="save_percent">Ŝpari je %d%%</string>
<string name="remove_user_confirmation">%1$s ne plu povos atingi %2$s</string>
<string name="share_list">Komunigi liston</string>
<string name="not_signed_in">Ne ensalutinte</string>
<string name="widget_id">Fenestraĵa ID: %d</string>
<string name="sort_start_group">Komenci %s</string>
<string name="open_last_viewed_list">Malfermi liston de laste rigardita</string>

@ -556,7 +556,6 @@
<string name="sign_in_with_github">Acceder con GitHub</string>
<string name="github_sponsors">Patrocinadores de GitHub</string>
<string name="google_play_subscribers">Suscriptores de Google Play</string>
<string name="not_signed_in">No registrado</string>
<string name="no_google_play_subscription">No se encontró ninguna suscripción de Google Play elegible</string>
<string name="insufficient_sponsorship">No se encontró ningún patrocinio de GitHub elegible</string>
<string name="above_average">Por encima del promedio</string>

@ -491,7 +491,6 @@
<string name="third_party_licenses">Kolmandate osapoolte litsentsid</string>
<string name="name_your_price">Ütle oma hind</string>
<string name="hide_unused_places">Peida kasutamata asukohad</string>
<string name="not_signed_in">Ei ole sisse logitud</string>
<string name="app_password_last_access">Viimati kasutatud: %s</string>
<string name="invite_awaiting_response">Kutse ootab vastust</string>
<string name="completion_sound">Esita lõpetamise heli</string>

@ -555,7 +555,6 @@
<string name="authentication_required">Autentifikazioa derrigorrezkoa</string>
<string name="sign_in_with_github">Hasi saioa GitHub erabiliz</string>
<string name="google_play_subscribers">Google Play harpideak</string>
<string name="not_signed_in">Saioa hasi gabe</string>
<string name="no_google_play_subscription">Ez da Google Play harpidetza egokirik aurkitu</string>
<string name="insufficient_sponsorship">Ez da GitHub sponsorship egokirik aurkitu</string>
<string name="local_lists">Zerrenda lokalak</string>

@ -585,7 +585,6 @@
<string name="astrid_sort_order">Astrid manuaalinen lajittelu</string>
<string name="github_sponsors">GitHubin sponsorit</string>
<string name="google_play_subscribers">Google Play -tilaajat</string>
<string name="not_signed_in">Ei kirjautunut sisään</string>
<string name="authorization_cancelled">Lupa peruutettu</string>
<string name="privacy">yksityisyys</string>
<string name="open_source">Avoin lähdekoodi</string>

@ -561,7 +561,6 @@
<string name="sign_in_with_github">Connectez-vous avec « GitHub »</string>
<string name="github_sponsors">Parrains « GitHub »</string>
<string name="google_play_subscribers">Abonnés « Google Play »</string>
<string name="not_signed_in">Non connecté</string>
<string name="no_google_play_subscription">Aucun abonnement « Google Play » éligible n\'a été trouvé</string>
<string name="insufficient_sponsorship">Aucun parrainage « GitHub » éligible n\'a été trouvé</string>
<string name="above_average">Au dessus de la moyenne</string>

@ -512,7 +512,6 @@
<string name="custom_filter_has_reminder">Ten un recordatorio</string>
<string name="price_per_month">$%s/mes</string>
<string name="authorization_cancelled">Cancelouse a autenticación</string>
<string name="not_signed_in">Sen acceso</string>
<string name="above_average">Por riba da media</string>
<string name="save_percent">Gardar %d%%</string>
<string name="app_password">Contrasinal da app</string>

@ -139,7 +139,6 @@
<string name="local_lists">Popisi na uređaju</string>
<string name="help">Pomoć</string>
<string name="choose_synchronization_service">Odaberi platformu</string>
<string name="not_signed_in">Nisi prijavljen/a</string>
<string name="contact_developer">Kontaktiraj programera</string>
<string name="hide_unused_places">Sakrij nekorištena mjesta</string>
<string name="place_settings">Postavke mjesta</string>

@ -539,7 +539,6 @@
<string name="sign_in_with_github">Bejelentkezés GitHub fiókkal</string>
<string name="github_sponsors">GitHub szponzorok</string>
<string name="google_play_subscribers">Google Play előfizetők</string>
<string name="not_signed_in">Nincs bejelentkezve</string>
<string name="no_google_play_subscription">Nem található Google Play előfizetés</string>
<string name="insufficient_sponsorship">Nem található GitHub szponzorálás</string>
<string name="save_percent">Mentés %d%%</string>

@ -441,7 +441,6 @@
<string name="sign_in_with_google">Masuk dengan Google</string>
<string name="github_sponsors">Sponsor GitHub</string>
<string name="google_play_subscribers">Pelanggan Google Play</string>
<string name="not_signed_in">Tidak masuk</string>
<string name="authorization_cancelled">Otorisasi dibatalkan</string>
<string name="follow_reddit">Gabung r/tasks</string>
<string name="current_subscription">Langganan saat ini: %s</string>

@ -556,7 +556,6 @@
<string name="sign_in_with_github">Accedi con GitHub</string>
<string name="github_sponsors">Sponsor di GitHub</string>
<string name="google_play_subscribers">Abbonati a Google Play</string>
<string name="not_signed_in">Accesso non eseguito</string>
<string name="no_google_play_subscription">Nessun abbonamento Google Play idoneo trovato</string>
<string name="insufficient_sponsorship">Nessuna sponsorizzazione GitHub idonea trovata</string>
<string name="decsync_selection_description">Sincronizzazione basata sui file</string>

@ -565,7 +565,6 @@
<string name="sign_in_with_google">להיכנס באמצעות Google</string>
<string name="github_sponsors">נותני חסות GitHub</string>
<string name="google_play_subscribers">מנויי Google Play</string>
<string name="not_signed_in">לא מחובר</string>
<string name="authorization_cancelled">הרשאה בוטלה</string>
<string name="no_google_play_subscription">לא נמצא מינוי Google Play זכאי</string>
<string name="insufficient_sponsorship">לא נמצאה חסות GitHub זכאית</string>

@ -561,7 +561,6 @@
<string name="issue_tracker">Issue tracker / 問題追跡</string>
<string name="privacy">プライバシー</string>
<string name="authorization_cancelled">認証が取り消されました</string>
<string name="not_signed_in">サインインしていません</string>
<string name="tasks_org_account_required">Tasks.org のアカウントが必要です</string>
<string name="account_not_included">「価格はあなた次第」でのサブスクリプションには含まれていません</string>
<string name="list_members">リストメンバー</string>

@ -521,7 +521,6 @@
<string name="sign_in_with_google">구글을 이용해서 로그인</string>
<string name="github_sponsors">깃허브 후원자</string>
<string name="google_play_subscribers">구글 플레이 구독자</string>
<string name="not_signed_in">로그인 되어있지 않음</string>
<string name="follow_reddit">r/tasks 구독</string>
<string name="no_google_play_subscription">적합한 구글 플레이 구독을 찾을 수 없습니다</string>
<string name="insufficient_sponsorship">적합한 깃허브 후원 자격을 찾을 수 없습니다</string>

@ -566,7 +566,6 @@
<string name="app_passwords">Programėlių slaptažodžiai</string>
<string name="app_password">Programėlės slaptažodis</string>
<string name="authentication_required">Autentifikacija privaloma</string>
<string name="not_signed_in">Neprisijungta</string>
<string name="authorization_cancelled">Autorizavimas atšauktas</string>
<string name="privacy">Privatumas</string>
<string name="open_source">Atviras kodas</string>

@ -542,7 +542,6 @@
<string name="insufficient_sponsorship">Fant ikke noe GitHub-sponsorabonnement</string>
<string name="migrate_count">Flytt %s til Tasks.org</string>
<string name="sign_in_with_github">Logg inn med GitHub</string>
<string name="not_signed_in">Ikke innlogget</string>
<plurals name="list_count">
<item quantity="one">%d liste</item>
<item quantity="other">%d lister</item>

@ -539,7 +539,6 @@
<string name="github_sponsors">GitHub sponsors</string>
<string name="google_play_subscribers">Google Play abonnees</string>
<string name="sign_in_with_github">Log in met GitHub</string>
<string name="not_signed_in">Niet ingelogd</string>
<string name="no_google_play_subscription">Geen in aanmerking komend Google Play abonnement gevonden</string>
<string name="insufficient_sponsorship">Geen in aanmerking komend GitHub-sponsoring gevonden</string>
<string name="save_percent">Bespaar %d%%</string>

@ -538,7 +538,6 @@
<string name="authentication_required">Wymagane uwierzytelnienie</string>
<string name="sign_in_with_github">Zaloguj z GitHub</string>
<string name="sign_in_with_google">Zaloguj z Google</string>
<string name="not_signed_in">Niezalogowany</string>
<string name="repeat_monthly_fifth_week">piąty</string>
<plurals name="list_count">
<item quantity="one">%d lista</item>

@ -592,7 +592,6 @@
<string name="sign_in_with_google">Faça login com Google</string>
<string name="github_sponsors">Patrocinadores do GitHub</string>
<string name="google_play_subscribers">Assinantes do Google Play</string>
<string name="not_signed_in">Não autenticado</string>
<string name="authorization_cancelled">Autorização cancelada</string>
<string name="privacy">Privacidade</string>
<string name="open_source">Código aberto</string>

@ -558,7 +558,6 @@
<string name="sign_in_with_google">Iniciar sessão com o Google</string>
<string name="github_sponsors">Patrocinadores do GitHub</string>
<string name="google_play_subscribers">Subscritores do Google Play</string>
<string name="not_signed_in">Não autenticado</string>
<string name="privacy">Privacidade</string>
<string name="open_source">Código-fonte aberto</string>
<string name="issue_tracker">Reportar problemas</string>

@ -55,7 +55,6 @@
<string name="sign_in_with_google">Conectează-te cu Google</string>
<string name="github_sponsors">Sponsorii GitHub</string>
<string name="google_play_subscribers">Abonații Google Play</string>
<string name="not_signed_in">Nu este conectat</string>
<string name="authorization_cancelled">Autorizație anulată</string>
<string name="privacy">Confidențialitate</string>
<string name="open_source">Sursă deschisă</string>

@ -550,7 +550,6 @@
<string name="sign_in_with_google">Войти через Google</string>
<string name="github_sponsors">Спонсоры GitHub</string>
<string name="google_play_subscribers">Подписчики Google Play</string>
<string name="not_signed_in">Не авторизован</string>
<string name="authorization_cancelled">Авторизация отменена</string>
<string name="no_google_play_subscription">Подходящей подписки Google Play не найдено</string>
<string name="insufficient_sponsorship">Подходящее спонсорство GitHub не найдено</string>

@ -91,7 +91,6 @@
<string name="sign_in_with_github">GitHub සමඟ පුරනය වන්න</string>
<string name="github_sponsors">GitHub අනුග්‍රාහකයන්</string>
<string name="google_play_subscribers">Google Play ග්‍රාහකයින්</string>
<string name="not_signed_in">පුරනය වී නොමැත</string>
<string name="authorization_cancelled">අවසරය අවලංගු කරන ලදි</string>
<string name="privacy">පෞද්ගලිකත්වය</string>
<string name="open_source">විවෘත මූලාශ්‍රය</string>

@ -685,7 +685,6 @@
<string name="filter_no_priority">Bez nastavenej dôležitosti</string>
<string name="filter_no_tags">Bez štítkov</string>
<string name="privacy">Súkromie</string>
<string name="not_signed_in">Neprihlásený</string>
<string name="alarm_before_due">%s pred termínom</string>
<string name="documentation">Dokumentácia</string>
<string name="color_wheel">Viac farieb</string>

@ -468,7 +468,6 @@
<string name="sign_in_with_google">Logga in med Google</string>
<string name="github_sponsors">GitHub-sponsorer</string>
<string name="google_play_subscribers">Google Play-prenumeranter</string>
<string name="not_signed_in">Inte inloggad</string>
<string name="authorization_cancelled">Auktorisationen upphävd</string>
<string name="privacy">Integritet</string>
<string name="open_source">Öppen källkod</string>

@ -682,7 +682,6 @@
<string name="alarm_after_start">தொடக்கத்திற்குப் பிறகு %s</string>
<string name="sort_not_available">குறிச்சொற்கள், வடிப்பான்கள் அல்லது இடங்களுக்கு கிடைக்கவில்லை</string>
<string name="authorization_cancelled">ஏற்பு ரத்து செய்யப்பட்டது</string>
<string name="not_signed_in">உள்நுழையவில்லை</string>
<string name="above_average">சராசரிக்கு மேல்</string>
<string name="app_passwords">பயன்பாட்டு கடவுச்சொற்கள்</string>
<string name="upgrade_tasks_org_account_description">Tasks.org உடன் ஒத்திசைத்து பிற பயனர்களுடன் ஒத்துழைக்கவும்</string>

@ -81,7 +81,6 @@
<string name="logout">ออกจากระบบ</string>
<string name="this_feature_requires_a_subscription">คุณลักษณะนี้ต้องการการสมัครใช้งาน</string>
<string name="network_error">ไม่สามารถเชื่อมต่อได้</string>
<string name="not_signed_in">ไม่ได้ลงชื่อเข้าใช้</string>
<string name="authorization_cancelled">ยกเลิกการตรวจสอบแล้ว</string>
<string name="privacy">ข้อมูลส่วนบุคคล</string>
<string name="open_source">เปิดแหล่งที่มา</string>

@ -539,7 +539,6 @@
<string name="sign_in_with_github">GitHub ile oturum aç</string>
<string name="github_sponsors">GitHub Sponsorları</string>
<string name="google_play_subscribers">Google Play aboneleri</string>
<string name="not_signed_in">Oturum açılmadı</string>
<string name="no_google_play_subscription">Uygun Google Play aboneliği bulunamadı</string>
<string name="insufficient_sponsorship">Uygun GitHub sponsorluğu bulunamadı</string>
<string name="save_percent">%%%d tasarruf</string>

@ -478,7 +478,6 @@
<string name="sign_in_with_google">Увійти через Google</string>
<string name="github_sponsors">Спонсори GitHub</string>
<string name="google_play_subscribers">Підписники Google Play</string>
<string name="not_signed_in">Не авторизовані</string>
<string name="authorization_cancelled">Авторизацію скасовано</string>
<string name="follow_reddit">Долучитися до r/tasks</string>
<string name="current_subscription">Поточна підписка: %s</string>

@ -100,7 +100,6 @@
<string name="sign_in_with_google">Đăng nhập bằng Google</string>
<string name="google_play_subscribers">Người đăng ký trên Google Play</string>
<string name="github_sponsors">Nhà tài trợ trên GitHub</string>
<string name="not_signed_in">Chưa đăng nhập</string>
<string name="authorization_cancelled">Đã huỷ xác thực</string>
<string name="privacy">Riêng tư</string>
<string name="open_source">Mã nguồn mở</string>

@ -533,7 +533,6 @@
<string name="sign_in_with_github">使用 GitHub 登录</string>
<string name="github_sponsors">Github 赞助者</string>
<string name="google_play_subscribers">Google Play 订阅者</string>
<string name="not_signed_in">未登录</string>
<string name="no_google_play_subscription">找不到符合要求的 Google Play 订阅</string>
<string name="insufficient_sponsorship">找不到符合要求的 GitHub 赞助</string>
<string name="above_average">高于平均</string>

@ -287,7 +287,6 @@
<string name="sign_in_with_google">使用 Google 登入</string>
<string name="github_sponsors">GitHub 贊助者</string>
<string name="google_play_subscribers">Goole Play 訂閱者</string>
<string name="not_signed_in">未登入</string>
<string name="authorization_cancelled">取消授權</string>
<string name="current_subscription">目前訂閱: %s</string>
<string name="price_per_month">$%s/每月</string>

@ -13,7 +13,7 @@
<string name="davx5">DAVx⁵</string>
<string name="decsync">DecSync CC</string>
<string name="tasks_org">Tasks.org</string>
<string name="microsoft">Microsoft To Do</string>
<string name="microsoft">Microsoft To&#xA0;Do</string>
<string name="etebase_url">https://api.etebase.com/partner/tasksorg/</string>
<string name="help_url_sync">https://tasks.org/sync</string>
<string name="caldav_server_owncloud">ownCloud</string>
@ -99,7 +99,6 @@
<string name="p_tags_enabled">drawer_tags_enabled</string>
<string name="p_tags_hide_unused">drawer_tags_hide_unused</string>
<string name="p_places_enabled">drawer_places_enabled</string>
<string name="p_lists_enabled">drawer_lists_enabled</string>
<string name="p_places_hide_unused">drawer_places_hide_unused</string>
<!-- show comments in task edit -->
@ -411,6 +410,7 @@
<string name="param_result">result</string>
<string name="param_state">state</string>
<string name="param_click">click</string>
<string name="param_selection">selection</string>
<string name="event_todoagenda">cp_todoagenda</string>
<string name="event_astrid2taskprovider">cp_astrid2taskprovider</string>
<string name="event_sync_add_account">sync_add_account</string>
@ -422,6 +422,7 @@
<string name="event_request_review">request_review</string>
<string name="event_create_shortcut">create_shortcut</string>
<string name="event_create_widget">create_widget</string>
<string name="event_onboarding_sync">onboarding_sync</string>
<string name="param_type">type</string>
<string name="p_picker_mode_date">picker_mode_date</string>
<string name="p_picker_mode_time">picker_mode_time</string>
@ -429,7 +430,6 @@
<string name="p_completed_tasks_at_bottom">completed_tasks_at_bottom</string>
<string name="p_shown_beast_mode_hint">shown_beast_mode_hint</string>
<string name="p_last_sync">last_sync_time</string>
<string name="p_microsoft_sync">microsoft_sync</string>
<string name="p_multiline_title">multiline_title</string>
<string name="p_dynamic_color">dynamic_color</string>
</resources>

@ -411,6 +411,8 @@ File %1$s contained %2$s.\n\n
<string name="copy_selected_tasks">Copy selected tasks?</string>
<string name="date_and_time">Date and time</string>
<string name="add_account">Add account</string>
<string name="continue_without_sync">Continue without sync</string>
<string name="help_me_choose">Help me choose</string>
<string name="user">User</string>
<string name="password">Password</string>
<string name="url">URL</string>
@ -479,6 +481,7 @@ File %1$s contained %2$s.\n\n
<string name="requires_pro_subscription">Requires pro subscription</string>
<string name="this_feature_requires_a_subscription">This feature requires a subscription</string>
<string name="logout">Log out</string>
<string name="delete_tasks_warning">%s will be deleted. This cannot be undone!</string>
<string name="logout_warning">All data for this account will be removed from your device</string>
<string name="cannot_access_account">Cannot access account</string>
<string name="reinitialize_account">Reinitialize</string>
@ -638,7 +641,6 @@ File %1$s contained %2$s.\n\n
<string name="open_source">Open source</string>
<string name="privacy">Privacy</string>
<string name="authorization_cancelled">Authorization cancelled</string>
<string name="not_signed_in">Not signed in</string>
<string name="google_play_subscribers">Google Play subscribers</string>
<string name="github_sponsors">GitHub Sponsors</string>
<string name="sign_in_with_google">Sign in with Google</string>

@ -4,8 +4,9 @@
xmlns:tools="http://schemas.android.com/tools">
<Preference
android:key="@string/add_account"
android:title="@string/not_signed_in" />
android:key="@string/add_account"
android:title="@string/add_account"
app:icon="@drawable/ic_outline_add_24px" />
<Preference
app:fragment="org.tasks.preferences.fragments.LookAndFeel"

@ -2,12 +2,6 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_microsoft_sync"
android:title="Microsoft To Do Sync"
android:summary="Early access feature, please report any bugs!" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="@string/p_astrid_sort_enabled"

@ -59,11 +59,4 @@
</PreferenceCategory>
<PreferenceCategory android:title="@string/local_lists">
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/p_lists_enabled"
android:title="@string/enabled" />
</PreferenceCategory>
</PreferenceScreen>

@ -66,13 +66,12 @@ abstract class CaldavDao {
@Query("""
SELECT *
FROM caldav_accounts
WHERE cda_account_type NOT IN (:exclude)
ORDER BY CASE cda_account_type
WHEN $TYPE_TASKS THEN 0
ELSE 1
END, UPPER(cda_name)
""")
abstract fun watchAccounts(exclude: List<Int> = emptyList()): Flow<List<CaldavAccount>>
abstract fun watchAccounts(): Flow<List<CaldavAccount>>
@Query("""
SELECT *
@ -416,9 +415,16 @@ ORDER BY primary_sort
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insertOrReplace(googleTaskList: CaldavCalendar): Long
companion object {
const val LOCAL = "local"
@Query("""
SELECT COUNT(*)
FROM caldav_tasks
INNER JOIN caldav_lists ON cd_calendar = cdl_uuid
WHERE cdl_account = :account
AND cd_deleted = 0
""")
abstract suspend fun countTasks(account: String): Int
companion object {
fun Long.toAppleEpoch(): Long = (this - APPLE_EPOCH) / 1000
}
}

@ -5,7 +5,6 @@ import androidx.room.Delete
import androidx.room.Query
import androidx.room.Transaction
import co.touchlab.kermit.Logger
import org.tasks.data.dao.CaldavDao.Companion.LOCAL
import org.tasks.data.db.SuspendDbUtils.chunkedMap
import org.tasks.data.db.SuspendDbUtils.eachChunk
import org.tasks.data.entity.CaldavAccount
@ -90,7 +89,14 @@ WHERE recurring = 1
@Delete
internal abstract suspend fun deleteCaldavAccount(caldavAccount: CaldavAccount)
@Query("DELETE FROM tasks WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task INNER JOIN caldav_lists ON cdl_uuid = cd_calendar WHERE cdl_account = '$LOCAL' AND deleted > 0 AND cd_deleted = 0)")
@Query("""
DELETE FROM tasks WHERE _id IN (
SELECT _id FROM tasks
INNER JOIN caldav_tasks ON _id = cd_task
INNER JOIN caldav_lists ON cdl_uuid = cd_calendar
INNER JOIN caldav_accounts ON cdl_account = cda_uuid
WHERE cda_account_type == ${CaldavAccount.TYPE_LOCAL} AND deleted > 0 AND cd_deleted = 0)
""")
abstract suspend fun purgeDeleted()
@Transaction

@ -62,6 +62,9 @@ data class CaldavAccount(
val isGoogleTasks: Boolean
get() = accountType == TYPE_GOOGLE_TASKS
val isLocalList: Boolean
get() = accountType == TYPE_LOCAL
val isSuppressRepeatingTasks: Boolean
get() = when (serverType) {
SERVER_OPEN_XCHANGE,

@ -21,7 +21,4 @@ interface DrawerConfiguration {
val recentlyModifiedFilter: Boolean
get() = true
val localListsEnabled: Boolean
get() = true
}

@ -11,8 +11,12 @@ import tasks.kmp.generated.resources.default_list
private val mutex = Mutex()
suspend fun CaldavDao.setupLocalAccount(): CaldavAccount = mutex.withLock {
val account = getLocalAccount()
suspend fun CaldavDao.newLocalAccount(): CaldavAccount = mutex.withLock {
val account = CaldavAccount(
accountType = CaldavAccount.TYPE_LOCAL,
uuid = UUIDHelper.newUUID(),
)
.let { it.copy(id = insert(it)) }
getLocalList(account)
return account
}
@ -22,12 +26,7 @@ suspend fun CaldavDao.getLocalList() = mutex.withLock {
}
suspend fun CaldavDao.getLocalAccount() =
getAccountByUuid(CaldavDao.LOCAL)
?: CaldavAccount(
accountType = CaldavAccount.TYPE_LOCAL,
uuid = CaldavDao.LOCAL,
)
.let { it.copy(id = insert(it)) }
getAccounts(CaldavAccount.TYPE_LOCAL).firstOrNull() ?: newLocalAccount()
private suspend fun CaldavDao.getLocalList(account: CaldavAccount): CaldavCalendar =
getCalendarsByAccount(account.uuid!!).getOrNull(0)

@ -13,7 +13,6 @@ import org.tasks.data.dao.TaskDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.setupLocalAccount
import org.tasks.data.toLocationFilter
import org.tasks.data.toTagFilter
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType
@ -193,9 +192,8 @@ class FilterProvider(
showCreate: Boolean,
forceExpand: Boolean,
): List<FilterListItem> =
caldavDao.getAccounts()
.ifEmpty { listOf(caldavDao.setupLocalAccount()) }
.filter { it.accountType != TYPE_LOCAL || configuration.localListsEnabled }
caldavDao
.getAccounts()
.flatMap {
caldavFilter(
it,
@ -213,7 +211,7 @@ class FilterProvider(
return listOf(
NavigationDrawerSubheader(
if (account.accountType == TYPE_LOCAL) {
getString(Res.string.drawer_local_lists)
account.name?.takeIf { it.isNotBlank() } ?: getString(Res.string.drawer_local_lists)
} else {
account.name
},

Loading…
Cancel
Save