New WelcomeScreen with consent disclosure

pull/4057/merge
Alex Baker 3 days ago
parent 6361145925
commit 660e831725

@ -37,4 +37,7 @@ class Firebase @Inject constructor(
private fun days(default: Long): Long =
TimeUnit.DAYS.toMillis(default)
fun getTosVersion(): Int =
context.resources.getInteger(R.integer.default_tos_version)
}

@ -109,4 +109,13 @@ class Firebase @Inject constructor(
private fun days(key: String, default: Long): Long =
TimeUnit.DAYS.toMillis(remoteConfig?.getLong(key) ?: default)
fun getTosVersion(): Int {
val default = context.resources.getInteger(R.integer.default_tos_version)
return remoteConfig
?.getLong(context.getString(R.string.remote_config_tos_version))
?.toInt()
?.takeIf { it >= default }
?: default
}
}

@ -28,6 +28,7 @@ import androidx.compose.material3.adaptive.layout.calculatePaneScaffoldDirective
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.unit.dp
@ -39,7 +40,6 @@ 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
@ -55,6 +55,9 @@ import org.tasks.billing.Inventory
import org.tasks.caldav.CaldavAccountSettingsActivity
import org.tasks.compose.AddAccountDestination
import org.tasks.compose.HomeDestination
import org.tasks.compose.TosUpdateDialog
import org.tasks.compose.WelcomeDestination
import org.tasks.compose.WelcomeScreen
import org.tasks.compose.accounts.AddAccountScreen
import org.tasks.compose.accounts.AddAccountViewModel
import org.tasks.compose.home.HomeScreen
@ -67,6 +70,7 @@ 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.openUri
import org.tasks.extensions.Context.toast
import org.tasks.extensions.broughtToFront
import org.tasks.extensions.flagsToString
@ -76,6 +80,7 @@ import org.tasks.filters.Filter
import org.tasks.jobs.WorkManager
import org.tasks.preferences.DefaultFilterProvider
import org.tasks.preferences.Preferences
import org.tasks.preferences.TasksPreferences
import org.tasks.preferences.fragments.FRAG_TAG_IMPORT_TASKS
import org.tasks.sync.AddAccountDialog
import org.tasks.sync.SyncAdapters
@ -103,6 +108,7 @@ class MainActivity : AppCompatActivity() {
@Inject lateinit var caldavDao: CaldavDao
@Inject lateinit var syncAdapters: SyncAdapters
@Inject lateinit var workManager: WorkManager
@Inject lateinit var tasksPreferences: TasksPreferences
private val viewModel: MainActivityViewModel by viewModels()
private var currentNightMode = 0
@ -140,24 +146,80 @@ class MainActivity : AppCompatActivity() {
.accountExists
.collectAsStateWithLifecycle(null)
.value
val currentTosVersion = firebase.getTosVersion()
val acceptedTosVersion by tasksPreferences
.flow(TasksPreferences.acceptedTosVersion, 0)
.collectAsStateWithLifecycle(0)
val needsTosAcceptance = acceptedTosVersion < currentTosVersion
suspend fun setAcceptedTosVersion(version: Int) {
tasksPreferences.set(TasksPreferences.acceptedTosVersion, version)
}
LaunchedEffect(hasAccount) {
Timber.d("hasAccount=$hasAccount")
if (hasAccount == false) {
navController.navigate(AddAccountDestination(showImport = true))
when (hasAccount) {
false -> navController.navigate(WelcomeDestination) {
popUpTo(0) { inclusive = true }
}
true -> navController.navigate(HomeDestination) {
popUpTo(0) { inclusive = true }
}
else -> {}
}
isReady = hasAccount != null
}
NavHost(
navController = navController,
startDestination = HomeDestination,
) {
composable<AddAccountDestination> {
val route = it.toRoute<AddAccountDestination>()
LaunchedEffect(hasAccount) {
if (route.showImport && hasAccount == true) {
navController.popBackStack()
composable<WelcomeDestination> {
val addAccountViewModel: AddAccountViewModel = hiltViewModel()
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)
}
}
}
WelcomeScreen(
onBack = { finish() },
onSignIn = {
lifecycleScope.launch {
firebase.logEvent(R.string.event_accept_tos)
setAcceptedTosVersion(currentTosVersion)
navController.navigate(AddAccountDestination)
}
},
onContinueWithoutSync = {
lifecycleScope.launch {
firebase.logEvent(R.string.event_accept_tos)
firebase.logEvent(R.string.event_onboarding_sync, R.string.param_selection to "local")
setAcceptedTosVersion(currentTosVersion)
addAccountViewModel.createLocalAccount()
}
},
onImportBackup = {
lifecycleScope.launch {
firebase.logEvent(R.string.event_accept_tos)
firebase.logEvent(
R.string.event_onboarding_sync,
R.string.param_selection to "import_backup"
)
setAcceptedTosVersion(currentTosVersion)
importBackupLauncher.launch(
FileHelper.newFilePickerIntent(
this@MainActivity,
preferences.backupDirectory
),
)
}
},
openLegalUrl = { url -> openUri(url) }
)
}
composable<AddAccountDestination> {
val addAccountViewModel: AddAccountViewModel = hiltViewModel()
val microsoftVM: MicrosoftSignInViewModel = hiltViewModel()
val syncLauncher =
@ -171,16 +233,7 @@ class MainActivity : AppCompatActivity() {
?.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() },
@ -220,18 +273,27 @@ class MainActivity : AppCompatActivity() {
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
}
// Show ToS update dialog for existing users that need re-acceptance
if (needsTosAcceptance) {
TosUpdateDialog(
isUpdate = acceptedTosVersion > 0,
onAccept = {
lifecycleScope.launch {
firebase.logEvent(R.string.event_accept_tos_update)
setAcceptedTosVersion(currentTosVersion)
workManager.sync(immediate = true)
}
},
onExit = { finish() },
openUrl = { openUri(it) }
)
}
val scope = rememberCoroutineScope()
val state = viewModel.state.collectAsStateWithLifecycle().value
val drawerState = rememberDrawerState(

@ -21,10 +21,6 @@ import androidx.activity.viewModels
import androidx.annotation.MainThread
import androidx.annotation.WorkerThread
import androidx.browser.customtabs.CustomTabsIntent
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Dialog
import androidx.lifecycle.lifecycleScope
import at.bitfire.dav4jvm.exception.HttpException
@ -45,9 +41,7 @@ import org.tasks.billing.Inventory
import org.tasks.billing.PurchaseActivity
import org.tasks.billing.PurchaseActivityViewModel.Companion.EXTRA_GITHUB
import org.tasks.billing.PurchaseActivityViewModel.Companion.EXTRA_NAME_YOUR_PRICE
import org.tasks.compose.ConsentDialog
import org.tasks.compose.SignInDialog
import org.tasks.dialogs.DialogBuilder
import org.tasks.extensions.Context.openUri
import org.tasks.themes.TasksTheme
import org.tasks.themes.Theme
@ -71,7 +65,6 @@ import javax.inject.Inject
class SignInActivity : ComponentActivity() {
@Inject lateinit var theme: Theme
@Inject lateinit var inventory: Inventory
@Inject lateinit var dialogBuilder: DialogBuilder
@Inject lateinit var firebase: Firebase
private val viewModel: SignInViewModel by viewModels()
@ -102,29 +95,17 @@ class SignInActivity : ComponentActivity() {
viewModel.error.observe(this, this::handleError)
val autoSelect = intent.getSerializableExtra(EXTRA_SELECT_SERVICE) as Platform?
setContent {
var selectedPlatform by rememberSaveable {
mutableStateOf(autoSelect)
}
TasksTheme(
theme = theme.themeBase.index,
primary = theme.themeColor.primaryColor,
) {
selectedPlatform
?.let {
Dialog(onDismissRequest = { finish() }) {
ConsentDialog { agree ->
if (agree) {
selectService(it)
} else {
finish()
}
}
}
}
?: Dialog(onDismissRequest = { finish() }) {
if (autoSelect != null) {
selectService(autoSelect)
} else {
setContent {
TasksTheme(
theme = theme.themeBase.index,
primary = theme.themeColor.primaryColor,
) {
Dialog(onDismissRequest = { finish() }) {
SignInDialog(
selected = { selectedPlatform = it },
selected = { selectService(it) },
help = {
openUri(R.string.help_url_sync)
finish()
@ -132,6 +113,7 @@ class SignInActivity : ComponentActivity() {
cancel = { finish() }
)
}
}
}
}
}
@ -176,6 +158,11 @@ class SignInActivity : ComponentActivity() {
}
}
private suspend fun setupAccount(): Boolean {
val account = viewModel.setupAccount(authService)
return account != null
}
override fun onDestroy() {
super.onDestroy()
@ -199,15 +186,17 @@ class SignInActivity : ComponentActivity() {
RC_AUTH ->
if (resultCode == RESULT_OK) {
lifecycleScope.launch {
val account = try {
try {
viewModel.handleResult(authService, data!!)
if (authService.authStateManager.current.isAuthorized) {
if (setupAccount()) {
setResult(RESULT_OK)
finish()
}
}
} catch (e: Exception) {
returnError(e)
}
if (account != null) {
setResult(RESULT_OK)
finish()
}
}
} else {
returnError(

@ -4,7 +4,6 @@ import android.content.Context
import android.content.Intent
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.tasks.data.UUIDHelper
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import net.openid.appauth.AuthorizationException
@ -14,8 +13,9 @@ import net.openid.appauth.GrantTypeValues
import net.openid.appauth.TokenRequest
import org.tasks.R
import org.tasks.caldav.CaldavClientProvider
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.UUIDHelper
import org.tasks.data.dao.CaldavDao
import org.tasks.data.entity.CaldavAccount
import org.tasks.security.KeyStoreEncryption
import timber.log.Timber
import javax.inject.Inject
@ -37,7 +37,7 @@ class SignInViewModel @Inject constructor(
authService = AuthorizationService(iss, context, debugConnectionBuilder)
}
suspend fun handleResult(authService: AuthorizationService, intent: Intent): CaldavAccount? {
suspend fun handleResult(authService: AuthorizationService, intent: Intent) {
val response = AuthorizationResponse.fromIntent(intent)
val ex = AuthorizationException.fromIntent(intent)
val authStateManager = authService.authStateManager
@ -53,12 +53,7 @@ class SignInViewModel @Inject constructor(
ex?.let {
error.value = it
return null
}
return authStateManager.current
.takeIf { it.isAuthorized }
?.let { setupAccount(authService) }
}
suspend fun setupAccount(authService: AuthorizationService): CaldavAccount? {
@ -66,7 +61,7 @@ class SignInViewModel @Inject constructor(
val tokenString = auth.accessToken ?: return null
val idToken = auth.idToken?.let { IdToken(it) } ?: return null
val username = "${authService.iss}_${idToken.sub}"
try {
return try {
val homeSet = provider
.forUrl(
context.getString(R.string.tasks_caldav_url),
@ -75,7 +70,7 @@ class SignInViewModel @Inject constructor(
)
.homeSet(username, tokenString)
val password = encryption.encrypt(tokenString)
return caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
caldavDao.getAccount(CaldavAccount.TYPE_TASKS, username)
?.let {
it.copy(error = null, password = password)
.also { caldavDao.update(it) }
@ -91,9 +86,10 @@ class SignInViewModel @Inject constructor(
it.copy(id = caldavDao.insert(it))
}
} catch (e: Exception) {
Timber.d("setupAccount: caught ${e.javaClass.simpleName} - ${e.message}")
error.postValue(e)
null
}
return null
}
private suspend fun exchangeAuthorizationCode(

@ -12,6 +12,7 @@ import org.tasks.billing.Inventory
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.getPassword
import org.tasks.http.HttpClientFactory
import org.tasks.preferences.TasksPreferences
import org.tasks.security.KeyStoreEncryption
import java.util.concurrent.TimeUnit
import javax.inject.Inject
@ -21,6 +22,7 @@ class CaldavClientProvider @Inject constructor(
private val encryption: KeyStoreEncryption,
private val inventory: Inventory,
private val httpClientFactory: HttpClientFactory,
private val tasksPreferences: TasksPreferences,
) {
private val tasksUrl = context.getString(R.string.tasks_caldav_url)
@ -29,7 +31,8 @@ class CaldavClientProvider @Inject constructor(
username: String? = null,
password: String? = null
): CaldavClient {
val auth = getAuthInterceptor(username, password, url)
val tosVersion = tasksPreferences.get(TasksPreferences.acceptedTosVersion, 0)
val auth = getAuthInterceptor(username, password, url, tosVersion)
return CaldavClient(
this,
createHttpClient(
@ -48,10 +51,12 @@ class CaldavClientProvider @Inject constructor(
}
suspend fun forAccount(account: CaldavAccount, url: String? = account.url): CaldavClient {
val tosVersion = tasksPreferences.get(TasksPreferences.acceptedTosVersion, 0)
val auth = getAuthInterceptor(
account.username,
account.getPassword(encryption),
account.url
account.url,
tosVersion,
)
val client = createHttpClient(auth)
return if (account.isTasksOrg) {
@ -64,10 +69,11 @@ class CaldavClientProvider @Inject constructor(
private fun getAuthInterceptor(
username: String?,
password: String?,
url: String?
url: String?,
tosVersion: Int,
): Interceptor? = when {
username.isNullOrBlank() || password.isNullOrBlank() -> null
url?.startsWith(tasksUrl) == true -> TasksBasicAuth(username, password, inventory)
url?.startsWith(tasksUrl) == true -> TasksBasicAuth(username, password, inventory, tosVersion)
else -> BasicDigestAuthHandler(null, username, password)
}

@ -124,7 +124,7 @@ class CaldavSynchronizer @Inject constructor(
setError(account, e)
} catch (e: HttpException) {
when(e.code) {
402, in 500..599 -> {}
402, 451, in 500..599 -> {}
else -> { firebase.reportException(e) }
}
setError(account, e)

@ -8,7 +8,8 @@ import org.tasks.billing.Inventory
class TasksBasicAuth(
val user: String,
token: String,
private val inventory: Inventory
private val inventory: Inventory,
private val tosVersion: Int,
) : Interceptor {
private val credentials = Credentials.basic(user, token, Charsets.UTF_8)
@ -18,6 +19,7 @@ class TasksBasicAuth(
builder.header(SKU, it.sku)
builder.header(TOKEN, it.purchaseToken)
}
builder.header(TOS_VERSION, tosVersion.toString())
return chain.proceed(builder.build())
}
@ -25,5 +27,6 @@ class TasksBasicAuth(
private const val AUTHORIZATION = "Authorization"
private const val SKU = "tasks-sku"
private const val TOKEN = "tasks-token"
private const val TOS_VERSION = "tasks-tos-version"
}
}
}

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

@ -2,7 +2,6 @@ package org.tasks.compose
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@ -77,44 +76,6 @@ fun SignInDialog(
}
}
@Composable
fun ConsentDialog(
agree: (Boolean) -> Unit,
) {
Column(Modifier.background(MaterialTheme.colorScheme.surface)) {
Text(
text = stringResource(id = R.string.sign_in_to_tasks),
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = stringResource(id = R.string.sign_in_to_tasks_disclosure),
modifier = Modifier.padding(horizontal = 16.dp),
color = MaterialTheme.colorScheme.onSurface,
)
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
) {
TextButton(onClick = { agree(false) }) {
Text(
text = stringResource(id = R.string.consent_deny),
color = MaterialTheme.colorScheme.onSurface,
)
}
TextButton(onClick = { agree(true) }) {
Text(
text = stringResource(id = R.string.consent_agree),
color = MaterialTheme.colorScheme.onSurface,
)
}
}
}
}
@Preview(widthDp = 320)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
@ -123,12 +84,3 @@ fun SignInDialogPreview() {
SignInDialog(selected = {}, help = {}, cancel = {})
}
}
@Preview(widthDp = 320)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, widthDp = 320)
@Composable
fun DisclosurePreview() {
TasksTheme {
ConsentDialog(agree = {})
}
}

@ -0,0 +1,75 @@
package org.tasks.compose
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.window.DialogProperties
import org.tasks.R
import org.tasks.themes.TasksTheme
@Composable
fun TosUpdateDialog(
isUpdate: Boolean,
onAccept: () -> Unit,
onExit: () -> Unit,
openUrl: (String) -> Unit,
) {
AlertDialog(
onDismissRequest = onExit,
properties = DialogProperties(
dismissOnBackPress = true,
dismissOnClickOutside = false,
),
title = {
Text(
text = stringResource(
if (isUpdate) R.string.tos_updated_title else R.string.terms_of_service_proper
)
)
},
text = {
LegalDisclosure(openLegalUrl = openUrl, textAlign = TextAlign.Start)
},
dismissButton = {
TextButton(onClick = onExit) {
Text(text = stringResource(R.string.exit))
}
},
confirmButton = {
Button(onClick = onAccept) {
Text(text = stringResource(R.string.accept))
}
}
)
}
@PreviewLightDark
@Composable
fun TosUpdateDialogPreview() {
TasksTheme {
TosUpdateDialog(
isUpdate = true,
onAccept = {},
onExit = {},
openUrl = {},
)
}
}
@PreviewLightDark
@Composable
fun TosDialogPreview() {
TasksTheme {
TosUpdateDialog(
isUpdate = false,
onAccept = {},
onExit = {},
openUrl = {},
)
}
}

@ -0,0 +1,237 @@
package org.tasks.compose
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.widthIn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
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.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.TextDecoration
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.themes.TasksTheme
@Composable
fun WelcomeScreen(
onBack: () -> Unit,
onSignIn: () -> Unit,
onContinueWithoutSync: () -> Unit,
onImportBackup: () -> Unit,
openLegalUrl: (String) -> Unit,
) {
BackHandler(onBack = onBack)
Scaffold { paddingValues ->
BoxWithConstraints(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
val isCompact = maxHeight < 500.dp
if (isCompact) {
Row(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(24.dp),
) {
Box(
modifier = Modifier.weight(1f),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_round_icon),
contentDescription = stringResource(R.string.tasks_org),
tint = Color.Unspecified,
modifier = Modifier.size(120.dp)
)
}
WelcomeContent(
onSignIn = onSignIn,
onContinueWithoutSync = onContinueWithoutSync,
onImportBackup = onImportBackup,
openLegalUrl = openLegalUrl,
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState()),
)
}
} else {
Column(
modifier = Modifier
.fillMaxSize()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center,
) {
Spacer(modifier = Modifier.weight(1f))
Icon(
painter = painterResource(id = R.drawable.ic_round_icon),
contentDescription = stringResource(R.string.tasks_org),
tint = Color.Unspecified,
modifier = Modifier.size(152.dp)
)
Spacer(modifier = Modifier.weight(1f))
WelcomeContent(
onSignIn = onSignIn,
onContinueWithoutSync = onContinueWithoutSync,
onImportBackup = onImportBackup,
openLegalUrl = openLegalUrl,
modifier = Modifier.fillMaxWidth(),
)
}
}
}
}
}
@Composable
private fun WelcomeContent(
onSignIn: () -> Unit,
onContinueWithoutSync: () -> Unit,
onImportBackup: () -> Unit,
openLegalUrl: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val buttonModifier = Modifier
.widthIn(max = 400.dp)
.fillMaxWidth()
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally,
) {
LegalDisclosure(
openLegalUrl = openLegalUrl,
modifier = Modifier.widthIn(max = 400.dp),
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = onSignIn,
modifier = buttonModifier
) {
Text(text = stringResource(R.string.add_account))
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = onContinueWithoutSync,
modifier = buttonModifier
) {
Text(text = stringResource(R.string.continue_without_sync))
}
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(R.string.returning_user),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
TextButton(onClick = onImportBackup) {
Text(text = stringResource(R.string.backup_BAc_import))
}
}
}
@Composable
internal fun LegalDisclosure(
openLegalUrl: (String) -> Unit,
modifier: Modifier = Modifier,
textAlign: TextAlign = TextAlign.Center,
) {
val bodyStyle = MaterialTheme.typography.bodySmall.copy(
textAlign = textAlign,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
val linkStyle = SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline,
)
val tosText = stringResource(R.string.terms_of_service_proper)
val privacyText = stringResource(R.string.privacy_policy_proper)
val licenseText = stringResource(R.string.gplv3_license)
val template = stringResource(R.string.legal_disclosure)
val formatted = String.format(template, tosText, privacyText, licenseText)
val tosStart = formatted.indexOf(tosText)
val tosEnd = tosStart + tosText.length
val privacyStart = formatted.indexOf(privacyText)
val privacyEnd = privacyStart + privacyText.length
val licenseStart = formatted.indexOf(licenseText)
val licenseEnd = licenseStart + licenseText.length
val tosUrl = stringResource(R.string.url_tos)
val privacyUrl = stringResource(R.string.url_privacy_policy)
val licenseUrl = stringResource(R.string.url_license)
val annotatedText = buildAnnotatedString {
append(formatted)
addStyle(linkStyle, tosStart, tosEnd)
addStringAnnotation(tag = "url", annotation = tosUrl, start = tosStart, end = tosEnd)
addStyle(linkStyle, privacyStart, privacyEnd)
addStringAnnotation(tag = "url", annotation = privacyUrl, start = privacyStart, end = privacyEnd)
addStyle(linkStyle, licenseStart, licenseEnd)
addStringAnnotation(tag = "url", annotation = licenseUrl, start = licenseStart, end = licenseEnd)
}
ClickableText(
text = annotatedText,
style = bodyStyle,
modifier = modifier,
onClick = { offset ->
annotatedText.getStringAnnotations(tag = "url", start = offset, end = offset)
.firstOrNull()
?.let { openLegalUrl(it.item) }
}
)
}
@PreviewLightDark
@PreviewScreenSizes
@PreviewFontScale
@Composable
fun WelcomeScreenPreview() {
TasksTheme {
WelcomeScreen(
onBack = {},
onSignIn = {},
onContinueWithoutSync = {},
onImportBackup = {},
openLegalUrl = {},
)
}
}

@ -18,16 +18,12 @@ 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
@ -36,7 +32,6 @@ 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
@ -55,41 +50,27 @@ 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()
}
}
BackHandler(onBack = onBack)
Scaffold(
topBar = {
TopAppBar(
colors = TopAppBarDefaults.topAppBarColors(),
navigationIcon = {
if (!gettingStarted) {
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
contentDescription = stringResource(R.string.back),
)
}
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)
}
)
Text(text = stringResource(R.string.add_account))
}
)
}
@ -107,21 +88,6 @@ fun AddAccountScreen(
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,
@ -130,28 +96,28 @@ fun AddAccountScreen(
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,
@ -159,29 +125,20 @@ fun AddAccountScreen(
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
)
}
}
}
}
@ -241,108 +198,18 @@ fun AccountTypeCard(
}
}
@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 = {},
)
}
}

@ -35,6 +35,7 @@ import org.tasks.gtasks.GoogleTaskSynchronizer
import org.tasks.injection.BaseWorker
import org.tasks.opentasks.OpenTasksSynchronizer
import org.tasks.preferences.Preferences
import org.tasks.preferences.TasksPreferences
import org.tasks.sync.microsoft.MicrosoftSynchronizer
import org.tasks.time.DateTimeUtils2.currentTimeMillis
import timber.log.Timber
@ -53,11 +54,13 @@ class SyncWork @AssistedInject constructor(
private val openTasksSynchronizer: Lazy<OpenTasksSynchronizer>,
private val microsoftSynchronizer: Lazy<MicrosoftSynchronizer>,
private val openTaskDao: OpenTaskDao,
private val inventory: Inventory
private val inventory: Inventory,
private val tasksPreferences: TasksPreferences,
) : BaseWorker(context, workerParams, firebase) {
override suspend fun run(): Result {
Timber.d("Starting...")
if (isBackground) {
ContextCompat.getSystemService(context, ConnectivityManager::class.java)?.apply {
if (restrictBackgroundStatus == ConnectivityManagerCompat.RESTRICT_BACKGROUND_STATUS_ENABLED) {
@ -95,9 +98,15 @@ class SyncWork @AssistedInject constructor(
private val syncStatus = R.string.p_sync_ongoing
private suspend fun hasTosAcceptance(): Boolean {
val currentTosVersion = firebase.getTosVersion()
val acceptedTosVersion = tasksPreferences.get(TasksPreferences.acceptedTosVersion, 0)
return acceptedTosVersion >= currentTosVersion
}
private suspend fun doSync() {
val hasNetworkConnectivity = context.hasNetworkConnectivity()
if (hasNetworkConnectivity) {
if (hasNetworkConnectivity && hasTosAcceptance()) {
googleTaskJobs().plus(caldavJobs()).awaitAll()
}
inventory.updateTasksAccount()

@ -84,6 +84,7 @@ class HelpAndFeedback : InjectingPreferenceFragment() {
openUrl(R.string.follow_reddit, R.string.url_reddit)
openUrl(R.string.follow_twitter, R.string.url_twitter)
openUrl(R.string.source_code, R.string.url_source_code)
openUrl(R.string.terms_of_service, R.string.url_tos)
openUrl(R.string.privacy_policy, R.string.url_privacy_policy)
}

@ -22,9 +22,12 @@ import org.tasks.auth.SignInActivity
import org.tasks.auth.SignInActivity.Platform
import org.tasks.billing.Inventory
import org.tasks.billing.Purchase
import org.tasks.analytics.Firebase
import org.tasks.data.entity.CaldavAccount
import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired
import org.tasks.data.entity.CaldavAccount.Companion.isTosRequired
import org.tasks.extensions.Context.openUri
import org.tasks.preferences.TasksPreferences
import org.tasks.extensions.Context.toast
import org.tasks.jobs.WorkManager
import org.tasks.kmp.org.tasks.time.DateStyle
@ -40,8 +43,11 @@ class TasksAccount : BaseAccountPreference() {
@Inject lateinit var inventory: Inventory
@Inject lateinit var localBroadcastManager: LocalBroadcastManager
@Inject lateinit var workManager: WorkManager
@Inject lateinit var firebase: Firebase
@Inject lateinit var tasksPreferences: TasksPreferences
private val viewModel: TasksAccountViewModel by viewModels()
private var tosDialogShown = false
private val refreshReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
@ -209,6 +215,11 @@ class TasksAccount : BaseAccountPreference() {
findPreference(R.string.local_lists).summary =
getString(R.string.migrate_count, quantityString)
}
if (account.isTosRequired() && !tosDialogShown) {
tosDialogShown = true
showTosDialog()
}
}
private suspend fun refreshPasswords(passwords: List<TasksAccountViewModel.AppPassword>) {
@ -250,6 +261,24 @@ class TasksAccount : BaseAccountPreference() {
)
}
private fun showTosDialog() {
dialogBuilder
.newDialog(R.string.tos_updated_title)
.setNeutralButton(R.string.view_tos) { _, _ ->
context?.openUri(R.string.url_tos)
}
.setPositiveButton(R.string.accept) { _, _ ->
lifecycleScope.launch {
val currentTosVersion = firebase.getTosVersion()
tasksPreferences.set(TasksPreferences.acceptedTosVersion, currentTosVersion)
caldavDao.update(account.copy(error = null))
workManager.sync(immediate = true)
}
}
.setNegativeButton(R.string.cancel, null)
.show()
}
companion object {
fun newTasksAccountPreference(account: CaldavAccount): Fragment {
val fragment = TasksAccount()

@ -675,7 +675,6 @@
</plurals>
<string name="now">اﻵن</string>
<string name="TEA_creation_date">تاريخ الإنشاء</string>
<string name="sign_in_to_tasks_disclosure">سيتم إرسال عنوان البريد الإلكتروني ومعرف الحساب الخاصان بك وتخزينهما من قبل Tasks.org. هذه المعلومات سيتم استخدامها من أجل المصادقة ومن أجل تزويدك بالإعلانات المهمة المتعلقة بالخدمة. لن يتم مشاركة هذه المعلومات مع أحد.</string>
<string name="default_reminder">تذكير تلقائي افتراضي</string>
<string name="rmd_time_description">أظهر الإشعارات من أجل المهام بدون أوقات محددة</string>
<string name="completed">مكتمل</string>
@ -807,6 +806,5 @@
<string name="enable_alarms_description">للتأكد من إخطارك في الوقت المناسب، امنح الإذن لضبط التنبيهات والتذكيرات في الإعدادات</string>
<string name="continue_without_sync">المتابعة بدون مزامنة</string>
<string name="delete_tasks_warning">%s سيتم حذفها. لا يمكن التراجع عن ذلك!</string>
<string name="help_me_choose">ساعدني في الاختيار</string>
<string name="widget_view_more_tasks">عرض المزيد من المهام</string>
</resources>

@ -406,7 +406,6 @@
<string name="date_and_time">Data y hora</string>
<string name="add_account">Amestar cuenta</string>
<string name="continue_without_sync">Continuar ensin sincronizar</string>
<string name="help_me_choose">Aidame a escoyer</string>
<string name="user">Usuariu</string>
<string name="password">Conseña</string>
<string name="url">URL</string>

@ -649,7 +649,6 @@
<string name="sort_completion_group">Завършена %s</string>
<string name="microsoft_selection_description">Синхронизирайте задачите си с личния профил в Microsoft</string>
<string name="sign_in">Вход</string>
<string name="sign_in_to_tasks_disclosure">Адресът на електронната поща и идентификаторът на профила ще бъдат изпратени и съхранени от Tasks.org. Информацията ще бъде използвана за удостоверяване и за предоставяне на важни съобщения, свързани с услугата. Тази информация няма да бъде споделяна с никого.</string>
<string name="consent_agree">Съгласявам се</string>
<string name="consent_deny">Не сега</string>
<string name="default_reminder">Подразбирано напомняне</string>
@ -719,6 +718,5 @@
<string name="multiline_title_off">Докоснете „Готово“ за запазване на задачата</string>
<string name="delete_tasks_warning">Задачата %s ще бъде премахната. Действието е необратимо!</string>
<string name="continue_without_sync">Без синхронизиране</string>
<string name="help_me_choose">Помощ в избора</string>
<string name="widget_view_more_tasks">Повече задачи</string>
</resources>

@ -508,7 +508,6 @@
<string name="delete_selected_tasks">Voleu suprimir les tasques seleccionades?</string>
<string name="add_account">Afegeiu un compte</string>
<string name="continue_without_sync">Continua sense sincronitzar</string>
<string name="help_me_choose">Ajuda\'m a triar</string>
<string name="bundle_notifications">Notificacions del paquet</string>
<string name="default_reminder">Recordatori predeterminat</string>
<string name="badges">Insígnies</string>
@ -668,7 +667,6 @@
<string name="above_average">Per sobre de la mitjana</string>
<string name="save_percent">Desa %d%%</string>
<string name="sign_in_to_tasks">Inicieu la sessió a Tasks.org</string>
<string name="sign_in_to_tasks_disclosure">La vostra adreça de correu electrònic i l\'ID del compte es transmetran i s\'emmagatzemaran a Tasks.org. Aquesta informació s\'utilitzarà per a l\'autenticació i per a proporcionar-vos anuncis relacionats amb serveis importants. Aquesta informació no es compartirà amb ningú.</string>
<string name="app_password">Contrasenya de l\'aplicació</string>
<string name="app_passwords">Contrasenyes d\'aplicacions</string>
<string name="app_passwords_more_info">Sincronitzeu les vostres tasques i calendaris amb l\'escriptori de tercers i les aplicacions mòbils. Fes clic aquí per a més informació</string>

@ -671,7 +671,6 @@
<string name="default_reminder">Výchozí upomínka</string>
<string name="rmd_time_description">Zobrazovat upozornění na úkoly bez termínů</string>
<string name="microsoft_selection_description">Synchronizovat se svým osobním účtem Microsoft</string>
<string name="sign_in_to_tasks_disclosure">Vaše e-mailová adresa a ID účtu budou přeneseny a uloženy na Tasks.org. Tyto informace budou použity pro ověření a pro poskytování důležitých oznámení souvisejících se službami. Tyto informace nebudou s nikým sdíleny.</string>
<string name="consent_agree">Souhlasím</string>
<string name="consent_deny">Teď ne</string>
<string name="sign_in">Přihlásit se</string>
@ -739,7 +738,6 @@
<string name="multiline_title_off">Stiskněte tlačítko Hotovo pro uložení úkolu</string>
<string name="banner_app_updated_title">Aplikace aktualizována</string>
<string name="banner_app_updated_description">Aplikace Tasks byla aktualizována na %s. Chcete zobrazit seznam změn?</string>
<string name="help_me_choose">Pomozte mi s výběrem</string>
<string name="delete_tasks_warning">Úkol %s bude odstraněn. Tato akce je nevratná!</string>
<string name="continue_without_sync">Pokračovat bez synchronizace</string>
<string name="widget_view_more_tasks">Zobrazit další úkoly</string>

@ -678,7 +678,6 @@
<string name="repeats_on"></string>
<string name="repeats_never">Aldrig</string>
<string name="sort_ascending">Stigende</string>
<string name="sign_in_to_tasks_disclosure">Din e-mailadresse og dit konto-ID overføres og gemmes af Tasks.org. Disse oplysninger bruges til godkendelse og til at give dig vigtige tjenesterelaterede meddelelser og deles ikke med nogen.</string>
<string name="theme_dynamic">Dynamisk</string>
<string name="subtasks_multilevel_microsoft">Underopgaver på flere niveauer understøttes ikke af Microsoft To Do</string>
<string name="donate_nag">Overvej at vise din støtte med en donation!</string>
@ -718,7 +717,6 @@
<string name="banner_app_updated_title">App opdateret</string>
<string name="multiline_title_off">Tryk på Færdig for at gemme opgave</string>
<string name="continue_without_sync">Fortsæt uden synkronisering</string>
<string name="help_me_choose">Hjælp mig med at vælge</string>
<string name="delete_tasks_warning">%s bliver slettet. Dette kan ikke fortrydes!</string>
<string name="widget_view_more_tasks">Vis flere opgaver</string>
</resources>

@ -650,7 +650,6 @@
<string name="enable_reminders_description">Erinnerungen sind in den Android-Einstellungen deaktiviert</string>
<string name="consent_agree">Zustimmen</string>
<string name="consent_deny">Nicht jetzt</string>
<string name="sign_in_to_tasks_disclosure">Deine E-Mail-Adresse und Account-ID werden an Tasks.org übertragen und dort gespeichert. Diese Informationen werden verwedet, um dich zu authentifizieren und über wichtige servicebezogene Informationen zu benachrichtigen. Deine Daten werden mit niemandem geteilt.</string>
<string name="microsoft_selection_description">Mit persönlichem Microsoft-Konto synchronisieren</string>
<string name="sign_in">Anmelden</string>
<string name="rmd_time_description">Benachrichtigungen für Aufgaben ohne Fälligkeitsdatum anzeigen</string>
@ -717,7 +716,6 @@
<string name="banner_app_updated_title">App aktualisiert</string>
<string name="banner_app_updated_description">Tasks wurde gerade auf %s aktualisiert. Möchtest du dir die Änderungsnotizen ansehen?</string>
<string name="multiline_title_off">Erledigt drücken, um Aufgabe zu speichern</string>
<string name="help_me_choose">Hilf mir bei der Auswahl</string>
<string name="continue_without_sync">Weiter ohne Synchronisierung</string>
<string name="delete_tasks_warning">%s wird/werden gelöscht. Dies kann nicht rückgängig gemacht werden!</string>
<string name="widget_view_more_tasks">Mehr Aufgaben anzeigen</string>

@ -337,7 +337,6 @@
<string name="date_and_time">Ημερομηνία και ώρα</string>
<string name="add_account">Προσθήκη λογαριασμού</string>
<string name="continue_without_sync">Συνέχεια χωρίς συγχρονισμό</string>
<string name="help_me_choose">Βοήθησε με να διαλέξω</string>
<string name="user">Χρήστης</string>
<string name="password">Κωδικός</string>
<string name="url">Ηλεκτρονική διεύθυνση (URL)</string>
@ -570,7 +569,6 @@
<string name="above_average">Άνω του μέσου όρου</string>
<string name="save_percent">Αποθήκευση %d%%</string>
<string name="sign_in_to_tasks">Συνδεθείτε στο Tasks.org</string>
<string name="sign_in_to_tasks_disclosure">Η διεύθυνση ηλεκτρονικού ταχυδρομείου και το αναγνωριστικό του λογαριασμού σας θα μεταδοθούν και θα αποθηκευτούν από το Tasks.org. Οι πληροφορίες αυτές θα χρησιμοποιηθούν για την εξακρίβωση της ταυτότητας και για την παροχή σημαντικών ανακοινώσεων σχετικά με την υπηρεσία. Αυτές οι πληροφορίες δεν θα κοινοποιηθούν σε κανέναν.</string>
<string name="app_password">Κωδικός εφαρμογής</string>
<string name="app_passwords">Κωδικοί εφαρμογής</string>
<string name="app_passwords_more_info">Συγχρονίστε τις εργασίες και τα ημερολόγια σας με εφαρμογές ηλεκτρονικών υπολογιστών και κινητών συσκευών τρίτων. Πατήστε εδώ για περισσότερες πληροφορίες</string>

@ -655,7 +655,6 @@
<string name="etesync_account_description">Bezonas konton de EteSync.com aŭ memgastigan servilon</string>
<string name="wearable_notifications">Sciigoj per portebla aparataro</string>
<string name="android_auto_backup_device_summary">Vi devas ankaŭ doni permeson al savkopia servo per viaj aparataj agordoj. Ne ĉiuj aparatoj provizas savkopian servon.</string>
<string name="sign_in_to_tasks_disclosure">Via retpoŝtadreso kaj konta ID estos transsendita kaj konservita de Tasks.org. Ĉi tiu informo estos uzata por aŭtentigi kaj por provizi al vi gravajn anoncojn rilate al servo. Ĉi tiu informo ne estos diskongita al iu ajn.</string>
<string name="repeats_every">Ripetiĝas ĉiu</string>
<string name="badges_description">Vidigi nombro da taskoj sur lanĉpiktogramo de Tasks. Ne ĉiuj lanĉiloj subtenas ĉi tiajn simbolojn.</string>
<string name="caldav_account_description">Bezonas konton de CalDAV servprovizanto aŭ memgastigan servilon. Por trovi servprovizanton vizitu task.org/caldav</string>
@ -719,6 +718,5 @@
<string name="banner_app_updated_description">Tasks estis ĝisdatigita al %s. Ĉu vi deziras legi la eldonajn notojn?</string>
<string name="delete_tasks_warning">Forigas %s. Ne eblas malfari tion!</string>
<string name="continue_without_sync">Daŭrigi sen sinkronigo</string>
<string name="help_me_choose">Helpu min elekti</string>
<string name="widget_view_more_tasks">Vidi pli da taskoj</string>
</resources>

@ -670,7 +670,6 @@
<string name="TEA_creation_date">Fecha de creación</string>
<string name="microsoft_selection_description">Sincronizar con su cuenta personal de Microsoft</string>
<string name="sign_in">Iniciar sesión</string>
<string name="sign_in_to_tasks_disclosure">Tu dirección de correo electrónico y el ID de tu cuenta serán transmitidos y almacenados por Tasks.org. Esta información se utilizará para la autenticación y para proporcionarle anuncios importantes relacionados con el servicio. Esta información no se compartirá con nadie.</string>
<string name="consent_agree">De acuerdo</string>
<string name="consent_deny">No ahora</string>
<string name="default_reminder">Aviso por defecto</string>
@ -740,7 +739,6 @@
<string name="multiline_title_off">Presione en Hecho para guardar la tarea</string>
<string name="comment">Comentario</string>
<string name="continue_without_sync">Continua sin sincronizar</string>
<string name="help_me_choose">Ayudame a escoger</string>
<string name="delete_tasks_warning">%s será eliminado. ¡Esto no se puede deshacer!</string>
<string name="widget_view_more_tasks">Ver más tareas</string>
</resources>

@ -652,7 +652,6 @@
<string name="migrate_count">Teisalda %s teenusesse Tasks.org</string>
<string name="sort_completion_group">Lõpetatud %s</string>
<string name="auto_dismiss_datetime_list">Ülesannete loend</string>
<string name="sign_in_to_tasks_disclosure">Sinu e-posti aadress ja kasutajatunnus saadetakse teenusele Tasks.org ja salvestuvad seal. Neid kasutatakse autentimiseks ja teenusega seotud oluliste teavituste jaoks, kuid ei jagata kolmandate osapooltega.</string>
<string name="google_tasks_selection_description">Google\'i kasutajakontoga lihtsustatud andmete sünkroniseerimise võimalus</string>
<string name="upgrade_blurb_1">Tere! Minu nimi on Alex ja ma olen sõltumatu arendaja, kes on Taski arendust juhtinud</string>
<string name="chips">Infosildid</string>
@ -718,7 +717,6 @@
<string name="banner_app_updated_title">Rakendus on uuendatud</string>
<string name="multiline_title_off">Ülesande salvestamiseks vajuta nuppu „Valmis“</string>
<string name="continue_without_sync">Jätka ilma sünkroniseerimata</string>
<string name="help_me_choose">Aita mul valida</string>
<string name="delete_tasks_warning">%s saab olema kustutatud. Seda tegevust ei saa tagasi pöörata!</string>
<string name="widget_view_more_tasks">Vaata veel ülesandeid</string>
</resources>

@ -652,7 +652,6 @@
<string name="enable_reminders">Aktibatu oroigarriak</string>
<string name="sort_completion_group">Osatu %s</string>
<string name="TEA_creation_date">Sortze-data</string>
<string name="sign_in_to_tasks_disclosure">Zure helbide elektronikoa eta kontuaren IDa Tasks.org-era transmititu eta biltegiratuko da. Informazio hau autentifikazio eta zerbitzuari erlazionatutako iragarki garrantzitsuak emateko erabiliko da. Informazio hau ez da inorekin partekatuko.</string>
<string name="consent_agree">Onartu</string>
<string name="consent_deny">Orain ez</string>
<string name="microsoft_selection_description">Sinkronizatu zure Microsoft kontu pertsonalarekin</string>

@ -684,7 +684,6 @@
<string name="show_edit_screen_without_unlock">Näytä muokkaa näyttöä ilman lukituksen avaamista</string>
<string name="theme_dynamic">Dynaaminen</string>
<string name="donate_nag">Harkitse tuen osoittamista lahjoituksella!</string>
<string name="sign_in_to_tasks_disclosure">Sähköpostiosoitteesi ja käyttäjä-ID:si lähetetään säilytettäväksi Tasks.org:iin. Näitä tietoja käytetään tunnistautumiseen ja tärkeiden palveluun liittyvien ilmoitusten toimittamiseen. Tietoja ei jaeta kenenkään kanssa.</string>
<string name="show_edit_screen_without_unlock_summary">Aktivoi pika-asetusten käyttö ilman laitteen lukituksen avaamista</string>
<string name="sort_not_available">Ei käytettävissä tunnisteiden, suodattimien tai paikkojen kanssa</string>
<string name="customize_drawer">Muokkaa vetolaatikkoa</string>
@ -700,7 +699,6 @@
<string name="comment">Kommentti</string>
<string name="yesterday">Eilen</string>
<string name="continue_without_sync">Jatka ilman synkronointia</string>
<string name="help_me_choose">Auta minua valitsemaan</string>
<string name="delete_tasks_warning">%s poistetaan. Tätä ei voi perua!</string>
<string name="banner_app_updated_title">Sovellus päivitetty</string>
<string name="banner_app_updated_description">Sovellus päivitettiin juuri %s. Haluatko lukea julkaisutiedot?</string>

@ -671,7 +671,6 @@
<string name="microsoft_selection_description">Synchroniser avec votre compte « Microsoft » personnel</string>
<string name="sign_in">Se connecter</string>
<string name="consent_deny">Pas maintenant</string>
<string name="sign_in_to_tasks_disclosure">Votre adresse électronique et votre identifiant de compte seront transmis et stockés par « Tasks.org ». Ces informations seront utilisées pour l\'authentification et pour vous fournir des annonces importantes relatives au service. Ces informations ne seront partagées avec personne.</string>
<string name="consent_agree">Accepter</string>
<string name="default_reminder">Rappel par défaut</string>
<string name="rmd_time_description">Afficher les notifications pour les tâches dont les heures d\'échéances ne sont pas renseignées</string>
@ -739,7 +738,6 @@
<string name="banner_app_updated_description">Tasks vient d\'être mis à jour vers %s. Souhaitez-vous consulter les notes de mise à jour?</string>
<string name="banner_app_updated_title">Application mise à jour</string>
<string name="continue_without_sync">Continuer sans synchronisation</string>
<string name="help_me_choose">Aidez-moi à choisir</string>
<string name="delete_tasks_warning">%s sera supprimé. Cette opération est irréversible!</string>
<string name="multiline_title_off">Appuyer sur Terminé pour enregistrer la tâche</string>
<string name="widget_view_more_tasks">Voir plus de tâches</string>

@ -562,7 +562,6 @@
<string name="show_edit_screen_without_unlock_summary">Activa o uso do mosaico Configuración rápida sen desbloquear o dispositivo</string>
<string name="theme_dynamic">Dinámico</string>
<string name="continue_without_sync">Continuar sen sincronización</string>
<string name="help_me_choose">Axúdame a escoller</string>
<string name="default_reminder">Recordatorio predeterminado</string>
<string name="repeats_custom_recurrence">Recorrencia personalizada</string>
<string name="repeats_every">Repítese cada</string>
@ -622,7 +621,6 @@
<string name="google_play_subscribers">Subscritores de Google Play</string>
<string name="github_sponsors">Patrocinadores de GitHub</string>
<string name="github_sponsor">Patrocinador</string>
<string name="sign_in_to_tasks_disclosure">O teu enderezo de correo electrónico e o ID da túa conta serán transmitidos e almacenados por Tasks.org. Esta información usarase para a autenticación e para proporcionarche anuncios importantes relacionados co servizo. Esta información non se compartirá con ninguén.</string>
<string name="app_password_delete_confirmation">Calquera aplicación que use este contrasinal pechará sesión</string>
<string name="app_password_save">Usa estas credenciais para configurar unha aplicación de terceiros. Conceden acceso completo á túa conta de Tasks.org, non as escribas nin as compartas con ninguén!</string>
<string name="account_not_included">Non incluído nas subscricións de \'Prezo flexible\'</string>

@ -681,7 +681,6 @@
<string name="subtasks_multilevel_microsoft">תתי־משימות מקוננות אינן נתמכות על ידי Microsoft To Do</string>
<string name="sort_completion_group">השלמה %s</string>
<string name="custom_filter_has_reminder">יש תזכורת</string>
<string name="sign_in_to_tasks_disclosure">כתובת הדוא\"ל ומזהה החשבון שלך ישודרו ויאוחסנו על ידי Tasks.org. מידע זה ישמש עבור אימות וכדי לספק לך הכרזות חשובות הקשורות לשירות. מידע זה לא ישותף עם אף אחד.</string>
<string name="enable_reminders_description">תזכורות מושבתות בהגדרות Android</string>
<string name="cost_free">עלות: חינם</string>
<string name="multiline_title_on">יש ללחוץ על מקש Enter כדי להוסיף מעברי שורה</string>
@ -742,7 +741,6 @@
<string name="repeats_after">לאחר</string>
<string name="comment">הערה</string>
<string name="continue_without_sync">להמשיך ללא סנכרון</string>
<string name="help_me_choose">עזור לי לבחור</string>
<string name="delete_tasks_warning">%s יימחק. לא ניתן לבטל זאת!</string>
<string name="widget_view_more_tasks">הצג עוד משימות</string>
</resources>

@ -670,7 +670,6 @@
<string name="TEA_creation_date">Datum stvaranja</string>
<string name="microsoft_selection_description">Sinkroniziraj s tvojim osobnim Microsoft računom</string>
<string name="sign_in">Prijavi se</string>
<string name="sign_in_to_tasks_disclosure">Tvoja e-mail adresa i ID računa će se poslati i spremiti na Tasks.org . Ovi će se podaci koristiti za provjeru autentifikacije i za slanje važnih najava vezanih uz uslugu. Ovi podaci se neće dijeliti ni s kim.</string>
<string name="consent_agree">Prihvati</string>
<string name="consent_deny">Ne sada</string>
<string name="default_reminder">Standardni podsjetitelj</string>
@ -737,7 +736,6 @@
<string name="comment">Komentar</string>
<string name="app_settings">Postavke aplikacije</string>
<string name="continue_without_sync">Nastavi bez sinkronizacije</string>
<string name="help_me_choose">Pomogni mi odabrati</string>
<string name="delete_tasks_warning">%s će se izbrisati. Ovo se ne može opozvati!</string>
<string name="banner_app_updated_title">Aplikacija je aktualizirana</string>
<string name="banner_app_updated_description">Aplikacija Tasks je upravo aktualizirana na %s. Želiš li vidjeti bilješke o izdanju?</string>

@ -652,7 +652,6 @@
<string name="sign_in">Bejelentkezés</string>
<string name="consent_agree">Beleegyezés</string>
<string name="consent_deny">Most nem</string>
<string name="sign_in_to_tasks_disclosure">Az Ön e-mail címét és azonodítóját a Tasks.org tárolja. Ezek az információk azonosításra és fontos, a szolgáltatással kapcsolatos információk eljuttatásának céljából kerülnek felhasználásra. Ez az információ senkivel sem kerül megosztásra.</string>
<string name="enable_reminders">Emlékeztetők engedélyezése</string>
<string name="sort_list">Lista szerint</string>
<string name="sort_sorting">Sorbarendezés</string>
@ -718,7 +717,6 @@
<string name="banner_app_updated_description">A Tasks frissítve lett a(z) %s verzióra. Szeretné látni a változások listáját?</string>
<string name="delete_tasks_warning">A(z) %s törlésre kerül. Ez a művelet nem visszavonható!</string>
<string name="continue_without_sync">Folytatás szinkronizálás nélkül</string>
<string name="help_me_choose">Segítséget kérek a választásban</string>
<string name="multiline_title_off">Kattints a Kész gombra a feladat elmentéséhez</string>
<string name="widget_view_more_tasks">További feladatok megtekintése</string>
</resources>

@ -671,7 +671,6 @@
<string name="microsoft_selection_description">Sincronizzazione con l\'account personale Microsoft</string>
<string name="sign_in">Accedi</string>
<string name="consent_agree">Accetto</string>
<string name="sign_in_to_tasks_disclosure">Il tuo indirizzo e-mail e l\'ID dell\'account saranno trasmessi e memorizzati da Tasks.org. Queste informazioni saranno utilizzate per l\'autenticazione e per inviarti annunci importanti relativi al servizio. Queste informazioni non saranno condivise con nessuno.</string>
<string name="consent_deny">Non ora</string>
<string name="default_reminder">Promemoria predefinito</string>
<string name="rmd_time_description">Mostra le notifiche per le attività senza scadenza</string>
@ -740,7 +739,6 @@
<string name="banner_app_updated_description">Tasks è stata aggiornata a %s. Vuoi vedere le note di rilascio?</string>
<string name="multiline_title_off">Premi il tasto di conferma per salvare l\'attività</string>
<string name="continue_without_sync">Continua senza sincronizzazione</string>
<string name="help_me_choose">Aiutami a scegliere</string>
<string name="delete_tasks_warning">%s verrà cancellato. L\'operazione non può essere annullata!</string>
<string name="widget_view_more_tasks">Visualizza ulteriori attività</string>
</resources>

@ -538,7 +538,6 @@
<string name="app_password">アプリパスワード</string>
<string name="invite_declined">招待を辞退</string>
<string name="upgrade_more_customization">さらなるカスタマイズ</string>
<string name="sign_in_to_tasks_disclosure">あなたのメールアドレスとアカウントID は、Tasks.org によって送信および保存されます。この情報は、認証とサービス関連の重要なお知らせを提供するために使用され、誰とも共有されることはありません。</string>
<string name="consent_agree">同意する</string>
<string name="consent_deny">あとで</string>
<string name="customize_edit_screen_summary">フィールドの並び替えや削除</string>
@ -706,7 +705,6 @@
<string name="multiline_title_off">タスクを保存するには「完了」を押してください</string>
<string name="cost_free">料金: 無料</string>
<string name="continue_without_sync">同期せずに続ける</string>
<string name="help_me_choose">選択を手伝ってください</string>
<string name="delete_tasks_warning">%sは削除されます。これは元に戻せません</string>
<string name="cost_more_money">料金: $$$</string>
<string name="widget_view_more_tasks">ほかのタスクを表示</string>

@ -611,7 +611,6 @@
<string name="microsoft_selection_description">마이크로소프트 계정과 동기화</string>
<string name="enable_reminders">알람 활성화</string>
<string name="hint_customize_edit_body">필드 순서를 변경하거나 삭제하실 수 있습니다</string>
<string name="sign_in_to_tasks_disclosure">이메일 주소와 계정 ID가 Tasks.org으로 전송되어 보관됩니다. 이 정보는 계정 인증과 서비스 관련 중요 공지를 전달하기 위해 쓰이며, 다른 누구와도 공유하지 않습니다.</string>
<string name="completion_sound">완료 효과음 재생</string>
<string name="alarm_before_start">시작 %s 전</string>
<string name="alarm_before_due">마감 %s 전</string>
@ -705,7 +704,6 @@
<string name="delete_comment">코멘트</string>
<string name="comment">코멘트</string>
<string name="continue_without_sync">동기화 없이 계속하기</string>
<string name="help_me_choose">고르는 것을 도와주세요</string>
<string name="delete_tasks_warning">%s 를 삭제합니다. 삭제 후 되돌릴 수 없습니다!</string>
<string name="banner_app_updated_title">앱이 업데이트됨</string>
<string name="banner_app_updated_description">Tasks.org가 방금 %s 버전으로 업데이트되었습니다. 릴리즈 노트를 열까요?</string>

@ -640,7 +640,6 @@
<string name="default_reminder">Numatytas priminimas</string>
<string name="randomly_every">Atsitiktinai kas %s</string>
<string name="completed">Įvykdyta</string>
<string name="sign_in_to_tasks_disclosure">Jūsų el. pašto adresas ir paskyros ID bus perduodami ir saugomi Tasks.org. Ši informacija bus naudojama autentiškumui patvirtinti ir svarbiems su paslaugomis susijusiems pranešimams teikti. Ši informacija nebus niekam perduodama.</string>
<string name="dismiss">Atsisakyti</string>
<string name="consent_agree">Sutinku</string>
<string name="consent_deny">Ne dabar</string>

@ -664,7 +664,6 @@
<string name="rmd_time_description">Vis varslinger for oppgaver uten frister</string>
<string name="donate_today">Donér i dag</string>
<string name="donate_maybe_later">Kanskje senere</string>
<string name="sign_in_to_tasks_disclosure">Din e-postadresse og konto-ID sendes til og lagres av Tasks.org. Denne informasjonen benyttes for autentisering og for å kunne tilby deg viktige tjenesterelaterte kunngjøringer. Denne informasjonen deles ikke med noen.</string>
<plurals name="repeat_occurrence">
<item quantity="one">hendelse</item>
<item quantity="other">hendelser</item>

@ -651,7 +651,6 @@
<string name="sign_in">Log in</string>
<string name="consent_agree">Akkoord</string>
<string name="consent_deny">Niet nu</string>
<string name="sign_in_to_tasks_disclosure">Uw e-mailadres en account-ID worden verzonden naar Tasks.org en opgeslagen. Deze informatie wordt gebruikt voor authenticatie en om u te voorzien van belangrijke servicegerelateerde aankondigingen. Deze informatie wordt met niemand gedeeld.</string>
<string name="default_reminder">Standaardherinnering</string>
<string name="rmd_time_description">Toon meldingen voor taken zonder vervaltijd</string>
<string name="sort_list">Op lijstvolgorde</string>
@ -718,7 +717,6 @@
<string name="banner_app_updated_description">Tasks is zojuist geüpdatet naar %s. Wil je de release-opmerkingen zien?</string>
<string name="multiline_title_off">Druk op Gereed om de taak op te slaan</string>
<string name="continue_without_sync">Doorgaan zonder synchronisatie</string>
<string name="help_me_choose">Help me kiezen</string>
<string name="delete_tasks_warning">%s wordt verwijderd. Dit kan niet ongedaan worden gemaakt!</string>
<string name="widget_view_more_tasks">Meer taken zien</string>
</resources>

@ -675,7 +675,6 @@
<string name="enable_reminders_description">Przypomnienia są wyłączone w Ustawieniach Androida</string>
<string name="rmd_time_description">Wyświetlanie powiadomień o zadaniach bez terminów do wykonania</string>
<string name="microsoft_selection_description">Synchronizuj z osobistym kontem Microsoft</string>
<string name="sign_in_to_tasks_disclosure">Twój adres e-mail i identyfikator konta będą przekazywane i przechowywane przez Tasks.org. Informacje te będą wykorzystywane do uwierzytelniania oraz do przekazywania Ci ważnych ogłoszeń związanych z naszymi usługami. Informacje te nie będą nikomu udostępniane.</string>
<string name="consent_agree">Zgadzam się</string>
<string name="consent_deny">Nie teraz</string>
<string name="default_reminder">Domyślne przypomnienie</string>
@ -717,5 +716,4 @@
<string name="change_priority">Zmień priorytet</string>
<string name="theme_dynamic">Dynamiczny</string>
<string name="continue_without_sync">Kontynuuj bez synchronizacji</string>
<string name="help_me_choose">Pomóż mi wybrać</string>
</resources>

@ -668,7 +668,6 @@
<string name="rmd_time_description">Mostrar notificações de tarefas sem prazo</string>
<string name="consent_agree">Concordar</string>
<string name="consent_deny">Agora não</string>
<string name="sign_in_to_tasks_disclosure">Seu endereço de e-mail e seu ID da conta serão transmitidos e armazenados pelo Tasks.org. Estas informações serão usadas para autenticação e para fornecer-lhe anúncios importantes relacionados ao serviço. Estas informações não serão compartilhadas com ninguém.</string>
<string name="enable_reminders">Ativar lembretes</string>
<string name="microsoft_selection_description">Sincronize com sua conta pessoal da Microsoft</string>
<string name="enable_reminders_description">Os lembretes estão desabilitados nas configurações do Android</string>
@ -740,6 +739,5 @@
<string name="banner_app_updated_description">As tarefas acabaram de ser atualizadas para %s. Gostaria de ver as notas de atualização?</string>
<string name="multiline_title_off">Pressione \'Concluir\' para salvar a tarefa</string>
<string name="continue_without_sync">Continuar sem sincronizar</string>
<string name="help_me_choose">Ajude-me a escolher</string>
<string name="delete_tasks_warning">%s será excluído. Isso não pode ser desfeito!</string>
</resources>

@ -648,7 +648,6 @@
<string name="enable_reminders_description">Os lembretes estão desativados nas configurações do Android</string>
<string name="sort_completion_group">Concluído %s</string>
<string name="TEA_creation_date">Data de criação</string>
<string name="sign_in_to_tasks_disclosure">O seu endereço de e-mail e ID de conta serão transmitidos e armazenados pelo Tasks.org. Esta informação será utilizada para autenticação e para lhe fornecer anúncios importantes relacionados com serviços. Esta informação não será partilhada com ninguém.</string>
<string name="consent_agree">Concordo</string>
<string name="consent_deny">Agora não</string>
<string name="microsoft_selection_description">Sincronize com a sua conta pessoal Microsoft</string>
@ -719,7 +718,6 @@
<string name="add_shortcut_to_home_screen">Adicionar atalho para a tela inicial</string>
<string name="cost_free">Custo: Grátis</string>
<string name="send_application_logs">Enviar logs da aplicação</string>
<string name="help_me_choose">Ajude-me a escolher</string>
<string name="continue_without_sync">Continuar sem sync</string>
<string name="delete_tasks_warning">%s será apagado. Isso não poderá ser desfeito!</string>
<string name="widget_view_more_tasks">Ver mais tarefas</string>

@ -675,7 +675,6 @@
<string name="rmd_time_description">Afișează notificări pentru sarcinile fără termene limită</string>
<string name="consent_agree">De acord</string>
<string name="consent_deny">Nu acum</string>
<string name="sign_in_to_tasks_disclosure">Adresa de e-mail și ID-ul contului vor fi transmise și stocate de Tasks.org. Aceste informații vor fi utilizate pentru autentificare și pentru a furniza anunțuri importante legate de servicii. Aceste informații nu vor fi partajate cu nimeni.</string>
<string name="microsoft_selection_description">Sincronizarea cu contul personal Microsoft</string>
<string name="sign_in">Conectează-te</string>
<string name="sort_ascending">Crescător</string>
@ -743,7 +742,6 @@
<string name="enable_alarms_description">Pentru a te asigura că ești notificat la momentul potrivit, acordă permisiunea de a seta alarme și mementouri în Setări</string>
<string name="multiline_title_off">Apasă Finalizat pentru a salva sarcina</string>
<string name="continue_without_sync">Continuă fără sincronizare</string>
<string name="help_me_choose">Ajută-mă să aleg</string>
<string name="delete_tasks_warning">%s va fi șters. Acest lucru nu poate fi anulat!</string>
<string name="banner_app_updated_title">Aplicație actualizată</string>
<string name="banner_app_updated_description">Tasks a fost actualizată la versiunea %s. Dorești să vezi modificările făcute?</string>

@ -673,7 +673,6 @@
<string name="enable_reminders">Включить напоминания</string>
<string name="enable_reminders_description">Напоминания отключены в настройках Android</string>
<string name="TEA_creation_date">Дата создания</string>
<string name="sign_in_to_tasks_disclosure">Ваш адрес электронной почты и идентификатор учетной записи будут передаваться и храниться на сайте Tasks.org. Эта информация будет использоваться для аутентификации и предоставления вам важных объявлений, связанных с сервисом. Эта информация не будет передана никому.</string>
<string name="consent_agree">Соглашаюсь</string>
<string name="consent_deny">Не сейчас</string>
<string name="sort_completion_group">Готовность %s</string>
@ -747,7 +746,6 @@
<string name="banner_app_updated_title">Приложение обновлено</string>
<string name="multiline_title_off">Нажмите Done, чтобы сохранить задачу</string>
<string name="continue_without_sync">Продолжить без синхронизации</string>
<string name="help_me_choose">Помогите мне выбрать</string>
<string name="delete_tasks_warning">%s будет удалена. Это нельзя отменить!</string>
<string name="widget_view_more_tasks">Просмотреть больше задач</string>
</resources>

@ -692,7 +692,6 @@
<string name="app_password">Heslo aplikácie</string>
<string name="google_play_subscribers">Predplatitelia Google Play</string>
<string name="sign_in_with_google">Prihlásenie účtom Google</string>
<string name="sign_in_to_tasks_disclosure">Vaša e-mailová adresa a ID účtu bude prenesená a uložená do Tasks.org. Tieto informácie sa použijú na overenie a pre poskytovanie dôležitých oznámení súvisiacich so službami. Tieto informácie nebudú s nikým zdieľané.</string>
<string name="app_passwords">Heslá aplikácie</string>
<string name="app_passwords_more_info">Synchronizujte svoje úlohy a kalendáre s desktopovými a mobilnými aplikáciami tretích strán. Pre viac informácii kliknite sem</string>
<string name="invite_invalid">Neplatná pozvánka</string>

@ -366,7 +366,6 @@
<string name="date_and_time">Време и датум</string>
<string name="add_account">Додај налог</string>
<string name="continue_without_sync">Настави без синхронизације</string>
<string name="help_me_choose">Помоћ при одабиру</string>
<string name="user">Корисник</string>
<string name="password">Шифра</string>
<string name="url">URL</string>
@ -664,7 +663,6 @@
<string name="above_average">Изнад просека</string>
<string name="save_percent">Усними %d%%</string>
<string name="sign_in_to_tasks">Пријави се на Tasks.org</string>
<string name="sign_in_to_tasks_disclosure">Мејл адреса и идентификациони број ће бити послати и складиштени од стране Tasks.org. Ове информације ће се користити за аутентикацију и обезбеђење важних обавештења у вези сервиса. Ове информације се неће делити ни са ким.</string>
<string name="app_password">Шифра апликације</string>
<string name="app_passwords">Шифре апликације</string>
<string name="app_passwords_more_info">Синхронизуј задатке и календар са десктоп и мобилним апликацијама треће стране. Тапни овде за додатне информације</string>

@ -643,7 +643,6 @@
<string name="hint_customize_edit_body">Du kan anpassa den här skärmen genom att ordna om eller ta bort fält</string>
<string name="default_reminder">Standardpåminnelse</string>
<string name="rmd_time_description">Visa aviseringar för uppgifter utan deadlinetid</string>
<string name="sign_in_to_tasks_disclosure">Din e-postadress och konto-id kommer föras över till och lagras av Tasks.org. Uppgifterna kommer användas för autentisering och för att förse dig med viktiga tjänsterelaterade meddelanden. Informationen kommer inte delas med någon annan.</string>
<string name="consent_agree">Samtyck</string>
<string name="consent_deny">Inte nu</string>
<string name="enable_reminders_description">Påminnelser är avstängda i Android-inställningarna</string>
@ -717,7 +716,6 @@
<string name="banner_app_updated_title">App uppdaterad</string>
<string name="banner_app_updated_description">Tasks uppdaterades just till %s. Vill du se versionsnyheterna?</string>
<string name="multiline_title_off">Tryck på \"klar\" för att spara uppgiften</string>
<string name="help_me_choose">Hjälp mig att välja</string>
<string name="delete_tasks_warning">%s kommer att tas bort. Detta kan inte ångras!</string>
<string name="continue_without_sync">Fortsätt utan synkronisering</string>
<string name="widget_view_more_tasks">Visa mer uppgifter</string>

@ -689,7 +689,6 @@
<string name="sign_in_with_google">Google உடன் உள்நுழைக</string>
<string name="migrate_count">%s ஐ tasks.org க்கு நகர்த்தவும்</string>
<string name="save_percent">%d %% சேமிக்கவும்</string>
<string name="sign_in_to_tasks_disclosure">உங்கள் மின்னஞ்சல் முகவரி மற்றும் கணக்கு ஐடி TASKS.org ஆல் அனுப்பப்பட்டு சேமிக்கப்படும். இந்த செய்தி அங்கீகாரத்திற்காகவும், முக்கியமான பணி தொடர்பான அறிவிப்புகளை உங்களுக்கு வழங்கவும் பயன்படுத்தப்படும். இந்த செய்தி யாருடனும் பகிரப்படாது.</string>
<string name="generate_new_password">புதிய கடவுச்சொல்லை உருவாக்குங்கள்</string>
<string name="app_password_enter_description">உங்கள் கடவுச்சொல்லை ஒரு பெயரைக் கொடுங்கள் (விரும்பினால்)</string>
<string name="app_password_created_at">உருவாக்கப்பட்டது: %s</string>
@ -718,6 +717,5 @@
<string name="multiline_title_off">பணியைச் சேமிக்க முடிந்தது என்பதை அழுத்தவும்</string>
<string name="banner_app_updated_description">பணிகள் இப்போதுதான் %s ஆகப் புதுப்பிக்கப்பட்டது. வெளியீட்டுக் குறிப்புகளைப் பார்க்க விரும்புகிறீர்களா?</string>
<string name="continue_without_sync">ஒத்திசைவு இல்லாமல் தொடரவும்</string>
<string name="help_me_choose">தேர்வு செய்ய எனக்கு உதவு</string>
<string name="delete_tasks_warning">%s நீக்கப்படும். இதை செயல்தவிர்க்க முடியாது!</string>
</resources>

@ -651,7 +651,6 @@
<string name="sign_in">Oturum aç</string>
<string name="consent_agree">Kabul et</string>
<string name="consent_deny">Şimdi değil</string>
<string name="sign_in_to_tasks_disclosure">E-posta adresiniz ve hesap kimliğiniz Tasks.org tarafından iletilecek ve saklanacaktır. Bu bilgiler kimlik doğrulama ve hizmetle ilgili önemli duyuruları size sağlamak için kullanılacaktır. Bu bilgiler hiç kimse ile paylaşılmayacaktır.</string>
<string name="default_reminder">Öntanımlı anımsatıcı</string>
<string name="rmd_time_description">Bitiş zamanı olmayan görevler için bildirimleri göster</string>
<string name="sort_list">Listeye göre</string>
@ -719,6 +718,5 @@
<string name="multiline_title_off">Tamamlandıya basarak görevi kaydet</string>
<string name="delete_tasks_warning">%s silinecek. Bu geri alınamaz!</string>
<string name="continue_without_sync">Eşzamanlamadan sürdür</string>
<string name="help_me_choose">Seçmeme yardım et</string>
<string name="widget_view_more_tasks">Daha çok görev gör</string>
</resources>

@ -678,7 +678,6 @@
<string name="microsoft_selection_description">Синхронізація з особистим обліковим записом Microsoft</string>
<string name="sign_in">Увійти</string>
<string name="consent_deny">Не зараз</string>
<string name="sign_in_to_tasks_disclosure">Ваша адреса електронної пошти та ID облікового запису будуть передані та збережені Tasks.org. Ці дані використовуватимуться для автентифікації та надання вам важливих оголошень, пов\'язаних з обслуговуванням. Ці дані нікому не будуть передані.</string>
<string name="consent_agree">Погоджуюся</string>
<string name="rmd_time_description">Показувати сповіщення про завдання без запланованого часу</string>
<string name="default_reminder">Типове нагадування</string>
@ -748,7 +747,6 @@
<string name="banner_app_updated_description">Tasks щойно оновлено до %s. Переглянете примітки до випуску?</string>
<string name="multiline_title_off">Натисніть готово, щоб зберегти завдання</string>
<string name="continue_without_sync">Продовжити без синхронізації</string>
<string name="help_me_choose">Допоможіть обрати</string>
<string name="delete_tasks_warning">%s буде видалено. Дію неможливо скасувати!</string>
<string name="widget_view_more_tasks">Переглянути інші завдання</string>
</resources>

@ -634,7 +634,6 @@
<string name="default_reminder">Lời nhắc mặc định</string>
<string name="rmd_time_description">Hiện thông báo cho công việc không có hạn chót</string>
<string name="microsoft_selection_description">Đồng bộ với tài khoản Microsoft cá nhân</string>
<string name="sign_in_to_tasks_disclosure">Địa chỉ email và ID tài khoản của bạn sẽ được Tasks.org truyền đi và giữ lại. Thông tin này sẽ được sử dụng để xác thực và cung cấp các thông báo quan trọng liên quan đến dịch vụ. Thông tin này sẽ không được chia sẻ với ai.</string>
<string name="sign_in">Đăng nhập</string>
<string name="consent_agree">Đồng ý</string>
<string name="consent_deny">Không phải bây giờ</string>

@ -639,7 +639,6 @@
<string name="sort_completion_group">完成 %s</string>
<string name="microsoft_selection_description">与您的个人 Microsoft 账号同步</string>
<string name="sign_in">登录</string>
<string name="sign_in_to_tasks_disclosure">Tasks.org 将传输和存储您的电子邮箱地址和账号 ID。 此信息将用于身份验证并向您提供与服务相关的重要公告。 此信息不会与任何人共享。</string>
<string name="consent_agree">同意</string>
<string name="consent_deny">再想想</string>
<string name="default_reminder">默认提醒</string>
@ -707,7 +706,6 @@
<string name="banner_app_updated_title">应用已更新</string>
<string name="multiline_title_off">按完成保存任务</string>
<string name="continue_without_sync">继续但不同步</string>
<string name="help_me_choose">帮我选择</string>
<string name="delete_tasks_warning">%s 将会删除。此操作无法撤销!</string>
<string name="widget_view_more_tasks">查看更多任务</string>
</resources>

@ -546,7 +546,6 @@
<string name="markdown_description">在標題和描述中啟用 Markdown</string>
<string name="more_options">更多選項</string>
<string name="hint_customize_edit_body">您可以透過重新排列或移除欄位來自訂此畫面</string>
<string name="sign_in_to_tasks_disclosure">您的電子郵件地址和帳戶 ID 將被傳輸並儲存於 Tasks.org。這些資訊將用於驗證和提供重要的服務相關公告。這些資訊不會與任何人分享。</string>
<string name="app_password_save">請使用這些憑證來設定第三方應用程式。它們授予您對 Tasks.org 帳戶的完全存取權限,請勿將其寫下或與他人分享!</string>
<string name="repeats_every">每次重複</string>
<string name="share_list">分享清單</string>

@ -38,6 +38,8 @@
<string name="url_issue_tracker">https://github.com/tasks/tasks/issues</string>
<string name="url_changelog">https://tasks.org/changelog</string>
<string name="url_source_code">https://tasks.org/source</string>
<string name="url_license">https://tasks.org/license</string>
<string name="url_tos">https://tasks.org/terms</string>
<string name="url_privacy_policy">https://tasks.org/privacy</string>
<string name="url_backups">https://tasks.org/backups</string>
<string name="url_translations">https://tasks.org/translations</string>
@ -422,6 +424,8 @@
<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="event_accept_tos">accept_tos</string>
<string name="event_accept_tos_update">accept_tos_update</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>
@ -431,4 +435,6 @@
<string name="p_last_sync">last_sync_time</string>
<string name="p_multiline_title">multiline_title</string>
<string name="p_dynamic_color">dynamic_color</string>
<string name="remote_config_tos_version">tos_version</string>
<integer name="default_tos_version">1</integer>
</resources>

@ -411,7 +411,6 @@ File %1$s contained %2$s.\n\n
<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>
@ -637,6 +636,7 @@ File %1$s contained %2$s.\n\n
<string name="issue_tracker">Issue tracker</string>
<string name="open_source">Open source</string>
<string name="privacy">Privacy</string>
<string name="legal">Legal</string>
<string name="authorization_cancelled">Authorization cancelled</string>
<string name="google_play_subscribers">Google Play subscribers</string>
<string name="github_sponsors">GitHub Sponsors</string>
@ -650,7 +650,6 @@ File %1$s contained %2$s.\n\n
<string name="above_average">Above average</string>
<string name="save_percent">Save %d%%</string>
<string name="sign_in_to_tasks">Sign in to Tasks.org</string>
<string name="sign_in_to_tasks_disclosure">Your e-mail address and account ID will be transmitted and stored by Tasks.org. This information will be used for authentication and to provide you with important service related announcements. This information will not be shared with anyone.</string>
<string name="app_password">App password</string>
<string name="app_passwords">App passwords</string>
<string name="app_passwords_more_info">Synchronize your tasks and calendars with third-party desktop and mobile apps. Tap here for more info</string>
@ -705,8 +704,16 @@ File %1$s contained %2$s.\n\n
<string name="enable_alarms">Get notified at the right time</string>
<string name="enable_alarms_description">To make sure you\'re notified at the right time, grant permission to set alarms and reminders in Settings</string>
<string name="sign_in">Sign in</string>
<string name="consent_agree">Agree</string>
<string name="consent_deny">Not now</string>
<string name="legal_disclosure">By using Tasks.org, you agree to the %1$s and %2$s. Tasks is open source software provided under the %3$s.</string>
<string name="terms_of_service">Terms of service</string>
<string name="terms_of_service_proper">Terms of Service</string>
<string name="privacy_policy_proper">Privacy Policy</string>
<string name="gplv3_license">GPLv3 License</string>
<string name="tos_updated_title">We\'ve updated our Terms of Service</string>
<string name="view_tos">View Terms of Service</string>
<string name="accept">Accept</string>
<string name="exit">Exit</string>
<string name="returning_user">Returning user?</string>
<string name="sort_sorting">Sorting</string>
<string name="sort_grouping">Grouping</string>
<string name="sort_ascending">Ascending</string>

@ -73,12 +73,22 @@
</PreferenceCategory>
<PreferenceCategory
android:title="@string/privacy">
android:title="@string/legal">
<Preference
android:key="@string/terms_of_service"
android:title="@string/terms_of_service"
app:icon="@drawable/ic_outline_gavel_24px" />
<Preference
android:key="@string/privacy_policy"
android:title="@string/privacy_policy"
app:icon="@drawable/ic_outline_perm_identity_24px" />
</PreferenceCategory>
<PreferenceCategory>
<SwitchPreferenceCompat
android:defaultValue="true"
android:key="@string/p_collect_statistics"

@ -79,6 +79,8 @@ data class CaldavAccount(
fun isPaymentRequired() = error.isPaymentRequired()
fun isTosRequired() = error.isTosRequired()
val hasError: Boolean
get() = !error.isNullOrBlank()
@ -108,9 +110,12 @@ data class CaldavAccount(
const val ERROR_UNAUTHORIZED = "HTTP ${HttpURLConnection.HTTP_UNAUTHORIZED}"
const val ERROR_PAYMENT_REQUIRED = "HTTP ${HttpURLConnection.HTTP_PAYMENT_REQUIRED}"
const val ERROR_TOS_REQUIRED = "HTTP 451"
fun String?.openTaskType(): String? = this?.split(":")?.get(0)
fun String?.isPaymentRequired(): Boolean = this?.startsWith(ERROR_PAYMENT_REQUIRED) == true
fun String?.isTosRequired(): Boolean = this?.startsWith(ERROR_TOS_REQUIRED) == true
}
}

@ -3,6 +3,7 @@ package org.tasks.preferences
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.firstOrNull
@ -25,5 +26,6 @@ class TasksPreferences(private val dataStore: DataStore<Preferences>) {
val collapseTags = booleanPreferencesKey("drawer_collapse_tags")
val collapseDebug = booleanPreferencesKey("drawer_collapse_debug")
val collapsePlaces = booleanPreferencesKey("drawer_collapse_places")
val acceptedTosVersion = intPreferencesKey("accepted_tos_version")
}
}
Loading…
Cancel
Save