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

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

@ -11,8 +11,10 @@ import android.graphics.Color
import android.os.Bundle import android.os.Bundle
import androidx.activity.SystemBarStyle import androidx.activity.SystemBarStyle
import androidx.activity.compose.BackHandler import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
@ -31,13 +33,16 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.content.IntentCompat.getParcelableExtra
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.todoroo.astrid.adapter.SubheaderClickHandler import com.todoroo.astrid.adapter.SubheaderClickHandler
import com.todoroo.astrid.dao.TaskDao import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity
import com.todoroo.astrid.service.TaskCreator import com.todoroo.astrid.service.TaskCreator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -45,22 +50,36 @@ import kotlinx.coroutines.runBlocking
import org.tasks.BuildConfig import org.tasks.BuildConfig
import org.tasks.R import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.auth.SignInActivity
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.compose.AddAccountDestination
import org.tasks.compose.HomeDestination 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.compose.home.HomeScreen
import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.AlarmDao
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.dao.LocationDao import org.tasks.data.dao.LocationDao
import org.tasks.data.dao.TagDataDao import org.tasks.data.dao.TagDataDao
import org.tasks.data.entity.Task import org.tasks.data.entity.Task
import org.tasks.dialogs.ImportTasksDialog
import org.tasks.dialogs.NewFilterDialog import org.tasks.dialogs.NewFilterDialog
import org.tasks.etebase.EtebaseAccountSettingsActivity
import org.tasks.extensions.Context.nightMode import org.tasks.extensions.Context.nightMode
import org.tasks.extensions.Context.toast
import org.tasks.extensions.broughtToFront import org.tasks.extensions.broughtToFront
import org.tasks.extensions.flagsToString import org.tasks.extensions.flagsToString
import org.tasks.extensions.isFromHistory import org.tasks.extensions.isFromHistory
import org.tasks.files.FileHelper
import org.tasks.filters.Filter import org.tasks.filters.Filter
import org.tasks.jobs.WorkManager
import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences 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.ColorProvider
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme import org.tasks.themes.Theme
@ -82,6 +101,8 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var alarmDao: AlarmDao @Inject lateinit var alarmDao: AlarmDao
@Inject lateinit var firebase: Firebase @Inject lateinit var firebase: Firebase
@Inject lateinit var caldavDao: CaldavDao @Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
private val viewModel: MainActivityViewModel by viewModels() private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0 private var currentNightMode = 0
@ -125,7 +146,7 @@ class MainActivity : AppCompatActivity() {
LaunchedEffect(hasAccount) { LaunchedEffect(hasAccount) {
Timber.d("hasAccount=$hasAccount") Timber.d("hasAccount=$hasAccount")
if (hasAccount == false) { if (hasAccount == false) {
// TODO: navigate to add account screen navController.navigate(AddAccountDestination(showImport = true))
} }
isReady = hasAccount != null isReady = hasAccount != null
} }
@ -133,7 +154,87 @@ class MainActivity : AppCompatActivity() {
navController = navController, navController = navController,
startDestination = HomeDestination, 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> { composable<HomeDestination> {
if (hasAccount != true) {
return@composable
}
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val state = viewModel.state.collectAsStateWithLifecycle().value val state = viewModel.state.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState( val drawerState = rememberDrawerState(

@ -9,4 +9,5 @@ object Constants {
const val SYNC_TYPE_ETEBASE = "etebase" const val SYNC_TYPE_ETEBASE = "etebase"
const val SYNC_TYPE_DECSYNC = "decsync" const val SYNC_TYPE_DECSYNC = "decsync"
const val SYNC_TYPE_MICROSOFT = "microsoft" 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.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN 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.data.entity.Task
import org.tasks.databinding.ActivityCaldavAccountSettingsBinding import org.tasks.databinding.ActivityCaldavAccountSettingsBinding
import org.tasks.dialogs.DialogBuilder import org.tasks.dialogs.DialogBuilder
@ -116,7 +117,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT) 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)) newSnackbar(getString(R.string.this_feature_requires_a_subscription))
.setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE) .setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE)
.setAction(R.string.button_subscribe) { .setAction(R.string.button_subscribe) {
@ -308,7 +309,8 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
private fun newSnackbar(message: String?): Snackbar { private fun newSnackbar(message: String?): Snackbar {
val snackbar = Snackbar.make(binding.rootLayout, message!!, 8000) 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)) .setActionTextColor(getColor(R.color.snackbar_action_color))
snackbar snackbar
.view .view
@ -341,7 +343,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
} }
} }
private fun removeAccountPrompt() { protected open suspend fun removeAccountPrompt() {
if (requestInProgress()) { if (requestInProgress()) {
return return
} }
@ -378,7 +380,9 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.menu_help -> openUri(helpUrl) R.id.menu_help -> openUri(helpUrl)
R.id.remove -> removeAccountPrompt() R.id.remove -> lifecycleScope.launch {
removeAccountPrompt()
}
} }
return onOptionsItemSelected(item) 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 android.os.Bundle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.runBlocking
import org.tasks.compose.DeleteButton import org.tasks.compose.DeleteButton
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavCalendar
import org.tasks.themes.TasksTheme import org.tasks.themes.TasksTheme
@AndroidEntryPoint @AndroidEntryPoint
class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() { class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val canDelete = runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 }
setContent { setContent {
TasksTheme { TasksTheme {
BaseCaldavSettingsContent ( 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) = override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) =
onDeleted(true) onDeleted(true)
} }

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

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

@ -27,7 +27,4 @@ class PreferenceDrawerConfiguration(
override val recentlyModifiedFilter: Boolean override val recentlyModifiedFilter: Boolean
get() = preferences.getBoolean(R.string.p_show_recently_modified_filter, super.recentlyModifiedFilter) 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 android.content.Context
import androidx.hilt.work.HiltWorker import androidx.hilt.work.HiltWorker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import com.todoroo.astrid.service.TaskDeleter
import dagger.assisted.Assisted import dagger.assisted.Assisted
import dagger.assisted.AssistedInject import dagger.assisted.AssistedInject
import org.tasks.R
import org.tasks.analytics.Firebase import org.tasks.analytics.Firebase
import org.tasks.caldav.CaldavClientProvider import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.injection.BaseWorker import org.tasks.injection.BaseWorker
import org.tasks.preferences.Preferences
import org.tasks.sync.SyncAdapters import org.tasks.sync.SyncAdapters
@HiltWorker @HiltWorker
@ -20,14 +20,18 @@ class MigrateLocalWork @AssistedInject constructor(
firebase: Firebase, firebase: Firebase,
private val clientProvider: CaldavClientProvider, private val clientProvider: CaldavClientProvider,
private val caldavDao: CaldavDao, private val caldavDao: CaldavDao,
private val preferences: Preferences, private val syncAdapters: SyncAdapters,
private val syncAdapters: SyncAdapters private val taskDeleter: TaskDeleter,
) : BaseWorker(context, workerParams, firebase) { ) : BaseWorker(context, workerParams, firebase) {
override suspend fun run(): Result { override suspend fun run(): Result {
val uuid = inputData.getString(EXTRA_ACCOUNT) ?: return Result.failure() val uuid = inputData.getString(EXTRA_ACCOUNT) ?: return Result.failure()
val caldavAccount = caldavDao.getAccountByUuid(uuid) ?: return Result.failure() val caldavAccount = caldavDao.getAccountByUuid(uuid) ?: return Result.failure()
val caldavClient = clientProvider.forAccount(caldavAccount) 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( caldavDao.update(
it.copy( it.copy(
url = caldavClient.makeCollection(it.name!!, it.color), 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() syncAdapters.sync()
return Result.success() 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.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.CaldavTask
import org.tasks.data.entity.Task 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.CaldavFilter
import org.tasks.filters.CustomFilter import org.tasks.filters.CustomFilter
import org.tasks.filters.Filter import org.tasks.filters.Filter
@ -92,7 +90,7 @@ class DefaultFilterProvider @Inject constructor(
?.let { caldavDao.getAccountByUuid(it) } ?.let { caldavDao.getAccountByUuid(it) }
?.let { account -> CaldavFilter(calendar = list, account = account) } ?.let { account -> CaldavFilter(calendar = list, account = account) }
} }
?: CaldavFilter(calendar = caldavDao.getLocalList(), account = caldavDao.getLocalAccount()) ?: throw IllegalStateException()
defaultList = filter defaultList = filter
return filter return filter
} }

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

@ -27,7 +27,7 @@ const val REQUEST_DRIVE_BACKUP = 12002
private const val REQUEST_PICKER = 10003 private const val REQUEST_PICKER = 10003
private const val REQUEST_BACKUP_NOW = 10004 private const val REQUEST_BACKUP_NOW = 10004
private const val FRAG_TAG_EXPORT_TASKS = "frag_tag_export_tasks" 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 @AndroidEntryPoint
class Backups : InjectingPreferenceFragment() { class Backups : InjectingPreferenceFragment() {

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

@ -22,7 +22,6 @@ import org.tasks.auth.SignInActivity
import org.tasks.auth.SignInActivity.Platform import org.tasks.auth.SignInActivity.Platform
import org.tasks.billing.Inventory import org.tasks.billing.Inventory
import org.tasks.billing.Purchase import org.tasks.billing.Purchase
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired
import org.tasks.extensions.Context.openUri import org.tasks.extensions.Context.openUri
@ -203,7 +202,8 @@ class TasksAccount : BaseAccountPreference() {
} }
lifecycleScope.launch { 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) val quantityString = resources.getQuantityString(R.plurals.list_count, listCount, listCount)
findPreference(R.string.migrate).isVisible = listCount > 0 findPreference(R.string.migrate).isVisible = listCount > 0
findPreference(R.string.local_lists).summary = findPreference(R.string.local_lists).summary =

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

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

@ -641,7 +641,6 @@
<string name="app_password_delete_confirmation">Всички приложения, които използват паролата ще бъдат отписани</string> <string name="app_password_delete_confirmation">Всички приложения, които използват паролата ще бъдат отписани</string>
<string name="app_password_save">Използвайте тези данни за вход за настройка на друго приложение. Те дават пълен достъп до профила ви в Tasks.org за това не ги записвайте никъде и не ги споделяйте с никого!</string> <string name="app_password_save">Използвайте тези данни за вход за настройка на друго приложение. Те дават пълен достъп до профила ви в Tasks.org за това не ги записвайте никъде и не ги споделяйте с никого!</string>
<string name="app_passwords_more_info">Синхронизирайте задачите и календарите си с други настолни и мобилни приложения. Докоснете за повече информация</string> <string name="app_passwords_more_info">Синхронизирайте задачите и календарите си с други настолни и мобилни приложения. Докоснете за повече информация</string>
<string name="not_signed_in">Не сте вписани</string>
<string name="insufficient_subscription">Недостатъчно ниво на абонамент. За да бъде възстановена услугата надстройте своя абонамент.</string> <string name="insufficient_subscription">Недостатъчно ниво на абонамент. За да бъде възстановена услугата надстройте своя абонамент.</string>
<string name="authorization_cancelled">Удостоверяването е спряно</string> <string name="authorization_cancelled">Удостоверяването е спряно</string>
<string name="background_location">Местоположение във фонов режим</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="sign_in_with_google">Přihlášení přes Google</string>
<string name="github_sponsors">sponzoři na GitHubu</string> <string name="github_sponsors">sponzoři na GitHubu</string>
<string name="google_play_subscribers">předplatitelé Google Play</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="authorization_cancelled">Autorizace zrušena</string>
<string name="privacy">Soukromí</string> <string name="privacy">Soukromí</string>
<string name="open_source">Otevřený zdrojový kód</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="sign_in_with_google">Log ind med Google</string>
<string name="github_sponsors">GitHub-sponsorer</string> <string name="github_sponsors">GitHub-sponsorer</string>
<string name="google_play_subscribers">Google Play-abonnenter</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="authorization_cancelled">Autorisation annulleret</string>
<string name="follow_reddit">Følg r/tasks</string> <string name="follow_reddit">Følg r/tasks</string>
<string name="current_subscription">Nuværende abonnement: %s</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="sign_in_with_github">Mit GitHub anmelden</string>
<string name="github_sponsors">GitHub-Sponsoren</string> <string name="github_sponsors">GitHub-Sponsoren</string>
<string name="google_play_subscribers">Google Play-Abonnenten</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="authorization_cancelled">Autorisierung abgebrochen</string>
<string name="sign_in_with_google">Mit Google anmelden</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> <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="save_percent">Ŝpari je %d%%</string>
<string name="remove_user_confirmation">%1$s ne plu povos atingi %2$s</string> <string name="remove_user_confirmation">%1$s ne plu povos atingi %2$s</string>
<string name="share_list">Komunigi liston</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="widget_id">Fenestraĵa ID: %d</string>
<string name="sort_start_group">Komenci %s</string> <string name="sort_start_group">Komenci %s</string>
<string name="open_last_viewed_list">Malfermi liston de laste rigardita</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="sign_in_with_github">Acceder con GitHub</string>
<string name="github_sponsors">Patrocinadores de GitHub</string> <string name="github_sponsors">Patrocinadores de GitHub</string>
<string name="google_play_subscribers">Suscriptores de Google Play</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="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="insufficient_sponsorship">No se encontró ningún patrocinio de GitHub elegible</string>
<string name="above_average">Por encima del promedio</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="third_party_licenses">Kolmandate osapoolte litsentsid</string>
<string name="name_your_price">Ütle oma hind</string> <string name="name_your_price">Ütle oma hind</string>
<string name="hide_unused_places">Peida kasutamata asukohad</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="app_password_last_access">Viimati kasutatud: %s</string>
<string name="invite_awaiting_response">Kutse ootab vastust</string> <string name="invite_awaiting_response">Kutse ootab vastust</string>
<string name="completion_sound">Esita lõpetamise heli</string> <string name="completion_sound">Esita lõpetamise heli</string>

@ -555,7 +555,6 @@
<string name="authentication_required">Autentifikazioa derrigorrezkoa</string> <string name="authentication_required">Autentifikazioa derrigorrezkoa</string>
<string name="sign_in_with_github">Hasi saioa GitHub erabiliz</string> <string name="sign_in_with_github">Hasi saioa GitHub erabiliz</string>
<string name="google_play_subscribers">Google Play harpideak</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="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="insufficient_sponsorship">Ez da GitHub sponsorship egokirik aurkitu</string>
<string name="local_lists">Zerrenda lokalak</string> <string name="local_lists">Zerrenda lokalak</string>

@ -585,7 +585,6 @@
<string name="astrid_sort_order">Astrid manuaalinen lajittelu</string> <string name="astrid_sort_order">Astrid manuaalinen lajittelu</string>
<string name="github_sponsors">GitHubin sponsorit</string> <string name="github_sponsors">GitHubin sponsorit</string>
<string name="google_play_subscribers">Google Play -tilaajat</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="authorization_cancelled">Lupa peruutettu</string>
<string name="privacy">yksityisyys</string> <string name="privacy">yksityisyys</string>
<string name="open_source">Avoin lähdekoodi</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="sign_in_with_github">Connectez-vous avec « GitHub »</string>
<string name="github_sponsors">Parrains « GitHub »</string> <string name="github_sponsors">Parrains « GitHub »</string>
<string name="google_play_subscribers">Abonnés « Google Play »</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="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="insufficient_sponsorship">Aucun parrainage « GitHub » éligible n\'a été trouvé</string>
<string name="above_average">Au dessus de la moyenne</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="custom_filter_has_reminder">Ten un recordatorio</string>
<string name="price_per_month">$%s/mes</string> <string name="price_per_month">$%s/mes</string>
<string name="authorization_cancelled">Cancelouse a autenticación</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="above_average">Por riba da media</string>
<string name="save_percent">Gardar %d%%</string> <string name="save_percent">Gardar %d%%</string>
<string name="app_password">Contrasinal da app</string> <string name="app_password">Contrasinal da app</string>

@ -139,7 +139,6 @@
<string name="local_lists">Popisi na uređaju</string> <string name="local_lists">Popisi na uređaju</string>
<string name="help">Pomoć</string> <string name="help">Pomoć</string>
<string name="choose_synchronization_service">Odaberi platformu</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="contact_developer">Kontaktiraj programera</string>
<string name="hide_unused_places">Sakrij nekorištena mjesta</string> <string name="hide_unused_places">Sakrij nekorištena mjesta</string>
<string name="place_settings">Postavke 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="sign_in_with_github">Bejelentkezés GitHub fiókkal</string>
<string name="github_sponsors">GitHub szponzorok</string> <string name="github_sponsors">GitHub szponzorok</string>
<string name="google_play_subscribers">Google Play előfizetők</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="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="insufficient_sponsorship">Nem található GitHub szponzorálás</string>
<string name="save_percent">Mentés %d%%</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="sign_in_with_google">Masuk dengan Google</string>
<string name="github_sponsors">Sponsor GitHub</string> <string name="github_sponsors">Sponsor GitHub</string>
<string name="google_play_subscribers">Pelanggan Google Play</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="authorization_cancelled">Otorisasi dibatalkan</string>
<string name="follow_reddit">Gabung r/tasks</string> <string name="follow_reddit">Gabung r/tasks</string>
<string name="current_subscription">Langganan saat ini: %s</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="sign_in_with_github">Accedi con GitHub</string>
<string name="github_sponsors">Sponsor di GitHub</string> <string name="github_sponsors">Sponsor di GitHub</string>
<string name="google_play_subscribers">Abbonati a Google Play</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="no_google_play_subscription">Nessun abbonamento Google Play idoneo trovato</string>
<string name="insufficient_sponsorship">Nessuna sponsorizzazione GitHub idonea trovata</string> <string name="insufficient_sponsorship">Nessuna sponsorizzazione GitHub idonea trovata</string>
<string name="decsync_selection_description">Sincronizzazione basata sui file</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="sign_in_with_google">להיכנס באמצעות Google</string>
<string name="github_sponsors">נותני חסות GitHub</string> <string name="github_sponsors">נותני חסות GitHub</string>
<string name="google_play_subscribers">מנויי Google Play</string> <string name="google_play_subscribers">מנויי Google Play</string>
<string name="not_signed_in">לא מחובר</string>
<string name="authorization_cancelled">הרשאה בוטלה</string> <string name="authorization_cancelled">הרשאה בוטלה</string>
<string name="no_google_play_subscription">לא נמצא מינוי Google Play זכאי</string> <string name="no_google_play_subscription">לא נמצא מינוי Google Play זכאי</string>
<string name="insufficient_sponsorship">לא נמצאה חסות GitHub זכאית</string> <string name="insufficient_sponsorship">לא נמצאה חסות GitHub זכאית</string>

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

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

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

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

@ -539,7 +539,6 @@
<string name="github_sponsors">GitHub sponsors</string> <string name="github_sponsors">GitHub sponsors</string>
<string name="google_play_subscribers">Google Play abonnees</string> <string name="google_play_subscribers">Google Play abonnees</string>
<string name="sign_in_with_github">Log in met GitHub</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="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="insufficient_sponsorship">Geen in aanmerking komend GitHub-sponsoring gevonden</string>
<string name="save_percent">Bespaar %d%%</string> <string name="save_percent">Bespaar %d%%</string>

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

@ -592,7 +592,6 @@
<string name="sign_in_with_google">Faça login com Google</string> <string name="sign_in_with_google">Faça login com Google</string>
<string name="github_sponsors">Patrocinadores do GitHub</string> <string name="github_sponsors">Patrocinadores do GitHub</string>
<string name="google_play_subscribers">Assinantes do Google Play</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="authorization_cancelled">Autorização cancelada</string>
<string name="privacy">Privacidade</string> <string name="privacy">Privacidade</string>
<string name="open_source">Código aberto</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="sign_in_with_google">Iniciar sessão com o Google</string>
<string name="github_sponsors">Patrocinadores do GitHub</string> <string name="github_sponsors">Patrocinadores do GitHub</string>
<string name="google_play_subscribers">Subscritores do Google Play</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="privacy">Privacidade</string>
<string name="open_source">Código-fonte aberto</string> <string name="open_source">Código-fonte aberto</string>
<string name="issue_tracker">Reportar problemas</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="sign_in_with_google">Conectează-te cu Google</string>
<string name="github_sponsors">Sponsorii GitHub</string> <string name="github_sponsors">Sponsorii GitHub</string>
<string name="google_play_subscribers">Abonații Google Play</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="authorization_cancelled">Autorizație anulată</string>
<string name="privacy">Confidențialitate</string> <string name="privacy">Confidențialitate</string>
<string name="open_source">Sursă deschisă</string> <string name="open_source">Sursă deschisă</string>

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

@ -91,7 +91,6 @@
<string name="sign_in_with_github">GitHub සමඟ පුරනය වන්න</string> <string name="sign_in_with_github">GitHub සමඟ පුරනය වන්න</string>
<string name="github_sponsors">GitHub අනුග්‍රාහකයන්</string> <string name="github_sponsors">GitHub අනුග්‍රාහකයන්</string>
<string name="google_play_subscribers">Google Play ග්‍රාහකයින්</string> <string name="google_play_subscribers">Google Play ග්‍රාහකයින්</string>
<string name="not_signed_in">පුරනය වී නොමැත</string>
<string name="authorization_cancelled">අවසරය අවලංගු කරන ලදි</string> <string name="authorization_cancelled">අවසරය අවලංගු කරන ලදි</string>
<string name="privacy">පෞද්ගලිකත්වය</string> <string name="privacy">පෞද්ගලිකත්වය</string>
<string name="open_source">විවෘත මූලාශ්‍රය</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_priority">Bez nastavenej dôležitosti</string>
<string name="filter_no_tags">Bez štítkov</string> <string name="filter_no_tags">Bez štítkov</string>
<string name="privacy">Súkromie</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="alarm_before_due">%s pred termínom</string>
<string name="documentation">Dokumentácia</string> <string name="documentation">Dokumentácia</string>
<string name="color_wheel">Viac farieb</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="sign_in_with_google">Logga in med Google</string>
<string name="github_sponsors">GitHub-sponsorer</string> <string name="github_sponsors">GitHub-sponsorer</string>
<string name="google_play_subscribers">Google Play-prenumeranter</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="authorization_cancelled">Auktorisationen upphävd</string>
<string name="privacy">Integritet</string> <string name="privacy">Integritet</string>
<string name="open_source">Öppen källkod</string> <string name="open_source">Öppen källkod</string>

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

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

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

@ -478,7 +478,6 @@
<string name="sign_in_with_google">Увійти через Google</string> <string name="sign_in_with_google">Увійти через Google</string>
<string name="github_sponsors">Спонсори GitHub</string> <string name="github_sponsors">Спонсори GitHub</string>
<string name="google_play_subscribers">Підписники Google Play</string> <string name="google_play_subscribers">Підписники Google Play</string>
<string name="not_signed_in">Не авторизовані</string>
<string name="authorization_cancelled">Авторизацію скасовано</string> <string name="authorization_cancelled">Авторизацію скасовано</string>
<string name="follow_reddit">Долучитися до r/tasks</string> <string name="follow_reddit">Долучитися до r/tasks</string>
<string name="current_subscription">Поточна підписка: %s</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="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="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="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="authorization_cancelled">Đã huỷ xác thực</string>
<string name="privacy">Riêng tư</string> <string name="privacy">Riêng tư</string>
<string name="open_source">Mã nguồn mở</string> <string name="open_source">Mã nguồn mở</string>

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

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

@ -13,7 +13,7 @@
<string name="davx5">DAVx⁵</string> <string name="davx5">DAVx⁵</string>
<string name="decsync">DecSync CC</string> <string name="decsync">DecSync CC</string>
<string name="tasks_org">Tasks.org</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="etebase_url">https://api.etebase.com/partner/tasksorg/</string>
<string name="help_url_sync">https://tasks.org/sync</string> <string name="help_url_sync">https://tasks.org/sync</string>
<string name="caldav_server_owncloud">ownCloud</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_enabled">drawer_tags_enabled</string>
<string name="p_tags_hide_unused">drawer_tags_hide_unused</string> <string name="p_tags_hide_unused">drawer_tags_hide_unused</string>
<string name="p_places_enabled">drawer_places_enabled</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> <string name="p_places_hide_unused">drawer_places_hide_unused</string>
<!-- show comments in task edit --> <!-- show comments in task edit -->
@ -411,6 +410,7 @@
<string name="param_result">result</string> <string name="param_result">result</string>
<string name="param_state">state</string> <string name="param_state">state</string>
<string name="param_click">click</string> <string name="param_click">click</string>
<string name="param_selection">selection</string>
<string name="event_todoagenda">cp_todoagenda</string> <string name="event_todoagenda">cp_todoagenda</string>
<string name="event_astrid2taskprovider">cp_astrid2taskprovider</string> <string name="event_astrid2taskprovider">cp_astrid2taskprovider</string>
<string name="event_sync_add_account">sync_add_account</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_request_review">request_review</string>
<string name="event_create_shortcut">create_shortcut</string> <string name="event_create_shortcut">create_shortcut</string>
<string name="event_create_widget">create_widget</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="param_type">type</string>
<string name="p_picker_mode_date">picker_mode_date</string> <string name="p_picker_mode_date">picker_mode_date</string>
<string name="p_picker_mode_time">picker_mode_time</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_completed_tasks_at_bottom">completed_tasks_at_bottom</string>
<string name="p_shown_beast_mode_hint">shown_beast_mode_hint</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_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_multiline_title">multiline_title</string>
<string name="p_dynamic_color">dynamic_color</string> <string name="p_dynamic_color">dynamic_color</string>
</resources> </resources>

@ -411,6 +411,8 @@ File %1$s contained %2$s.\n\n
<string name="copy_selected_tasks">Copy selected tasks?</string> <string name="copy_selected_tasks">Copy selected tasks?</string>
<string name="date_and_time">Date and time</string> <string name="date_and_time">Date and time</string>
<string name="add_account">Add account</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="user">User</string>
<string name="password">Password</string> <string name="password">Password</string>
<string name="url">URL</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="requires_pro_subscription">Requires pro subscription</string>
<string name="this_feature_requires_a_subscription">This feature requires a subscription</string> <string name="this_feature_requires_a_subscription">This feature requires a subscription</string>
<string name="logout">Log out</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="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="cannot_access_account">Cannot access account</string>
<string name="reinitialize_account">Reinitialize</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="open_source">Open source</string>
<string name="privacy">Privacy</string> <string name="privacy">Privacy</string>
<string name="authorization_cancelled">Authorization cancelled</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="google_play_subscribers">Google Play subscribers</string>
<string name="github_sponsors">GitHub Sponsors</string> <string name="github_sponsors">GitHub Sponsors</string>
<string name="sign_in_with_google">Sign in with Google</string> <string name="sign_in_with_google">Sign in with Google</string>

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

@ -2,12 +2,6 @@
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" <PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> 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 <SwitchPreferenceCompat
android:defaultValue="false" android:defaultValue="false"
android:key="@string/p_astrid_sort_enabled" android:key="@string/p_astrid_sort_enabled"

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

@ -66,13 +66,12 @@ abstract class CaldavDao {
@Query(""" @Query("""
SELECT * SELECT *
FROM caldav_accounts FROM caldav_accounts
WHERE cda_account_type NOT IN (:exclude)
ORDER BY CASE cda_account_type ORDER BY CASE cda_account_type
WHEN $TYPE_TASKS THEN 0 WHEN $TYPE_TASKS THEN 0
ELSE 1 ELSE 1
END, UPPER(cda_name) END, UPPER(cda_name)
""") """)
abstract fun watchAccounts(exclude: List<Int> = emptyList()): Flow<List<CaldavAccount>> abstract fun watchAccounts(): Flow<List<CaldavAccount>>
@Query(""" @Query("""
SELECT * SELECT *
@ -416,9 +415,16 @@ ORDER BY primary_sort
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun insertOrReplace(googleTaskList: CaldavCalendar): Long abstract suspend fun insertOrReplace(googleTaskList: CaldavCalendar): Long
companion object { @Query("""
const val LOCAL = "local" 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 fun Long.toAppleEpoch(): Long = (this - APPLE_EPOCH) / 1000
} }
} }

@ -5,7 +5,6 @@ import androidx.room.Delete
import androidx.room.Query import androidx.room.Query
import androidx.room.Transaction import androidx.room.Transaction
import co.touchlab.kermit.Logger 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.chunkedMap
import org.tasks.data.db.SuspendDbUtils.eachChunk import org.tasks.data.db.SuspendDbUtils.eachChunk
import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount
@ -90,7 +89,14 @@ WHERE recurring = 1
@Delete @Delete
internal abstract suspend fun deleteCaldavAccount(caldavAccount: CaldavAccount) 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() abstract suspend fun purgeDeleted()
@Transaction @Transaction

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

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

@ -11,8 +11,12 @@ import tasks.kmp.generated.resources.default_list
private val mutex = Mutex() private val mutex = Mutex()
suspend fun CaldavDao.setupLocalAccount(): CaldavAccount = mutex.withLock { suspend fun CaldavDao.newLocalAccount(): CaldavAccount = mutex.withLock {
val account = getLocalAccount() val account = CaldavAccount(
accountType = CaldavAccount.TYPE_LOCAL,
uuid = UUIDHelper.newUUID(),
)
.let { it.copy(id = insert(it)) }
getLocalList(account) getLocalList(account)
return account return account
} }
@ -22,12 +26,7 @@ suspend fun CaldavDao.getLocalList() = mutex.withLock {
} }
suspend fun CaldavDao.getLocalAccount() = suspend fun CaldavDao.getLocalAccount() =
getAccountByUuid(CaldavDao.LOCAL) getAccounts(CaldavAccount.TYPE_LOCAL).firstOrNull() ?: newLocalAccount()
?: CaldavAccount(
accountType = CaldavAccount.TYPE_LOCAL,
uuid = CaldavDao.LOCAL,
)
.let { it.copy(id = insert(it)) }
private suspend fun CaldavDao.getLocalList(account: CaldavAccount): CaldavCalendar = private suspend fun CaldavDao.getLocalList(account: CaldavAccount): CaldavCalendar =
getCalendarsByAccount(account.uuid!!).getOrNull(0) 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
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL
import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS
import org.tasks.data.setupLocalAccount
import org.tasks.data.toLocationFilter import org.tasks.data.toLocationFilter
import org.tasks.data.toTagFilter import org.tasks.data.toTagFilter
import org.tasks.filters.NavigationDrawerSubheader.SubheaderType import org.tasks.filters.NavigationDrawerSubheader.SubheaderType
@ -193,9 +192,8 @@ class FilterProvider(
showCreate: Boolean, showCreate: Boolean,
forceExpand: Boolean, forceExpand: Boolean,
): List<FilterListItem> = ): List<FilterListItem> =
caldavDao.getAccounts() caldavDao
.ifEmpty { listOf(caldavDao.setupLocalAccount()) } .getAccounts()
.filter { it.accountType != TYPE_LOCAL || configuration.localListsEnabled }
.flatMap { .flatMap {
caldavFilter( caldavFilter(
it, it,
@ -213,7 +211,7 @@ class FilterProvider(
return listOf( return listOf(
NavigationDrawerSubheader( NavigationDrawerSubheader(
if (account.accountType == TYPE_LOCAL) { if (account.accountType == TYPE_LOCAL) {
getString(Res.string.drawer_local_lists) account.name?.takeIf { it.isNotBlank() } ?: getString(Res.string.drawer_local_lists)
} else { } else {
account.name account.name
}, },

Loading…
Cancel
Save