diff --git a/app/src/androidTest/java/org/tasks/data/DeletionDaoTests.kt b/app/src/androidTest/java/org/tasks/data/DeletionDaoTests.kt index 95ac4f7b8..cc45b38da 100644 --- a/app/src/androidTest/java/org/tasks/data/DeletionDaoTests.kt +++ b/app/src/androidTest/java/org/tasks/data/DeletionDaoTests.kt @@ -9,8 +9,8 @@ import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Test import org.tasks.data.dao.CaldavDao -import org.tasks.data.dao.CaldavDao.Companion.LOCAL import org.tasks.data.dao.DeletionDao +import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar import org.tasks.data.entity.CaldavTask import org.tasks.date.DateTimeUtils.newDateTime @@ -62,7 +62,8 @@ class DeletionDaoTests : InjectingTestCase() { fun purgeDeletedLocalTask() = runBlocking { val task = newTask(with(DELETION_TIME, newDateTime())) taskDao.createNew(task) - caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL)) + caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL)) + caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd")) caldavDao.insert(CaldavTask(task = task.id, calendar = "1234")) deletionDao.purgeDeleted() @@ -74,7 +75,8 @@ class DeletionDaoTests : InjectingTestCase() { fun dontPurgeActiveTasks() = runBlocking { val task = newTask() taskDao.createNew(task) - caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = LOCAL)) + caldavDao.insert(CaldavAccount(uuid = "abcd", accountType = CaldavAccount.TYPE_LOCAL)) + caldavDao.insert(CaldavCalendar(name = "", uuid = "1234", account = "abcd")) caldavDao.insert(CaldavTask(task = task.id, calendar = "1234")) deletionDao.purgeDeleted() diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 96871d96c..954258193 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -418,6 +418,10 @@ android:name=".caldav.CaldavAccountSettingsActivity" android:theme="@style/Tasks"/> + + diff --git a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt index da5d8cedf..3c3d69e62 100644 --- a/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt +++ b/app/src/main/java/com/todoroo/astrid/activity/MainActivity.kt @@ -11,8 +11,10 @@ import android.graphics.Color import android.os.Bundle import androidx.activity.SystemBarStyle import androidx.activity.compose.BackHandler +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode @@ -31,13 +33,16 @@ import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.dp import androidx.core.content.IntentCompat.getParcelableExtra import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import androidx.navigation.toRoute import com.todoroo.astrid.adapter.SubheaderClickHandler import com.todoroo.astrid.dao.TaskDao +import com.todoroo.astrid.gtasks.auth.GtasksLoginActivity import com.todoroo.astrid.service.TaskCreator import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.launch @@ -45,22 +50,36 @@ import kotlinx.coroutines.runBlocking import org.tasks.BuildConfig import org.tasks.R import org.tasks.analytics.Firebase +import org.tasks.auth.SignInActivity import org.tasks.billing.Inventory +import org.tasks.caldav.CaldavAccountSettingsActivity +import org.tasks.compose.AddAccountDestination import org.tasks.compose.HomeDestination +import org.tasks.compose.accounts.AddAccountScreen +import org.tasks.compose.accounts.AddAccountViewModel import org.tasks.compose.home.HomeScreen import org.tasks.data.dao.AlarmDao import org.tasks.data.dao.CaldavDao import org.tasks.data.dao.LocationDao import org.tasks.data.dao.TagDataDao import org.tasks.data.entity.Task +import org.tasks.dialogs.ImportTasksDialog import org.tasks.dialogs.NewFilterDialog +import org.tasks.etebase.EtebaseAccountSettingsActivity import org.tasks.extensions.Context.nightMode +import org.tasks.extensions.Context.toast import org.tasks.extensions.broughtToFront import org.tasks.extensions.flagsToString import org.tasks.extensions.isFromHistory +import org.tasks.files.FileHelper import org.tasks.filters.Filter +import org.tasks.jobs.WorkManager import org.tasks.preferences.DefaultFilterProvider import org.tasks.preferences.Preferences +import org.tasks.preferences.fragments.FRAG_TAG_IMPORT_TASKS +import org.tasks.sync.AddAccountDialog +import org.tasks.sync.SyncAdapters +import org.tasks.sync.microsoft.MicrosoftSignInViewModel import org.tasks.themes.ColorProvider import org.tasks.themes.TasksTheme import org.tasks.themes.Theme @@ -82,6 +101,8 @@ class MainActivity : AppCompatActivity() { @Inject lateinit var alarmDao: AlarmDao @Inject lateinit var firebase: Firebase @Inject lateinit var caldavDao: CaldavDao + @Inject lateinit var syncAdapters: SyncAdapters + @Inject lateinit var workManager: WorkManager private val viewModel: MainActivityViewModel by viewModels() private var currentNightMode = 0 @@ -125,7 +146,7 @@ class MainActivity : AppCompatActivity() { LaunchedEffect(hasAccount) { Timber.d("hasAccount=$hasAccount") if (hasAccount == false) { - // TODO: navigate to add account screen + navController.navigate(AddAccountDestination(showImport = true)) } isReady = hasAccount != null } @@ -133,7 +154,87 @@ class MainActivity : AppCompatActivity() { navController = navController, startDestination = HomeDestination, ) { + composable { + val route = it.toRoute() + 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 { + if (hasAccount != true) { + return@composable + } val scope = rememberCoroutineScope() val state = viewModel.state.collectAsStateWithLifecycle().value val drawerState = rememberDrawerState( diff --git a/app/src/main/java/org/tasks/analytics/Constants.kt b/app/src/main/java/org/tasks/analytics/Constants.kt index 39a7843ac..b27aa3926 100644 --- a/app/src/main/java/org/tasks/analytics/Constants.kt +++ b/app/src/main/java/org/tasks/analytics/Constants.kt @@ -9,4 +9,5 @@ object Constants { const val SYNC_TYPE_ETEBASE = "etebase" const val SYNC_TYPE_DECSYNC = "decsync" const val SYNC_TYPE_MICROSOFT = "microsoft" + const val SYNC_TYPE_LOCAL = "local" } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt index 453718cc7..a6062f790 100644 --- a/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/BaseCaldavAccountSettingsActivity.kt @@ -36,6 +36,7 @@ import org.tasks.compose.ServerSelector import org.tasks.data.dao.CaldavDao import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount.Companion.SERVER_UNKNOWN +import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.data.entity.Task import org.tasks.databinding.ActivityCaldavAccountSettingsBinding import org.tasks.dialogs.DialogBuilder @@ -116,7 +117,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager imm.showSoftInput(binding.name, InputMethodManager.SHOW_IMPLICIT) } - if (!inventory.hasPro) { + if (!inventory.hasPro && caldavAccount?.accountType != TYPE_LOCAL) { newSnackbar(getString(R.string.this_feature_requires_a_subscription)) .setDuration(BaseTransientBottomBar.LENGTH_INDEFINITE) .setAction(R.string.button_subscribe) { @@ -308,7 +309,8 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv private fun newSnackbar(message: String?): Snackbar { val snackbar = Snackbar.make(binding.rootLayout, message!!, 8000) - .setTextColor(getColor(R.color.snackbar_text_color)) + .setBackgroundTint(getColor(R.color.dialog_background)) + .setTextColor(getColor(R.color.text_primary)) .setActionTextColor(getColor(R.color.snackbar_action_color)) snackbar .view @@ -341,7 +343,7 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv } } - private fun removeAccountPrompt() { + protected open suspend fun removeAccountPrompt() { if (requestInProgress()) { return } @@ -378,7 +380,9 @@ abstract class BaseCaldavAccountSettingsActivity : ThemedInjectingAppCompatActiv override fun onMenuItemClick(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_help -> openUri(helpUrl) - R.id.remove -> removeAccountPrompt() + R.id.remove -> lifecycleScope.launch { + removeAccountPrompt() + } } return onOptionsItemSelected(item) } diff --git a/app/src/main/java/org/tasks/caldav/LocalAccountSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/LocalAccountSettingsActivity.kt new file mode 100644 index 000000000..febacd7a7 --- /dev/null +++ b/app/src/main/java/org/tasks/caldav/LocalAccountSettingsActivity.kt @@ -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 +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt b/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt index 87fe90983..47012d586 100644 --- a/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt +++ b/app/src/main/java/org/tasks/caldav/LocalListSettingsActivity.kt @@ -3,26 +3,20 @@ package org.tasks.caldav import android.os.Bundle import androidx.activity.compose.setContent import dagger.hilt.android.AndroidEntryPoint -import kotlinx.coroutines.runBlocking import org.tasks.compose.DeleteButton -import org.tasks.data.dao.CaldavDao import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavCalendar import org.tasks.themes.TasksTheme @AndroidEntryPoint class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() { - - override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val canDelete = runBlocking { caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).size > 1 } - setContent { TasksTheme { BaseCaldavSettingsContent ( - optionButton = { if (!isNew && canDelete) DeleteButton(caldavCalendar?.name ?: "") { delete() } } + optionButton = { if (!isNew) DeleteButton(caldavCalendar?.name ?: "") { delete() } } ) } } @@ -37,4 +31,4 @@ class LocalListSettingsActivity : BaseCaldavCalendarSettingsActivity() { override suspend fun deleteCalendar(caldavAccount: CaldavAccount, caldavCalendar: CaldavCalendar) = onDeleted(true) -} \ No newline at end of file +} diff --git a/app/src/main/java/org/tasks/compose/AddAccountDialog.kt b/app/src/main/java/org/tasks/compose/AddAccountDialog.kt index 9a06f6c98..de8c75250 100644 --- a/app/src/main/java/org/tasks/compose/AddAccountDialog.kt +++ b/app/src/main/java/org/tasks/compose/AddAccountDialog.kt @@ -16,7 +16,6 @@ import org.tasks.themes.TasksTheme fun AddAccountDialog( hasTasksAccount: Boolean, hasPro: Boolean, - enableMicrosoftSync: Boolean = true, selected: (Platform) -> Unit, ) { Column(modifier = Modifier.verticalScroll(rememberScrollState())) { @@ -36,15 +35,13 @@ fun AddAccountDialog( icon = R.drawable.ic_google, onClick = { selected(Platform.GOOGLE_TASKS) } ) - if (enableMicrosoftSync) { - SyncAccount( - title = R.string.microsoft, - cost = if (hasPro) null else R.string.cost_free, - description = R.string.microsoft_selection_description, - icon = R.drawable.ic_microsoft_tasks, - onClick = { selected(Platform.MICROSOFT) } - ) - } + SyncAccount( + title = R.string.microsoft, + cost = if (hasPro) null else R.string.cost_free, + description = R.string.microsoft_selection_description, + icon = R.drawable.ic_microsoft_tasks, + onClick = { selected(Platform.MICROSOFT) } + ) SyncAccount( title = R.string.davx5, cost = if (hasPro) null else R.string.cost_money, diff --git a/app/src/main/java/org/tasks/compose/Destinations.kt b/app/src/main/java/org/tasks/compose/Destinations.kt index a33d5f661..579b334a2 100644 --- a/app/src/main/java/org/tasks/compose/Destinations.kt +++ b/app/src/main/java/org/tasks/compose/Destinations.kt @@ -4,3 +4,6 @@ import kotlinx.serialization.Serializable @Serializable object HomeDestination + +@Serializable +data class AddAccountDestination(val showImport: Boolean) diff --git a/app/src/main/java/org/tasks/compose/accounts/AddAccountScreen.kt b/app/src/main/java/org/tasks/compose/accounts/AddAccountScreen.kt new file mode 100644 index 000000000..5e3a8692f --- /dev/null +++ b/app/src/main/java/org/tasks/compose/accounts/AddAccountScreen.kt @@ -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 = {}, + ) + } +} diff --git a/app/src/main/java/org/tasks/compose/accounts/AddAccountViewModel.kt b/app/src/main/java/org/tasks/compose/accounts/AddAccountViewModel.kt new file mode 100644 index 000000000..9dc7ee676 --- /dev/null +++ b/app/src/main/java/org/tasks/compose/accounts/AddAccountViewModel.kt @@ -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)) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/tasks/data/CaldavAccountExtensions.kt b/app/src/main/java/org/tasks/data/CaldavAccountExtensions.kt index 945aed3ee..ec853a187 100644 --- a/app/src/main/java/org/tasks/data/CaldavAccountExtensions.kt +++ b/app/src/main/java/org/tasks/data/CaldavAccountExtensions.kt @@ -7,6 +7,7 @@ import org.tasks.activities.GoogleTaskListSettingsActivity import org.tasks.caldav.BaseCaldavAccountSettingsActivity import org.tasks.caldav.CaldavAccountSettingsActivity import org.tasks.caldav.CaldavCalendarSettingsActivity +import org.tasks.caldav.LocalAccountSettingsActivity import org.tasks.caldav.LocalListSettingsActivity import org.tasks.data.OpenTaskDao.Companion.isDavx5 import org.tasks.data.OpenTaskDao.Companion.isDavx5Managed @@ -29,6 +30,7 @@ val CaldavAccount.prefTitle: Int uuid.isDecSync() -> R.string.decsync isMicrosoft -> R.string.microsoft isGoogleTasks -> R.string.gtasks_GPr_header + isLocalList -> R.string.local_lists else -> 0 } @@ -42,6 +44,7 @@ val CaldavAccount.prefIcon: Int uuid.isDecSync() -> R.drawable.ic_decsync isMicrosoft -> R.drawable.ic_microsoft_tasks isGoogleTasks -> R.drawable.ic_google + isLocalList -> R.drawable.ic_outline_cloud_off_24px else -> 0 } @@ -66,6 +69,7 @@ val CaldavAccount.accountSettingsClass: Class CaldavAccountSettingsActivity::class.java isEtebaseAccount -> EtebaseAccountSettingsActivity::class.java isOpenTasks -> OpenTaskAccountSettingsActivity::class.java + isLocalList -> LocalAccountSettingsActivity::class.java else -> throw IllegalArgumentException("Unexpected account type: $this") } diff --git a/app/src/main/java/org/tasks/filters/PreferenceDrawerConfiguration.kt b/app/src/main/java/org/tasks/filters/PreferenceDrawerConfiguration.kt index 766ea00ba..5447f4da0 100644 --- a/app/src/main/java/org/tasks/filters/PreferenceDrawerConfiguration.kt +++ b/app/src/main/java/org/tasks/filters/PreferenceDrawerConfiguration.kt @@ -27,7 +27,4 @@ class PreferenceDrawerConfiguration( override val recentlyModifiedFilter: Boolean get() = preferences.getBoolean(R.string.p_show_recently_modified_filter, super.recentlyModifiedFilter) - - override val localListsEnabled: Boolean - get() = preferences.getBoolean(R.string.p_lists_enabled, super.localListsEnabled) } \ No newline at end of file diff --git a/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt b/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt index 1ed084b40..bd24b8ce2 100644 --- a/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt +++ b/app/src/main/java/org/tasks/jobs/MigrateLocalWork.kt @@ -3,14 +3,14 @@ package org.tasks.jobs import android.content.Context import androidx.hilt.work.HiltWorker import androidx.work.WorkerParameters +import com.todoroo.astrid.service.TaskDeleter import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import org.tasks.R import org.tasks.analytics.Firebase import org.tasks.caldav.CaldavClientProvider import org.tasks.data.dao.CaldavDao +import org.tasks.data.entity.CaldavAccount import org.tasks.injection.BaseWorker -import org.tasks.preferences.Preferences import org.tasks.sync.SyncAdapters @HiltWorker @@ -20,14 +20,18 @@ class MigrateLocalWork @AssistedInject constructor( firebase: Firebase, private val clientProvider: CaldavClientProvider, private val caldavDao: CaldavDao, - private val preferences: Preferences, - private val syncAdapters: SyncAdapters + private val syncAdapters: SyncAdapters, + private val taskDeleter: TaskDeleter, ) : BaseWorker(context, workerParams, firebase) { override suspend fun run(): Result { val uuid = inputData.getString(EXTRA_ACCOUNT) ?: return Result.failure() val caldavAccount = caldavDao.getAccountByUuid(uuid) ?: return Result.failure() val caldavClient = clientProvider.forAccount(caldavAccount) - caldavDao.getCalendarsByAccount(CaldavDao.LOCAL).forEach { + val fromAccount = caldavDao + .getAccounts(CaldavAccount.TYPE_LOCAL) + .firstOrNull() + ?: return Result.success() + caldavDao.getCalendarsByAccount(fromAccount.uuid!!).forEach { caldavDao.update( it.copy( url = caldavClient.makeCollection(it.name!!, it.color), @@ -35,7 +39,7 @@ class MigrateLocalWork @AssistedInject constructor( ) ) } - preferences.setBoolean(R.string.p_lists_enabled, false) + taskDeleter.delete(fromAccount) syncAdapters.sync() return Result.success() } diff --git a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt index bc72bbc80..f86996191 100644 --- a/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt +++ b/app/src/main/java/org/tasks/preferences/DefaultFilterProvider.kt @@ -11,8 +11,6 @@ import org.tasks.data.dao.TagDataDao import org.tasks.data.entity.CaldavCalendar.Companion.ACCESS_READ_ONLY import org.tasks.data.entity.CaldavTask import org.tasks.data.entity.Task -import org.tasks.data.getLocalAccount -import org.tasks.data.getLocalList import org.tasks.filters.CaldavFilter import org.tasks.filters.CustomFilter import org.tasks.filters.Filter @@ -92,7 +90,7 @@ class DefaultFilterProvider @Inject constructor( ?.let { caldavDao.getAccountByUuid(it) } ?.let { account -> CaldavFilter(calendar = list, account = account) } } - ?: CaldavFilter(calendar = caldavDao.getLocalList(), account = caldavDao.getLocalAccount()) + ?: throw IllegalStateException() defaultList = filter return filter } diff --git a/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt b/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt index 13e9f7919..e280b6ea7 100644 --- a/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt +++ b/app/src/main/java/org/tasks/preferences/PreferencesViewModel.kt @@ -17,7 +17,6 @@ import org.tasks.R import org.tasks.backup.BackupConstants import org.tasks.data.dao.CaldavDao import org.tasks.data.entity.CaldavAccount -import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.date.DateTimeUtils.newDateTime import org.tasks.googleapis.InvokerFactory import org.tasks.gtasks.GoogleAccountManager @@ -38,7 +37,7 @@ class PreferencesViewModel @Inject constructor( val lastDriveBackup = MutableLiveData() val lastAndroidBackup = MutableLiveData() val caldavAccounts: Flow> - get() = caldavDao.watchAccounts(exclude = listOf(TYPE_LOCAL)) + get() = caldavDao.watchAccounts() private fun isStale(timestamp: Long?) = timestamp != null diff --git a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt index dffd5226f..3f6416628 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/Backups.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/Backups.kt @@ -27,7 +27,7 @@ const val REQUEST_DRIVE_BACKUP = 12002 private const val REQUEST_PICKER = 10003 private const val REQUEST_BACKUP_NOW = 10004 private const val FRAG_TAG_EXPORT_TASKS = "frag_tag_export_tasks" -private const val FRAG_TAG_IMPORT_TASKS = "frag_tag_import_tasks" +const val FRAG_TAG_IMPORT_TASKS = "frag_tag_import_tasks" @AndroidEntryPoint class Backups : InjectingPreferenceFragment() { diff --git a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt index d0b484c32..b51998860 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/MainSettingsFragment.kt @@ -136,6 +136,8 @@ class MainSettingsFragment : InjectingPreferenceFragment() { ) Platform.DECSYNC_CC -> context?.openUri(R.string.url_decsync) + + Platform.LOCAL -> {} } } } @@ -160,13 +162,6 @@ class MainSettingsFragment : InjectingPreferenceFragment() { }) } preferenceScreen.removeAt(current, index - current) - if (caldavAccounts.isEmpty()) { - addAccount.setTitle(R.string.not_signed_in) - addAccount.setIcon(R.drawable.ic_outline_cloud_off_24px) - } else { - addAccount.setTitle(R.string.add_account) - addAccount.setIcon(R.drawable.ic_outline_add_24px) - } tintIcons(addAccount, requireContext().getColor(R.color.icon_tint_with_alpha)) } @@ -210,7 +205,7 @@ class MainSettingsFragment : InjectingPreferenceFragment() { pref.setTitle(account.prefTitle) pref.summary = account.name pref.setIcon(account.prefIcon) - if (account.isCaldavAccount) { + if (account.isCaldavAccount || account.isLocalList) { tintIcons(pref, requireContext().getColor(R.color.icon_tint_with_alpha)) } pref.setOnPreferenceClickListener { diff --git a/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt b/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt index 2985a1827..ec3e8d206 100644 --- a/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt +++ b/app/src/main/java/org/tasks/preferences/fragments/TasksAccount.kt @@ -22,7 +22,6 @@ import org.tasks.auth.SignInActivity import org.tasks.auth.SignInActivity.Platform import org.tasks.billing.Inventory import org.tasks.billing.Purchase -import org.tasks.data.dao.CaldavDao import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount.Companion.isPaymentRequired import org.tasks.extensions.Context.openUri @@ -203,7 +202,8 @@ class TasksAccount : BaseAccountPreference() { } lifecycleScope.launch { - val listCount = caldavDao.listCount(CaldavDao.LOCAL) + val localAccount = caldavDao.getAccounts(CaldavAccount.TYPE_LOCAL).firstOrNull() + val listCount = localAccount?.uuid?.let { caldavDao.listCount(it) } ?: 0 val quantityString = resources.getQuantityString(R.plurals.list_count, listCount, listCount) findPreference(R.string.migrate).isVisible = listCount > 0 findPreference(R.string.local_lists).summary = diff --git a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt index 16af244f3..2f6b33d1d 100644 --- a/app/src/main/java/org/tasks/sync/AddAccountDialog.kt +++ b/app/src/main/java/org/tasks/sync/AddAccountDialog.kt @@ -7,6 +7,7 @@ import androidx.fragment.app.DialogFragment import androidx.fragment.app.setFragmentResult import dagger.hilt.android.AndroidEntryPoint import org.tasks.R +import org.tasks.compose.AddAccountDialog import org.tasks.dialogs.DialogBuilder import org.tasks.extensions.Context.openUri import org.tasks.preferences.Preferences @@ -34,7 +35,8 @@ class AddAccountDialog : DialogFragment() { DAVX5, CALDAV, ETESYNC, - DECSYNC_CC + DECSYNC_CC, + LOCAL, } override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { @@ -46,9 +48,8 @@ class AddAccountDialog : DialogFragment() { theme = theme.themeBase.index, primary = theme.themeColor.primaryColor, ) { - org.tasks.compose.AddAccountDialog( + AddAccountDialog( hasTasksAccount = hasTasksAccount, - enableMicrosoftSync = preferences.getBoolean(R.string.p_microsoft_sync, false), hasPro = hasPro, selected = this::selected ) diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 87a25eb25..a8067639c 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -481,7 +481,6 @@ راعي يستلزم التوثيق رعاة GitHub - لم تسجل الدخول تم إلغاء التخويل تعقب المشكلة انضم إلى #tasks على Libera Chat diff --git a/app/src/main/res/values-bg-rBG/strings.xml b/app/src/main/res/values-bg-rBG/strings.xml index 6eb9a9674..22837f35c 100644 --- a/app/src/main/res/values-bg-rBG/strings.xml +++ b/app/src/main/res/values-bg-rBG/strings.xml @@ -641,7 +641,6 @@ Всички приложения, които използват паролата ще бъдат отписани Използвайте тези данни за вход за настройка на друго приложение. Те дават пълен достъп до профила ви в Tasks.org за това не ги записвайте никъде и не ги споделяйте с никого! Синхронизирайте задачите и календарите си с други настолни и мобилни приложения. Докоснете за повече информация - Не сте вписани Недостатъчно ниво на абонамент. За да бъде възстановена услугата надстройте своя абонамент. Удостоверяването е спряно Местоположение във фонов режим diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index ee0c5c808..64e477316 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -578,7 +578,6 @@ Přihlášení přes Google sponzoři na GitHubu předplatitelé Google Play - Nepřihlášeni Autorizace zrušena Soukromí Otevřený zdrojový kód diff --git a/app/src/main/res/values-da/strings.xml b/app/src/main/res/values-da/strings.xml index 461d23315..fcdfca0fe 100644 --- a/app/src/main/res/values-da/strings.xml +++ b/app/src/main/res/values-da/strings.xml @@ -515,7 +515,6 @@ Log ind med Google GitHub-sponsorer Google Play-abonnenter - Ikke logget ind Autorisation annulleret Følg r/tasks Nuværende abonnement: %s diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 972b8d8cd..6e472509d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -543,7 +543,6 @@ Mit GitHub anmelden GitHub-Sponsoren Google Play-Abonnenten - Nicht angemeldet Autorisierung abgebrochen Mit Google anmelden Nicht im „Wähle deinen Preis“-Abonnement enthalten diff --git a/app/src/main/res/values-eo/strings.xml b/app/src/main/res/values-eo/strings.xml index 29a727948..27240733d 100644 --- a/app/src/main/res/values-eo/strings.xml +++ b/app/src/main/res/values-eo/strings.xml @@ -611,7 +611,6 @@ Ŝpari je %d%% %1$s ne plu povos atingi %2$s Komunigi liston - Ne ensalutinte Fenestraĵa ID: %d Komenci %s Malfermi liston de laste rigardita diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 41c8ef5ff..ba5503019 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -556,7 +556,6 @@ Acceder con GitHub Patrocinadores de GitHub Suscriptores de Google Play - No registrado No se encontró ninguna suscripción de Google Play elegible No se encontró ningún patrocinio de GitHub elegible Por encima del promedio diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml index e9f4b969b..2967e5cce 100644 --- a/app/src/main/res/values-et/strings.xml +++ b/app/src/main/res/values-et/strings.xml @@ -491,7 +491,6 @@ Kolmandate osapoolte litsentsid Ütle oma hind Peida kasutamata asukohad - Ei ole sisse logitud Viimati kasutatud: %s Kutse ootab vastust Esita lõpetamise heli diff --git a/app/src/main/res/values-eu/strings.xml b/app/src/main/res/values-eu/strings.xml index 1785ac596..0ab8516d7 100644 --- a/app/src/main/res/values-eu/strings.xml +++ b/app/src/main/res/values-eu/strings.xml @@ -555,7 +555,6 @@ Autentifikazioa derrigorrezkoa Hasi saioa GitHub erabiliz Google Play harpideak - Saioa hasi gabe Ez da Google Play harpidetza egokirik aurkitu Ez da GitHub sponsorship egokirik aurkitu Zerrenda lokalak diff --git a/app/src/main/res/values-fi/strings.xml b/app/src/main/res/values-fi/strings.xml index 8da091712..4ce60adbd 100644 --- a/app/src/main/res/values-fi/strings.xml +++ b/app/src/main/res/values-fi/strings.xml @@ -585,7 +585,6 @@ Astrid manuaalinen lajittelu GitHubin sponsorit Google Play -tilaajat - Ei kirjautunut sisään Lupa peruutettu yksityisyys Avoin lähdekoodi diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 82386b59c..afcdd98d4 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -561,7 +561,6 @@ Connectez-vous avec « GitHub » Parrains « GitHub » Abonnés « Google Play » - Non connecté Aucun abonnement « Google Play » éligible n\'a été trouvé Aucun parrainage « GitHub » éligible n\'a été trouvé Au dessus de la moyenne diff --git a/app/src/main/res/values-gl/strings.xml b/app/src/main/res/values-gl/strings.xml index 70f746b7a..e5574245b 100644 --- a/app/src/main/res/values-gl/strings.xml +++ b/app/src/main/res/values-gl/strings.xml @@ -512,7 +512,6 @@ Ten un recordatorio $%s/mes Cancelouse a autenticación - Sen acceso Por riba da media Gardar %d%% Contrasinal da app diff --git a/app/src/main/res/values-hr/strings.xml b/app/src/main/res/values-hr/strings.xml index ebc0ce8ef..ce42273de 100644 --- a/app/src/main/res/values-hr/strings.xml +++ b/app/src/main/res/values-hr/strings.xml @@ -139,7 +139,6 @@ Popisi na uređaju Pomoć Odaberi platformu - Nisi prijavljen/a Kontaktiraj programera Sakrij nekorištena mjesta Postavke mjesta diff --git a/app/src/main/res/values-hu/strings.xml b/app/src/main/res/values-hu/strings.xml index a0defeedb..34d9e1870 100644 --- a/app/src/main/res/values-hu/strings.xml +++ b/app/src/main/res/values-hu/strings.xml @@ -539,7 +539,6 @@ Bejelentkezés GitHub fiókkal GitHub szponzorok Google Play előfizetők - Nincs bejelentkezve Nem található Google Play előfizetés Nem található GitHub szponzorálás Mentés %d%% diff --git a/app/src/main/res/values-id/strings.xml b/app/src/main/res/values-id/strings.xml index c93e21876..a767a0985 100644 --- a/app/src/main/res/values-id/strings.xml +++ b/app/src/main/res/values-id/strings.xml @@ -441,7 +441,6 @@ Masuk dengan Google Sponsor GitHub Pelanggan Google Play - Tidak masuk Otorisasi dibatalkan Gabung r/tasks Langganan saat ini: %s diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index b1f63b93a..4d6fbe316 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -556,7 +556,6 @@ Accedi con GitHub Sponsor di GitHub Abbonati a Google Play - Accesso non eseguito Nessun abbonamento Google Play idoneo trovato Nessuna sponsorizzazione GitHub idonea trovata Sincronizzazione basata sui file diff --git a/app/src/main/res/values-iw/strings.xml b/app/src/main/res/values-iw/strings.xml index a7a65b2e8..869f83ec8 100644 --- a/app/src/main/res/values-iw/strings.xml +++ b/app/src/main/res/values-iw/strings.xml @@ -565,7 +565,6 @@ להיכנס באמצעות Google נותני חסות GitHub מנויי Google Play - לא מחובר הרשאה בוטלה לא נמצא מינוי Google Play זכאי לא נמצאה חסות GitHub זכאית diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index fe41abd94..015007c62 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -561,7 +561,6 @@ Issue tracker / 問題追跡 プライバシー 認証が取り消されました - サインインしていません Tasks.org のアカウントが必要です 「価格はあなた次第」でのサブスクリプションには含まれていません リストメンバー diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index 69c894fe2..f41d1f630 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -521,7 +521,6 @@ 구글을 이용해서 로그인 깃허브 후원자 구글 플레이 구독자 - 로그인 되어있지 않음 r/tasks 구독 적합한 구글 플레이 구독을 찾을 수 없습니다 적합한 깃허브 후원 자격을 찾을 수 없습니다 diff --git a/app/src/main/res/values-lt/strings.xml b/app/src/main/res/values-lt/strings.xml index d3edb7f54..109474eab 100644 --- a/app/src/main/res/values-lt/strings.xml +++ b/app/src/main/res/values-lt/strings.xml @@ -566,7 +566,6 @@ Programėlių slaptažodžiai Programėlės slaptažodis Autentifikacija privaloma - Neprisijungta Autorizavimas atšauktas Privatumas Atviras kodas diff --git a/app/src/main/res/values-nb/strings.xml b/app/src/main/res/values-nb/strings.xml index 074a7f445..4896b7a20 100644 --- a/app/src/main/res/values-nb/strings.xml +++ b/app/src/main/res/values-nb/strings.xml @@ -542,7 +542,6 @@ Fant ikke noe GitHub-sponsorabonnement Flytt %s til Tasks.org Logg inn med GitHub - Ikke innlogget %d liste %d lister diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index ba35e06bd..467b90326 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -539,7 +539,6 @@ GitHub sponsors Google Play abonnees Log in met GitHub - Niet ingelogd Geen in aanmerking komend Google Play abonnement gevonden Geen in aanmerking komend GitHub-sponsoring gevonden Bespaar %d%% diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 608442b43..0e1321f5d 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -538,7 +538,6 @@ Wymagane uwierzytelnienie Zaloguj z GitHub Zaloguj z Google - Niezalogowany piąty %d lista diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index 0a578be40..1e73fa2eb 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -592,7 +592,6 @@ Faça login com Google Patrocinadores do GitHub Assinantes do Google Play - Não autenticado Autorização cancelada Privacidade Código aberto diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 1951ababf..a23ab86ed 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -558,7 +558,6 @@ Iniciar sessão com o Google Patrocinadores do GitHub Subscritores do Google Play - Não autenticado Privacidade Código-fonte aberto Reportar problemas diff --git a/app/src/main/res/values-ro/strings.xml b/app/src/main/res/values-ro/strings.xml index 6630833f5..bcb6c2ed0 100644 --- a/app/src/main/res/values-ro/strings.xml +++ b/app/src/main/res/values-ro/strings.xml @@ -55,7 +55,6 @@ Conectează-te cu Google Sponsorii GitHub Abonații Google Play - Nu este conectat Autorizație anulată Confidențialitate Sursă deschisă diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 79d70d6f7..19176c88e 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -550,7 +550,6 @@ Войти через Google Спонсоры GitHub Подписчики Google Play - Не авторизован Авторизация отменена Подходящей подписки Google Play не найдено Подходящее спонсорство GitHub не найдено diff --git a/app/src/main/res/values-si/strings.xml b/app/src/main/res/values-si/strings.xml index 297933ad8..d4979b309 100644 --- a/app/src/main/res/values-si/strings.xml +++ b/app/src/main/res/values-si/strings.xml @@ -91,7 +91,6 @@ GitHub සමඟ පුරනය වන්න GitHub අනුග්‍රාහකයන් Google Play ග්‍රාහකයින් - පුරනය වී නොමැත අවසරය අවලංගු කරන ලදි පෞද්ගලිකත්වය විවෘත මූලාශ්‍රය diff --git a/app/src/main/res/values-sk/strings.xml b/app/src/main/res/values-sk/strings.xml index 95e00c096..f29e45fce 100644 --- a/app/src/main/res/values-sk/strings.xml +++ b/app/src/main/res/values-sk/strings.xml @@ -685,7 +685,6 @@ Bez nastavenej dôležitosti Bez štítkov Súkromie - Neprihlásený %s pred termínom Dokumentácia Viac farieb diff --git a/app/src/main/res/values-sv/strings.xml b/app/src/main/res/values-sv/strings.xml index a1eabf909..7a922d881 100644 --- a/app/src/main/res/values-sv/strings.xml +++ b/app/src/main/res/values-sv/strings.xml @@ -468,7 +468,6 @@ Logga in med Google GitHub-sponsorer Google Play-prenumeranter - Inte inloggad Auktorisationen upphävd Integritet Öppen källkod diff --git a/app/src/main/res/values-ta/strings.xml b/app/src/main/res/values-ta/strings.xml index 1a718b404..57c7d3693 100644 --- a/app/src/main/res/values-ta/strings.xml +++ b/app/src/main/res/values-ta/strings.xml @@ -682,7 +682,6 @@ தொடக்கத்திற்குப் பிறகு %s குறிச்சொற்கள், வடிப்பான்கள் அல்லது இடங்களுக்கு கிடைக்கவில்லை ஏற்பு ரத்து செய்யப்பட்டது - உள்நுழையவில்லை சராசரிக்கு மேல் பயன்பாட்டு கடவுச்சொற்கள் Tasks.org உடன் ஒத்திசைத்து பிற பயனர்களுடன் ஒத்துழைக்கவும் diff --git a/app/src/main/res/values-th/strings.xml b/app/src/main/res/values-th/strings.xml index f3fb740d0..c1cd31adb 100644 --- a/app/src/main/res/values-th/strings.xml +++ b/app/src/main/res/values-th/strings.xml @@ -81,7 +81,6 @@ ออกจากระบบ คุณลักษณะนี้ต้องการการสมัครใช้งาน ไม่สามารถเชื่อมต่อได้ - ไม่ได้ลงชื่อเข้าใช้ ยกเลิกการตรวจสอบแล้ว ข้อมูลส่วนบุคคล เปิดแหล่งที่มา diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 1fb44bedd..d2c41668d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -539,7 +539,6 @@ GitHub ile oturum aç GitHub Sponsorları Google Play aboneleri - Oturum açılmadı Uygun Google Play aboneliği bulunamadı Uygun GitHub sponsorluğu bulunamadı %%%d tasarruf diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index a7b8d0731..6d3137f9f 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -478,7 +478,6 @@ Увійти через Google Спонсори GitHub Підписники Google Play - Не авторизовані Авторизацію скасовано Долучитися до r/tasks Поточна підписка: %s diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 1af347a6c..25674c0bc 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -100,7 +100,6 @@ Đăng nhập bằng Google Người đăng ký trên Google Play Nhà tài trợ trên GitHub - Chưa đăng nhập Đã huỷ xác thực Riêng tư Mã nguồn mở diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 1eada7492..717d71f01 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -533,7 +533,6 @@ 使用 GitHub 登录 Github 赞助者 Google Play 订阅者 - 未登录 找不到符合要求的 Google Play 订阅 找不到符合要求的 GitHub 赞助 高于平均 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 324baa846..d06aeb4f3 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -287,7 +287,6 @@ 使用 Google 登入 GitHub 贊助者 Goole Play 訂閱者 - 未登入 取消授權 目前訂閱: %s $%s/每月 diff --git a/app/src/main/res/values/keys.xml b/app/src/main/res/values/keys.xml index d890de343..3f97b3338 100644 --- a/app/src/main/res/values/keys.xml +++ b/app/src/main/res/values/keys.xml @@ -13,7 +13,7 @@ DAVx⁵ DecSync CC Tasks.org - Microsoft To Do + Microsoft To Do https://api.etebase.com/partner/tasksorg/ https://tasks.org/sync ownCloud @@ -99,7 +99,6 @@ drawer_tags_enabled drawer_tags_hide_unused drawer_places_enabled - drawer_lists_enabled drawer_places_hide_unused @@ -411,6 +410,7 @@ result state click + selection cp_todoagenda cp_astrid2taskprovider sync_add_account @@ -422,6 +422,7 @@ request_review create_shortcut create_widget + onboarding_sync type picker_mode_date picker_mode_time @@ -429,7 +430,6 @@ completed_tasks_at_bottom shown_beast_mode_hint last_sync_time - microsoft_sync multiline_title dynamic_color diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index dd1b3fa9b..289cbbeb3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -411,6 +411,8 @@ File %1$s contained %2$s.\n\n Copy selected tasks? Date and time Add account + Continue without sync + Help me choose User Password URL @@ -479,6 +481,7 @@ File %1$s contained %2$s.\n\n Requires pro subscription This feature requires a subscription Log out + %s will be deleted. This cannot be undone! All data for this account will be removed from your device Cannot access account Reinitialize @@ -638,7 +641,6 @@ File %1$s contained %2$s.\n\n Open source Privacy Authorization cancelled - Not signed in Google Play subscribers GitHub Sponsors Sign in with Google diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 4f5b44302..423cb1df3 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -4,8 +4,9 @@ xmlns:tools="http://schemas.android.com/tools"> + android:key="@string/add_account" + android:title="@string/add_account" + app:icon="@drawable/ic_outline_add_24px" /> - - - - - - \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt index 9a5de8869..04537449c 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/CaldavDao.kt @@ -66,13 +66,12 @@ abstract class CaldavDao { @Query(""" SELECT * FROM caldav_accounts -WHERE cda_account_type NOT IN (:exclude) ORDER BY CASE cda_account_type WHEN $TYPE_TASKS THEN 0 ELSE 1 END, UPPER(cda_name) """) - abstract fun watchAccounts(exclude: List = emptyList()): Flow> + abstract fun watchAccounts(): Flow> @Query(""" SELECT * @@ -416,9 +415,16 @@ ORDER BY primary_sort @Insert(onConflict = OnConflictStrategy.REPLACE) abstract suspend fun insertOrReplace(googleTaskList: CaldavCalendar): Long - companion object { - const val LOCAL = "local" + @Query(""" + SELECT COUNT(*) + FROM caldav_tasks + INNER JOIN caldav_lists ON cd_calendar = cdl_uuid + WHERE cdl_account = :account + AND cd_deleted = 0 + """) + abstract suspend fun countTasks(account: String): Int + companion object { fun Long.toAppleEpoch(): Long = (this - APPLE_EPOCH) / 1000 } } \ No newline at end of file diff --git a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt index 9e78ac63e..09a494c30 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/dao/DeletionDao.kt @@ -5,7 +5,6 @@ import androidx.room.Delete import androidx.room.Query import androidx.room.Transaction import co.touchlab.kermit.Logger -import org.tasks.data.dao.CaldavDao.Companion.LOCAL import org.tasks.data.db.SuspendDbUtils.chunkedMap import org.tasks.data.db.SuspendDbUtils.eachChunk import org.tasks.data.entity.CaldavAccount @@ -90,7 +89,14 @@ WHERE recurring = 1 @Delete internal abstract suspend fun deleteCaldavAccount(caldavAccount: CaldavAccount) - @Query("DELETE FROM tasks WHERE _id IN (SELECT _id FROM tasks INNER JOIN caldav_tasks ON _id = cd_task INNER JOIN caldav_lists ON cdl_uuid = cd_calendar WHERE cdl_account = '$LOCAL' AND deleted > 0 AND cd_deleted = 0)") + @Query(""" + DELETE FROM tasks WHERE _id IN ( + SELECT _id FROM tasks + INNER JOIN caldav_tasks ON _id = cd_task + INNER JOIN caldav_lists ON cdl_uuid = cd_calendar + INNER JOIN caldav_accounts ON cdl_account = cda_uuid + WHERE cda_account_type == ${CaldavAccount.TYPE_LOCAL} AND deleted > 0 AND cd_deleted = 0) + """) abstract suspend fun purgeDeleted() @Transaction diff --git a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt index 72d8837bc..7ed916fd6 100644 --- a/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt +++ b/data/src/commonMain/kotlin/org/tasks/data/entity/CaldavAccount.kt @@ -62,6 +62,9 @@ data class CaldavAccount( val isGoogleTasks: Boolean get() = accountType == TYPE_GOOGLE_TASKS + val isLocalList: Boolean + get() = accountType == TYPE_LOCAL + val isSuppressRepeatingTasks: Boolean get() = when (serverType) { SERVER_OPEN_XCHANGE, diff --git a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/DrawerConfiguration.kt b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/DrawerConfiguration.kt index b48aabcc5..bb3565a9a 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/DrawerConfiguration.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/compose/drawer/DrawerConfiguration.kt @@ -21,7 +21,4 @@ interface DrawerConfiguration { val recentlyModifiedFilter: Boolean get() = true - - val localListsEnabled: Boolean - get() = true } \ No newline at end of file diff --git a/kmp/src/commonMain/kotlin/org/tasks/data/CaldavDaoExtensions.kt b/kmp/src/commonMain/kotlin/org/tasks/data/CaldavDaoExtensions.kt index e59d05564..7ccb8070c 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/data/CaldavDaoExtensions.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/data/CaldavDaoExtensions.kt @@ -11,8 +11,12 @@ import tasks.kmp.generated.resources.default_list private val mutex = Mutex() -suspend fun CaldavDao.setupLocalAccount(): CaldavAccount = mutex.withLock { - val account = getLocalAccount() +suspend fun CaldavDao.newLocalAccount(): CaldavAccount = mutex.withLock { + val account = CaldavAccount( + accountType = CaldavAccount.TYPE_LOCAL, + uuid = UUIDHelper.newUUID(), + ) + .let { it.copy(id = insert(it)) } getLocalList(account) return account } @@ -22,12 +26,7 @@ suspend fun CaldavDao.getLocalList() = mutex.withLock { } suspend fun CaldavDao.getLocalAccount() = - getAccountByUuid(CaldavDao.LOCAL) - ?: CaldavAccount( - accountType = CaldavAccount.TYPE_LOCAL, - uuid = CaldavDao.LOCAL, - ) - .let { it.copy(id = insert(it)) } + getAccounts(CaldavAccount.TYPE_LOCAL).firstOrNull() ?: newLocalAccount() private suspend fun CaldavDao.getLocalList(account: CaldavAccount): CaldavCalendar = getCalendarsByAccount(account.uuid!!).getOrNull(0) diff --git a/kmp/src/commonMain/kotlin/org/tasks/filters/FilterProvider.kt b/kmp/src/commonMain/kotlin/org/tasks/filters/FilterProvider.kt index 893002a6e..dac985bba 100644 --- a/kmp/src/commonMain/kotlin/org/tasks/filters/FilterProvider.kt +++ b/kmp/src/commonMain/kotlin/org/tasks/filters/FilterProvider.kt @@ -13,7 +13,6 @@ import org.tasks.data.dao.TaskDao import org.tasks.data.entity.CaldavAccount import org.tasks.data.entity.CaldavAccount.Companion.TYPE_LOCAL import org.tasks.data.entity.CaldavAccount.Companion.TYPE_OPENTASKS -import org.tasks.data.setupLocalAccount import org.tasks.data.toLocationFilter import org.tasks.data.toTagFilter import org.tasks.filters.NavigationDrawerSubheader.SubheaderType @@ -193,9 +192,8 @@ class FilterProvider( showCreate: Boolean, forceExpand: Boolean, ): List = - caldavDao.getAccounts() - .ifEmpty { listOf(caldavDao.setupLocalAccount()) } - .filter { it.accountType != TYPE_LOCAL || configuration.localListsEnabled } + caldavDao + .getAccounts() .flatMap { caldavFilter( it, @@ -213,7 +211,7 @@ class FilterProvider( return listOf( NavigationDrawerSubheader( if (account.accountType == TYPE_LOCAL) { - getString(Res.string.drawer_local_lists) + account.name?.takeIf { it.isNotBlank() } ?: getString(Res.string.drawer_local_lists) } else { account.name },