Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
206c756ad7 |
+1
-1
@@ -118,7 +118,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.compose.foundation:foundation:1.12.0'
|
implementation 'androidx.compose.foundation:foundation:1.12.1'
|
||||||
implementation 'androidx.compose.runtime:runtime:1.12.0'
|
implementation 'androidx.compose.runtime:runtime:1.12.0'
|
||||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package net.helcel.cowspent.android.account
|
package net.helcel.cowspent.android.account
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
@@ -37,8 +38,6 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
import net.helcel.cowspent.android.main.MainConstants
|
import net.helcel.cowspent.android.main.MainConstants
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
|
||||||
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
|
||||||
import net.helcel.cowspent.theme.ThemeUtils
|
import net.helcel.cowspent.theme.ThemeUtils
|
||||||
import net.helcel.cowspent.util.CospendClientUtil
|
import net.helcel.cowspent.util.CospendClientUtil
|
||||||
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
|
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
|
||||||
@@ -109,7 +108,8 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
viewModel.useSso = false
|
viewModel.useSso = false
|
||||||
preferences.edit { putBoolean(SETTINGS_USE_SSO, false) }
|
preferences.edit { putBoolean(SETTINGS_USE_SSO, false) }
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
onLogout = { viewModel.logout() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
allowFileAccess = false
|
allowFileAccess = false
|
||||||
javaScriptEnabled = true
|
javaScriptEnabled = true
|
||||||
domStorageEnabled = true
|
domStorageEnabled = true
|
||||||
userAgentString = getWebLoginUserAgent()
|
userAgentString = getWebLoginUserAgent(context)
|
||||||
}
|
}
|
||||||
webViewClient = object : WebViewClient() {
|
webViewClient = object : WebViewClient() {
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
@@ -188,12 +188,6 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startInitialAccountSync() {
|
|
||||||
if (!CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) return
|
|
||||||
CowspentSQLiteOpenHelper.getInstance(applicationContext)
|
|
||||||
.cowspentServerSyncHelper.runAccountProjectsSync()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Deprecated in Java")
|
@Deprecated("Deprecated in Java")
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
super.onActivityResult(requestCode, resultCode, data)
|
||||||
@@ -213,8 +207,6 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
viewModel.serverUrl = ssoAccount.url
|
viewModel.serverUrl = ssoAccount.url
|
||||||
viewModel.username = ssoAccount.userId
|
viewModel.username = ssoAccount.userId
|
||||||
|
|
||||||
startInitialAccountSync()
|
|
||||||
|
|
||||||
val resultData = Intent()
|
val resultData = Intent()
|
||||||
resultData.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED)
|
resultData.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED)
|
||||||
setResult(RESULT_OK, resultData)
|
setResult(RESULT_OK, resultData)
|
||||||
@@ -242,9 +234,13 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getWebLoginUserAgent(): String {
|
private fun getWebLoginUserAgent(context: Context): String {
|
||||||
val manufacturer = Build.MANUFACTURER.replaceFirstChar { it.titlecase(Locale.ROOT) }
|
val defaultUA = try {
|
||||||
return "$manufacturer ${Build.MODEL} (Cowspent/Android)"
|
android.webkit.WebSettings.getDefaultUserAgent(context)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
Build.MANUFACTURER + " " + Build.MODEL
|
||||||
|
}
|
||||||
|
return "$defaultUA Cowspent/Android"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun parseAndLoginFromWebView(dataString: String) {
|
private fun parseAndLoginFromWebView(dataString: String) {
|
||||||
@@ -316,8 +312,6 @@ class AccountActivity : AppCompatActivity() {
|
|||||||
remove(SETTINGS_KEY_LAST_MODIFIED)
|
remove(SETTINGS_KEY_LAST_MODIFIED)
|
||||||
}
|
}
|
||||||
|
|
||||||
startInitialAccountSync()
|
|
||||||
|
|
||||||
val data = Intent()
|
val data = Intent()
|
||||||
data.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED)
|
data.putExtra(MainConstants.CREDENTIALS_CHANGED, CREDENTIALS_CHANGED)
|
||||||
setResult(RESULT_OK, data)
|
setResult(RESULT_OK, data)
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||||
@@ -29,7 +28,8 @@ fun AccountScreen(
|
|||||||
viewModel: AccountViewModel,
|
viewModel: AccountViewModel,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onConnect: () -> Unit,
|
onConnect: () -> Unit,
|
||||||
onSsoClick: (Boolean) -> Unit
|
onSsoClick: (Boolean) -> Unit,
|
||||||
|
onLogout: () -> Unit
|
||||||
) {
|
) {
|
||||||
AccountScreenContent(
|
AccountScreenContent(
|
||||||
isLoggedIn = viewModel.isLoggedIn,
|
isLoggedIn = viewModel.isLoggedIn,
|
||||||
@@ -51,57 +51,7 @@ fun AccountScreen(
|
|||||||
onBack = onBack,
|
onBack = onBack,
|
||||||
onConnect = onConnect,
|
onConnect = onConnect,
|
||||||
onSsoClick = onSsoClick,
|
onSsoClick = onSsoClick,
|
||||||
onLogout = { viewModel.requestLogout() }
|
onLogout = onLogout
|
||||||
)
|
|
||||||
|
|
||||||
viewModel.logoutImpact?.let { impact ->
|
|
||||||
LogoutConfirmationDialog(
|
|
||||||
impact = impact,
|
|
||||||
onDismiss = { viewModel.cancelLogout() },
|
|
||||||
onConfirm = { viewModel.confirmLogout() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LogoutConfirmationDialog(
|
|
||||||
impact: AccountViewModel.LogoutImpact,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: () -> Unit
|
|
||||||
) {
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(R.string.logout_confirm_title)) },
|
|
||||||
text = {
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
if (impact.projects > 0) {
|
|
||||||
pluralStringResource(
|
|
||||||
R.plurals.logout_confirm_projects, impact.projects, impact.projects
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
stringResource(R.string.logout_confirm_no_projects)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
if (impact.unsyncedBills > 0) {
|
|
||||||
Spacer(Modifier.height(12.dp))
|
|
||||||
Text(
|
|
||||||
text = pluralStringResource(
|
|
||||||
R.plurals.warning_unsynced_bills,
|
|
||||||
impact.unsyncedBills,
|
|
||||||
impact.unsyncedBills
|
|
||||||
),
|
|
||||||
fontWeight = FontWeight.Bold
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(onClick = onConfirm) { Text(stringResource(R.string.action_logout)) }
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.simple_cancel)) }
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,11 +2,9 @@ package net.helcel.cowspent.android.account
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.annotation.VisibleForTesting
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.lifecycle.AndroidViewModel
|
import androidx.lifecycle.AndroidViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
@@ -14,17 +12,10 @@ import androidx.preference.PreferenceManager
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import net.helcel.cowspent.model.DBProject
|
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
|
||||||
import net.helcel.cowspent.util.CospendClientUtil
|
import net.helcel.cowspent.util.CospendClientUtil
|
||||||
import net.helcel.cowspent.util.SecureStorage
|
import net.helcel.cowspent.util.SecureStorage
|
||||||
|
|
||||||
class AccountViewModel(application: Application) : AndroidViewModel(application) {
|
class AccountViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val TAG = "AccountViewModel"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||||
|
|
||||||
var useSso by mutableStateOf(preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false))
|
var useSso by mutableStateOf(preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false))
|
||||||
@@ -50,9 +41,6 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
var showWebView by mutableStateOf(false)
|
var showWebView by mutableStateOf(false)
|
||||||
|
|
||||||
var isLoggedIn by mutableStateOf(false)
|
var isLoggedIn by mutableStateOf(false)
|
||||||
|
|
||||||
/** Non-null while the logout confirmation is up, carrying what it would cost. */
|
|
||||||
var logoutImpact by mutableStateOf<LogoutImpact?>(null)
|
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var isValidatingLogin by mutableStateOf(false)
|
var isValidatingLogin by mutableStateOf(false)
|
||||||
@@ -100,63 +88,8 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun projectsTheAccountProvided(db: CowspentSQLiteOpenHelper): List<DBProject> {
|
|
||||||
val offered = db.accountProjects
|
|
||||||
val cospendPath = "/index.php/apps/cospend"
|
|
||||||
return db.projects.filter { project ->
|
|
||||||
offered.any {
|
|
||||||
it.remoteId == project.remoteId &&
|
|
||||||
project.serverUrl?.replace("/+$".toRegex(), "") ==
|
|
||||||
it.ncUrl.replace("/+$".toRegex(), "") + cospendPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
data class LogoutImpact(val projects: Int, val unsyncedBills: Int)
|
|
||||||
|
|
||||||
private fun measureLogoutImpact(): LogoutImpact {
|
|
||||||
val db = CowspentSQLiteOpenHelper.getInstance(getApplication())
|
|
||||||
val projects = projectsTheAccountProvided(db)
|
|
||||||
val unsynced = projects.sumOf { db.countUnsyncedBills(it.id) }
|
|
||||||
return LogoutImpact(projects.size, unsynced)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun requestLogout() {
|
|
||||||
viewModelScope.launch {
|
|
||||||
logoutImpact = withContext(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
measureLogoutImpact()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Never let a failed count block signing out - just ask without the detail.
|
|
||||||
Log.e(TAG, "Could not measure what logging out would remove", e)
|
|
||||||
LogoutImpact(0, 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun cancelLogout() {
|
|
||||||
logoutImpact = null
|
|
||||||
}
|
|
||||||
|
|
||||||
fun confirmLogout() {
|
|
||||||
logoutImpact = null
|
|
||||||
logout()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun forgetProjectsTheAccountProvided() {
|
|
||||||
try {
|
|
||||||
val db = CowspentSQLiteOpenHelper.getInstance(getApplication())
|
|
||||||
projectsTheAccountProvided(db).forEach { db.deleteProject(it.id) }
|
|
||||||
db.clearAccountProjects()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Could not remove the account's projects on logout", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
fun logout() {
|
fun logout() {
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
withContext(Dispatchers.IO) { forgetProjectsTheAccountProvided() }
|
|
||||||
SecureStorage.removePassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
SecureStorage.removePassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
||||||
}
|
}
|
||||||
preferences.edit {
|
preferences.edit {
|
||||||
|
|||||||
@@ -63,16 +63,10 @@ fun EditBillScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
// The activity reads the project off the main thread and only then reports whether this is a
|
LaunchedEffect(Unit) {
|
||||||
// new bill, so the flag arrives after the first composition. Keying on it rather than on Unit
|
if (viewModel.isNewBill) {
|
||||||
// is what makes the effect run at all.
|
focusRequester.requestFocus()
|
||||||
LaunchedEffect(viewModel.isNewBill) {
|
}
|
||||||
if (!viewModel.isNewBill) return@LaunchedEffect
|
|
||||||
// The field has to be laid out before it can take focus, which is not yet true on the
|
|
||||||
// pass that composed it. Waiting for the next frame makes this hold whether the flag was
|
|
||||||
// already set or arrived later.
|
|
||||||
withFrameNanos { }
|
|
||||||
focusRequester.requestFocus()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
StatefulAlertDialog(
|
StatefulAlertDialog(
|
||||||
@@ -204,7 +198,7 @@ fun BillBasicInfoSection(
|
|||||||
|
|
||||||
OutlinedTextField(
|
OutlinedTextField(
|
||||||
value = viewModel.amount,
|
value = viewModel.amount,
|
||||||
onValueChange = { nv ->
|
onValueChange = { nv->
|
||||||
val filteredValue = nv.filter { it in "0123456789.+-*/" }
|
val filteredValue = nv.filter { it in "0123456789.+-*/" }
|
||||||
viewModel.amount = filteredValue
|
viewModel.amount = filteredValue
|
||||||
viewModel.updateSplits()
|
viewModel.updateSplits()
|
||||||
@@ -258,8 +252,8 @@ fun BillBasicInfoSection(
|
|||||||
placeholder = { Text(stringResource(R.string.label_what)) },
|
placeholder = { Text(stringResource(R.string.label_what)) },
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) },
|
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) },
|
||||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Next),
|
||||||
keyboardActions = KeyboardActions(onDone = { focusManager.clearFocus() })
|
keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Next) })
|
||||||
)
|
)
|
||||||
|
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -534,10 +528,7 @@ fun BillAdditionalDetailsSection(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var categoryExpanded by remember { mutableStateOf(false) }
|
var categoryExpanded by remember { mutableStateOf(false) }
|
||||||
val selectedCategory =
|
val selectedCategory =
|
||||||
categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(
|
categories.find { it.id == viewModel.categoryId } ?: CategoryUtils.getCategoryById(context, viewModel.categoryId)
|
||||||
context,
|
|
||||||
viewModel.categoryId
|
|
||||||
)
|
|
||||||
|
|
||||||
EditableExposedDropdownMenu(
|
EditableExposedDropdownMenu(
|
||||||
value = selectedCategory?.name ?: "",
|
value = selectedCategory?.name ?: "",
|
||||||
@@ -591,10 +582,7 @@ fun BillAdditionalDetailsSection(
|
|||||||
|
|
||||||
var pmExpanded by remember { mutableStateOf(false) }
|
var pmExpanded by remember { mutableStateOf(false) }
|
||||||
val selectedPm =
|
val selectedPm =
|
||||||
paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(
|
paymentModes.find { it.id == viewModel.paymentModeId } ?: CategoryUtils.getPaymentModeById(context, viewModel.paymentModeId)
|
||||||
context,
|
|
||||||
viewModel.paymentModeId
|
|
||||||
)
|
|
||||||
|
|
||||||
EditableExposedDropdownMenu(
|
EditableExposedDropdownMenu(
|
||||||
value = selectedPm?.name ?: "",
|
value = selectedPm?.name ?: "",
|
||||||
|
|||||||
@@ -219,7 +219,6 @@ fun CategoryList(
|
|||||||
onDelete = { onDelete(category) }
|
onDelete = { onDelete(category) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { LabelListBottomSpacer() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -239,19 +238,9 @@ fun PaymentModeList(
|
|||||||
onDelete = { onDelete(pm) }
|
onDelete = { onDelete(pm) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
item { LabelListBottomSpacer() }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Keeps the last row clear of the add button, which otherwise sits on top of its delete icon and
|
|
||||||
* swallows the tap - with nothing below to scroll to, that row simply cannot be deleted.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun LabelListBottomSpacer() {
|
|
||||||
Spacer(modifier = Modifier.height(64.dp).fillMaxWidth())
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LabelItem(
|
fun LabelItem(
|
||||||
name: String,
|
name: String,
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package net.helcel.cowspent.android.main
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -72,43 +69,13 @@ fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A full-height, centred state that a pull to refresh can still act on.
|
|
||||||
*
|
|
||||||
* PullRefresh reads the gesture from a nested scroll source, and a plain Column offers none - so
|
|
||||||
* on an empty list, which is exactly when a refresh is wanted most, the pull never fires. A
|
|
||||||
* single-item LazyColumn has nowhere to scroll but does provide that source.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun RefreshableFullScreenState(content: @Composable ColumnScope.() -> Unit) {
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillParentMaxSize().padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingBillsState() {
|
|
||||||
RefreshableFullScreenState {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.error_loading),
|
|
||||||
style = MaterialTheme.typography.subtitle1,
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyMembersState() {
|
fun EmptyMembersState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_members).uppercase(),
|
text = stringResource(R.string.error_no_members).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
@@ -127,7 +94,11 @@ fun EmptyMembersState() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyBillsState() {
|
fun EmptyBillsState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
@@ -232,7 +203,11 @@ fun SectionHeader(title: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyState() {
|
fun EmptyState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
|||||||
@@ -105,9 +105,9 @@ fun BillsListScreen(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||||
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
|
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
|
||||||
val keyExtraFeatures = stringResource(R.string.pref_key_extra_features)
|
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
|
||||||
var showExtraFeatures by remember(keyExtraFeatures) {
|
var showBetaFeatures by remember(keyBetaFeatures) {
|
||||||
mutableStateOf(sharedPreferences.getBoolean(keyExtraFeatures, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
StatefulAlertDialog(
|
StatefulAlertDialog(
|
||||||
@@ -132,10 +132,6 @@ fun BillsListScreen(
|
|||||||
onProjectAction(projectOptionsProjectId, 1)
|
onProjectAction(projectOptionsProjectId, 1)
|
||||||
viewModel.showProjectOptionsDialogByProjectId = null
|
viewModel.showProjectOptionsDialogByProjectId = null
|
||||||
},
|
},
|
||||||
onForgetProject = {
|
|
||||||
onProjectAction(projectOptionsProjectId, 9)
|
|
||||||
viewModel.showProjectOptionsDialogByProjectId = null
|
|
||||||
},
|
|
||||||
onManageMembers = {
|
onManageMembers = {
|
||||||
onProjectAction(projectOptionsProjectId, 2)
|
onProjectAction(projectOptionsProjectId, 2)
|
||||||
viewModel.showProjectOptionsDialogByProjectId = null
|
viewModel.showProjectOptionsDialogByProjectId = null
|
||||||
@@ -169,7 +165,7 @@ fun BillsListScreen(
|
|||||||
projectType = proj?.type ?: ProjectType.LOCAL,
|
projectType = proj?.type ?: ProjectType.LOCAL,
|
||||||
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
|
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
|
||||||
isShareable = proj?.isShareable() ?: true,
|
isShareable = proj?.isShareable() ?: true,
|
||||||
showExtraFeatures = showExtraFeatures
|
showBetaFeatures = showBetaFeatures
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -338,7 +334,7 @@ fun BillsListScreen(
|
|||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
if (!isSearchExpanded) {
|
if (!isSearchExpanded) {
|
||||||
if (showExtraFeatures && viewModel.hasUnlabeledBills) {
|
if (showBetaFeatures && viewModel.hasUnlabeledBills) {
|
||||||
IconButton(onClick = onLabelBillsClick) {
|
IconButton(onClick = onLabelBillsClick) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Category,
|
Icons.Default.Category,
|
||||||
@@ -412,8 +408,6 @@ fun BillsListScreen(
|
|||||||
.pullRefresh(pullRefreshState)) {
|
.pullRefresh(pullRefreshState)) {
|
||||||
when {
|
when {
|
||||||
viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick)
|
viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick)
|
||||||
(viewModel.isRefreshing || viewModel.isLoadingBills) && viewModel.bills.isEmpty() ->
|
|
||||||
LoadingBillsState()
|
|
||||||
viewModel.showNoMembers -> EmptyMembersState()
|
viewModel.showNoMembers -> EmptyMembersState()
|
||||||
viewModel.showNoBills -> EmptyBillsState()
|
viewModel.showNoBills -> EmptyBillsState()
|
||||||
viewModel.bills.isEmpty() -> EmptyState()
|
viewModel.bills.isEmpty() -> EmptyState()
|
||||||
|
|||||||
@@ -94,16 +94,14 @@ class BillsListViewActivity :
|
|||||||
private val syncCallBack = object : ICallback {
|
private val syncCallBack = object : ICallback {
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
mActionMode?.finish()
|
mActionMode?.finish()
|
||||||
viewModel.isRefreshing = false
|
|
||||||
refreshLists()
|
refreshLists()
|
||||||
|
viewModel.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinish(result: String, message: String) {}
|
override fun onFinish(result: String, message: String) {}
|
||||||
|
|
||||||
override fun onScheduled() {
|
override fun onScheduled() {
|
||||||
// Being queued is not being done. Clearing the indicator here stops the spinner while a
|
viewModel.isRefreshing = false
|
||||||
// large project is still downloading; synchronize() already releases it when nothing
|
|
||||||
// actually started.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,7 +268,6 @@ class BillsListViewActivity :
|
|||||||
8 -> {
|
8 -> {
|
||||||
startActivity(net.helcel.cowspent.android.label.LabelManagementActivity.createIntent(this@BillsListViewActivity, pid))
|
startActivity(net.helcel.cowspent.android.label.LabelManagementActivity.createIntent(this@BillsListViewActivity, pid))
|
||||||
}
|
}
|
||||||
9 -> onRemoveProjectClick(pid)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onAccountSwitcherClick = {
|
onAccountSwitcherClick = {
|
||||||
@@ -335,7 +332,6 @@ class BillsListViewActivity :
|
|||||||
super.onResume()
|
super.onResume()
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
val selectedProjectId = preferences.getLong("selected_project", 0)
|
val selectedProjectId = preferences.getLong("selected_project", 0)
|
||||||
setupDrawerProjects()
|
|
||||||
if (selectedProjectId != 0L) {
|
if (selectedProjectId != 0L) {
|
||||||
refreshLists()
|
refreshLists()
|
||||||
}
|
}
|
||||||
@@ -343,10 +339,6 @@ class BillsListViewActivity :
|
|||||||
|
|
||||||
synchronize(SyncTrigger.APP_OPEN)
|
synchronize(SyncTrigger.APP_OPEN)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
|
||||||
syncNewlyDiscoveredProjects(withContext(Dispatchers.IO) { db.projects })
|
|
||||||
}
|
|
||||||
|
|
||||||
registerBroadcastReceiver()
|
registerBroadcastReceiver()
|
||||||
updateAvatarInDrawer(CowspentServerSyncHelper.isNextcloudAccountConfigured(this))
|
updateAvatarInDrawer(CowspentServerSyncHelper.isNextcloudAccountConfigured(this))
|
||||||
isActivityVisible = true
|
isActivityVisible = true
|
||||||
@@ -383,8 +375,6 @@ class BillsListViewActivity :
|
|||||||
fun onProjectClick(projectId: Long) {
|
fun onProjectClick(projectId: Long) {
|
||||||
if (viewModel.selectedProjectId != projectId) {
|
if (viewModel.selectedProjectId != projectId) {
|
||||||
viewModel.selectedMemberId = null
|
viewModel.selectedMemberId = null
|
||||||
viewModel.bills = emptyList()
|
|
||||||
viewModel.isLoadingBills = true
|
|
||||||
}
|
}
|
||||||
setSelectedProject(projectId)
|
setSelectedProject(projectId)
|
||||||
navigationSelection = Category(null, null)
|
navigationSelection = Category(null, null)
|
||||||
@@ -408,34 +398,16 @@ class BillsListViewActivity :
|
|||||||
private fun onRemoveProjectClick(projectId: Long) {
|
private fun onRemoveProjectClick(projectId: Long) {
|
||||||
if (projectId == 0L) return
|
if (projectId == 0L) return
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val (proj, unsyncedBills) = withContext(Dispatchers.IO) {
|
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
|
||||||
val p = db.getProject(projectId) ?: return@withContext null
|
|
||||||
p to db.countUnsyncedBills(projectId)
|
|
||||||
} ?: return@launch
|
|
||||||
|
|
||||||
val message = buildString {
|
|
||||||
if (!proj.isLocal) append(getString(R.string.dialog_confirm_remove_project_msg))
|
|
||||||
if (unsyncedBills > 0) {
|
|
||||||
if (isNotEmpty()) append("\n\n")
|
|
||||||
append(
|
|
||||||
resources.getQuantityString(
|
|
||||||
R.plurals.warning_unsynced_bills, unsyncedBills, unsyncedBills
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.showDialog(
|
viewModel.showDialog(
|
||||||
title = getString(R.string.title_confirm),
|
title = getString(R.string.title_confirm),
|
||||||
message = message.ifEmpty { null },
|
message = if (!proj.isLocal) getString(R.string.dialog_confirm_remove_project_msg) else null,
|
||||||
positiveText = getString(R.string.simple_yes),
|
positiveText = getString(R.string.simple_yes),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
db.deleteProject(projectId)
|
db.deleteProject(projectId)
|
||||||
// Otherwise the next account sync sees a project the account offers
|
|
||||||
// with no local row, and creates it again.
|
|
||||||
CowspentServerSyncHelper.forgetAccountProject(applicationContext, proj)
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
||||||
.edit { remove(lastProjectSyncKey(projectId)) }
|
.edit { remove(lastProjectSyncKey(projectId)) }
|
||||||
val dbProjects = db.projects
|
val dbProjects = db.projects
|
||||||
@@ -647,80 +619,76 @@ class BillsListViewActivity :
|
|||||||
val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0)
|
val selectedProjectId = PreferenceManager.getDefaultSharedPreferences(applicationContext).getLong("selected_project", 0)
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
try {
|
val (projId, projName) = withContext(Dispatchers.IO) {
|
||||||
val (projId, projName) = withContext(Dispatchers.IO) {
|
if (selectedProjectId != 0L) {
|
||||||
if (selectedProjectId != 0L) {
|
db.getProject(selectedProjectId)?.let {
|
||||||
db.getProject(selectedProjectId)?.let {
|
it.id to (if (it.name == "null" || it.name.isEmpty()) it.remoteId else it.name)
|
||||||
it.id to (if (it.name == "null" || it.name.isEmpty()) it.remoteId else it.name)
|
} ?: (0L to "")
|
||||||
} ?: (0L to "")
|
} else {
|
||||||
} else {
|
0L to ""
|
||||||
0L to ""
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val title = if (selectedProjectId != 0L) projName else getString(R.string.app_name)
|
||||||
|
|
||||||
|
setSelectedProject(selectedProjectId)
|
||||||
|
viewModel.title = title
|
||||||
|
val query = viewModel.searchQuery.ifEmpty { null }
|
||||||
|
|
||||||
|
val (ljItems, memberCount) = withContext(Dispatchers.IO) {
|
||||||
|
val db = CowspentSQLiteOpenHelper.getInstance(applicationContext)
|
||||||
|
val billList: List<DBBill> = if (projId != 0L) {
|
||||||
|
db.searchBills(query, projId)
|
||||||
|
} else {
|
||||||
|
ArrayList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val title = if (selectedProjectId != 0L) projName else getString(R.string.app_name)
|
val bills = billList.filter {
|
||||||
|
val mid = viewModel.selectedMemberId
|
||||||
setSelectedProject(selectedProjectId)
|
mid == null || mid == it.payerId || it.billOwersIds.contains(mid)
|
||||||
viewModel.title = title
|
|
||||||
val query = viewModel.searchQuery.ifEmpty { null }
|
|
||||||
|
|
||||||
val (ljItems, memberCount) = withContext(Dispatchers.IO) {
|
|
||||||
val db = CowspentSQLiteOpenHelper.getInstance(applicationContext)
|
|
||||||
val billList: List<DBBill> = if (projId != 0L) {
|
|
||||||
db.searchBills(query, projId)
|
|
||||||
} else {
|
|
||||||
ArrayList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val bills = billList.filter {
|
|
||||||
val mid = viewModel.selectedMemberId
|
|
||||||
mid == null || mid == it.payerId || it.billOwersIds.contains(mid)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.hasUnlabeledBills = bills.any { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
|
|
||||||
|
|
||||||
val projectMembers = db.getMembersOfProject(projId, null)
|
|
||||||
val memberMap = projectMembers.associateBy { it.id }
|
|
||||||
|
|
||||||
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.id }
|
|
||||||
val projectCategories = db.getCategories(projId).associateBy { it.id }
|
|
||||||
|
|
||||||
BillFormatter.formatBills(
|
|
||||||
bills,
|
|
||||||
memberMap,
|
|
||||||
projectCategories,
|
|
||||||
projectPaymentModes
|
|
||||||
)
|
|
||||||
|
|
||||||
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
|
||||||
val itemList = BillsListUtils.groupAndSectionBills(
|
|
||||||
bills,
|
|
||||||
memberMap,
|
|
||||||
sdf,
|
|
||||||
applicationContext
|
|
||||||
)
|
|
||||||
|
|
||||||
itemList to projectMembers.size
|
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.showNoProjects = false
|
viewModel.hasUnlabeledBills = bills.any { it.categoryId == 0L && it.state != DBBill.STATE_DELETED }
|
||||||
viewModel.showNoMembers = false
|
|
||||||
viewModel.showNoBills = false
|
|
||||||
|
|
||||||
when {
|
val projectMembers = db.getMembersOfProject(projId, null)
|
||||||
memberCount == 0 -> {
|
val memberMap = projectMembers.associateBy { it.id }
|
||||||
viewModel.showNoMembers = true
|
|
||||||
}
|
val projectPaymentModes = db.getPaymentModes(projId).associateBy { it.id }
|
||||||
ljItems.isEmpty() -> {
|
val projectCategories = db.getCategories(projId).associateBy { it.id }
|
||||||
viewModel.showNoBills = true
|
|
||||||
viewModel.bills = emptyList()
|
BillFormatter.formatBills(
|
||||||
}
|
bills,
|
||||||
else -> {
|
memberMap,
|
||||||
viewModel.bills = ljItems
|
projectCategories,
|
||||||
}
|
projectPaymentModes
|
||||||
|
)
|
||||||
|
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||||
|
val itemList = BillsListUtils.groupAndSectionBills(
|
||||||
|
bills,
|
||||||
|
memberMap,
|
||||||
|
sdf,
|
||||||
|
applicationContext
|
||||||
|
)
|
||||||
|
|
||||||
|
itemList to projectMembers.size
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.showNoProjects = false
|
||||||
|
viewModel.showNoMembers = false
|
||||||
|
viewModel.showNoBills = false
|
||||||
|
|
||||||
|
when {
|
||||||
|
memberCount == 0 -> {
|
||||||
|
viewModel.showNoMembers = true
|
||||||
|
}
|
||||||
|
ljItems.isEmpty() -> {
|
||||||
|
viewModel.showNoBills = true
|
||||||
|
viewModel.bills = emptyList()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
viewModel.bills = ljItems
|
||||||
}
|
}
|
||||||
} finally {
|
|
||||||
viewModel.isLoadingBills = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -810,68 +778,40 @@ class BillsListViewActivity :
|
|||||||
val selectedProjectId = preferences.getLong("selected_project", 0)
|
val selectedProjectId = preferences.getLong("selected_project", 0)
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
|
|
||||||
// The account tells the app which projects exist at all, so a project joined elsewhere only
|
// The account and all-projects refresh belongs to opening the app, throttled by the
|
||||||
// shows up once it has been read. That happens on login, on opening the app - throttled by
|
// SyncOnOpen interval so that resuming within the interval does not repeat it.
|
||||||
// the interval so resuming does not repeat it - and on a manual refresh, which is an
|
val intervalMinutes = SyncSettings.intervalMinutes(applicationContext)
|
||||||
// explicit ask and so is never throttled.
|
|
||||||
val intervalMinutes = SyncSettings.OPEN_SYNC_INTERVAL_MINUTES
|
|
||||||
val lastAccountSync = preferences.getLong(getString(R.string.pref_key_last_account_sync_timestamp), 0L)
|
val lastAccountSync = preferences.getLong(getString(R.string.pref_key_last_account_sync_timestamp), 0L)
|
||||||
val openPass = trigger == SyncTrigger.APP_OPEN &&
|
val accountSyncDue = trigger == SyncTrigger.APP_OPEN &&
|
||||||
now - lastAccountSync > intervalMinutes * 60 * 1000L
|
now - lastAccountSync > intervalMinutes * 60 * 1000L
|
||||||
val accountSyncDue = openPass || trigger == SyncTrigger.MANUAL
|
|
||||||
|
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val remoteProjects = withContext(Dispatchers.IO) { db.projects }
|
val remoteProjects = withContext(Dispatchers.IO) { db.projects }
|
||||||
.filter { !it.isLocal }
|
.filter { !it.isLocal && !it.isArchived }
|
||||||
// Only the first one scheduled actually starts - the rest queue behind it - and it
|
|
||||||
// takes the screen's callback with it. So the selected project goes first: otherwise
|
|
||||||
// the spinner follows a project the user is not looking at, stops when that one
|
|
||||||
// finishes, and theirs is reported as empty while it is still waiting its turn.
|
|
||||||
.sortedByDescending { it.id == selectedProjectId }
|
|
||||||
|
|
||||||
viewModel.isRefreshing = true
|
viewModel.isRefreshing = true
|
||||||
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
||||||
|
|
||||||
if (accountSyncDue && CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
|
val started = if (accountSyncDue) {
|
||||||
db.cowspentServerSyncHelper.runAccountProjectsSync()
|
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
|
||||||
}
|
db.cowspentServerSyncHelper.runAccountProjectsSync()
|
||||||
|
|
||||||
val started = if (openPass) {
|
|
||||||
// The pass is throttled by this stamp, so it has to be written here rather than
|
|
||||||
// left to the account sync: that only runs when an account is configured and only
|
|
||||||
// records itself on success, so without one the stamp stayed at zero and the pass
|
|
||||||
// repeated on every single resume, re-scheduling every project each time.
|
|
||||||
preferences.edit {
|
|
||||||
putLong(getString(R.string.pref_key_last_account_sync_timestamp), now)
|
|
||||||
}
|
}
|
||||||
remoteProjects.filter { !it.isArchived }.count {
|
remoteProjects.count {
|
||||||
val full = neverSynced(it)
|
val scheduled = db.cowspentServerSyncHelper.scheduleSync(false, it, false) != null
|
||||||
val scheduled = db.cowspentServerSyncHelper.scheduleSync(false, it, full) != null
|
if (scheduled) markProjectSynced(preferences, it.id, now)
|
||||||
if (scheduled) {
|
|
||||||
markProjectSynced(preferences, it.id, now)
|
|
||||||
if (full) markFullySynced(preferences, it.id, now)
|
|
||||||
}
|
|
||||||
scheduled
|
scheduled
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val selectedProj = remoteProjects.find { it.id == selectedProjectId }
|
val selectedProj = remoteProjects.find { it.id == selectedProjectId }
|
||||||
val lastSync = preferences.getLong(lastProjectSyncKey(selectedProjectId), 0L)
|
val lastSync = preferences.getLong(lastProjectSyncKey(selectedProjectId), 0L)
|
||||||
// The pull writes the project cursor only after it has applied everything, so a cursor
|
val due = trigger == SyncTrigger.MANUAL ||
|
||||||
// still at zero means no sync ever finished and nothing is stored locally.
|
|
||||||
// Throttling that strands the user on an empty list with no spinner - which is what
|
|
||||||
// the back gesture does to a large project, since it destroys the activity while
|
|
||||||
// the sync is still running. IHateMoney never advances the cursor, so the interval
|
|
||||||
// stays in charge there.
|
|
||||||
val neverCompleted = selectedProj != null && neverSynced(selectedProj)
|
|
||||||
val due = trigger == SyncTrigger.MANUAL || neverCompleted ||
|
|
||||||
now - lastSync > SELECTED_PROJECT_SYNC_INTERVAL_MS
|
now - lastSync > SELECTED_PROJECT_SYNC_INTERVAL_MS
|
||||||
val fullSync = neverCompleted || (trigger == SyncTrigger.MANUAL &&
|
|
||||||
!partialRefreshPreferred(preferences, selectedProjectId, now))
|
|
||||||
if (selectedProj != null && due &&
|
if (selectedProj != null && due &&
|
||||||
db.cowspentServerSyncHelper.scheduleSync(false, selectedProj, fullSync) != null
|
db.cowspentServerSyncHelper.scheduleSync(
|
||||||
|
false, selectedProj, trigger == SyncTrigger.MANUAL
|
||||||
|
) != null
|
||||||
) {
|
) {
|
||||||
markProjectSynced(preferences, selectedProj.id, now)
|
markProjectSynced(preferences, selectedProj.id, now)
|
||||||
if (fullSync) markFullySynced(preferences, selectedProj.id, now)
|
|
||||||
1
|
1
|
||||||
} else 0
|
} else 0
|
||||||
}
|
}
|
||||||
@@ -882,52 +822,7 @@ class BillsListViewActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun neverSynced(project: DBProject) =
|
|
||||||
project.type != ProjectType.IHATEMONEY && (project.lastSyncedTimestamp ?: 0L) == 0L
|
|
||||||
|
|
||||||
@VisibleForTesting
|
|
||||||
internal fun syncNewlyDiscoveredProjects(projects: List<DBProject>) {
|
|
||||||
if (!db.cowspentServerSyncHelper.isSyncPossible) return
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(applicationContext)
|
|
||||||
val now = System.currentTimeMillis()
|
|
||||||
projects
|
|
||||||
.filter { !it.isLocal && !it.isArchived && neverSynced(it) }
|
|
||||||
.forEach {
|
|
||||||
if (db.cowspentServerSyncHelper.scheduleSync(false, it, true) != null) {
|
|
||||||
markProjectSynced(preferences, it.id, now)
|
|
||||||
markFullySynced(preferences, it.id, now)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun lastProjectSyncKey(projectId: Long) = "lastProjectSyncTimestamp_$projectId"
|
private fun lastProjectSyncKey(projectId: Long) = "lastProjectSyncTimestamp_$projectId"
|
||||||
private fun lastFullSyncKey(projectId: Long) = "lastFullSyncTimestamp_$projectId"
|
|
||||||
|
|
||||||
private fun markFullySynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
|
||||||
preferences.edit { putLong(lastFullSyncKey(projectId), at) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a pull to refresh should settle for the cheaper paged walk.
|
|
||||||
*
|
|
||||||
* A refresh by hand normally re-downloads the whole project, which is what makes it a usable
|
|
||||||
* repair when the local copy looks wrong. On a large project that is expensive to repeat, so
|
|
||||||
* with beta features on a refresh that follows a recent full one walks the recent pages
|
|
||||||
* instead - the common case of pulling twice in a row costs a page rather than the project.
|
|
||||||
*/
|
|
||||||
private fun partialRefreshPreferred(
|
|
||||||
preferences: SharedPreferences,
|
|
||||||
projectId: Long,
|
|
||||||
now: Long
|
|
||||||
): Boolean {
|
|
||||||
if (!preferences.getBoolean(getString(R.string.pref_key_extra_features), false)) return false
|
|
||||||
// "Always" means what it says: every manual refresh is a full sync.
|
|
||||||
val delayMinutes = SyncSettings.fullSyncDelayMinutes(applicationContext)
|
|
||||||
if (delayMinutes == 0) return false
|
|
||||||
val lastFull = preferences.getLong(lastFullSyncKey(projectId), 0L)
|
|
||||||
if (lastFull == 0L) return false
|
|
||||||
return now - lastFull < delayMinutes * 60 * 1000L
|
|
||||||
}
|
|
||||||
private fun markProjectSynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
private fun markProjectSynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
||||||
preferences.edit { putLong(lastProjectSyncKey(projectId), at) }
|
preferences.edit { putLong(lastProjectSyncKey(projectId), at) }
|
||||||
}
|
}
|
||||||
@@ -1002,7 +897,6 @@ class BillsListViewActivity :
|
|||||||
synchronize(SyncTrigger.PROJECT_OPEN)
|
synchronize(SyncTrigger.PROJECT_OPEN)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
syncNewlyDiscoveredProjects(dbProjects)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ class BillsListViewModel : ViewModel() {
|
|||||||
var selectedMemberId by mutableStateOf<Long?>(null)
|
var selectedMemberId by mutableStateOf<Long?>(null)
|
||||||
var bills by mutableStateOf<List<Item>>(emptyList())
|
var bills by mutableStateOf<List<Item>>(emptyList())
|
||||||
var isRefreshing by mutableStateOf(false)
|
var isRefreshing by mutableStateOf(false)
|
||||||
var isLoadingBills by mutableStateOf(false)
|
|
||||||
var searchQuery by mutableStateOf("")
|
var searchQuery by mutableStateOf("")
|
||||||
var title by mutableStateOf("")
|
var title by mutableStateOf("")
|
||||||
var accountName by mutableStateOf("")
|
var accountName by mutableStateOf("")
|
||||||
|
|||||||
@@ -11,30 +11,11 @@ import net.helcel.cowspent.R
|
|||||||
import net.helcel.cowspent.model.*
|
import net.helcel.cowspent.model.*
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import java.io.InputStreamReader
|
import java.io.InputStreamReader
|
||||||
import java.net.URLDecoder
|
|
||||||
import java.text.SimpleDateFormat
|
import java.text.SimpleDateFormat
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
object ProjectImportHelper {
|
object ProjectImportHelper {
|
||||||
|
|
||||||
private fun optional(line: Array<String>, columns: Map<String, Int>, name: String): String =
|
|
||||||
columns[name]?.takeIf { it < line.size }?.let { line[it] } ?: ""
|
|
||||||
|
|
||||||
/** Parses the `#rrggbb` member colour Cospend writes in its members section. */
|
|
||||||
private fun parseHexColor(value: String): Triple<Int, Int, Int>? {
|
|
||||||
val hex = value.trim().removePrefix("#")
|
|
||||||
if (hex.length != 6) return null
|
|
||||||
return try {
|
|
||||||
Triple(
|
|
||||||
hex.substring(0, 2).toInt(16),
|
|
||||||
hex.substring(2, 4).toInt(16),
|
|
||||||
hex.substring(4, 6).toInt(16)
|
|
||||||
)
|
|
||||||
} catch (_: NumberFormatException) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
fun getFileName(contentResolver: ContentResolver, uri: Uri): String {
|
fun getFileName(contentResolver: ContentResolver, uri: Uri): String {
|
||||||
var result: String? = null
|
var result: String? = null
|
||||||
@@ -78,7 +59,6 @@ object ProjectImportHelper {
|
|||||||
val bills = mutableListOf<DBBill>()
|
val bills = mutableListOf<DBBill>()
|
||||||
val membersActive = mutableMapOf<String, Boolean>()
|
val membersActive = mutableMapOf<String, Boolean>()
|
||||||
val membersWeight = mutableMapOf<String, Double>()
|
val membersWeight = mutableMapOf<String, Double>()
|
||||||
val membersColor = mutableMapOf<String, Triple<Int, Int, Int>>()
|
|
||||||
val billRemoteIdToPayerName = mutableMapOf<Long, String>()
|
val billRemoteIdToPayerName = mutableMapOf<Long, String>()
|
||||||
val billRemoteIdToOwerStr = mutableMapOf<Long, String>()
|
val billRemoteIdToOwerStr = mutableMapOf<Long, String>()
|
||||||
|
|
||||||
@@ -96,10 +76,7 @@ object ProjectImportHelper {
|
|||||||
|
|
||||||
currentSection = when {
|
currentSection = when {
|
||||||
columns.containsKey("what") && columns.containsKey("amount") -> "bills"
|
columns.containsKey("what") && columns.containsKey("amount") -> "bills"
|
||||||
columns.containsKey("name") && columns.containsKey("weight") &&
|
|
||||||
columns.containsKey("active") -> "members"
|
|
||||||
columns.containsKey("categoryid") && columns.containsKey("categoryname") -> "categories"
|
columns.containsKey("categoryid") && columns.containsKey("categoryname") -> "categories"
|
||||||
columns.containsKey("paymentmodeid") && columns.containsKey("paymentmodename") -> "paymentmodes"
|
|
||||||
columns.containsKey("exchange_rate") && columns.containsKey("currencyname") -> "currencies"
|
columns.containsKey("exchange_rate") && columns.containsKey("currencyname") -> "currencies"
|
||||||
else -> {
|
else -> {
|
||||||
onError(context.getString(R.string.import_error_header, row))
|
onError(context.getString(R.string.import_error_header, row))
|
||||||
@@ -109,19 +86,11 @@ object ProjectImportHelper {
|
|||||||
} else {
|
} else {
|
||||||
previousLineEmpty = false
|
previousLineEmpty = false
|
||||||
when (currentSection) {
|
when (currentSection) {
|
||||||
"members" -> {
|
|
||||||
val name = line[columns["name"]!!].trim()
|
|
||||||
if (name.isNotEmpty()) {
|
|
||||||
membersWeight[name] = optional(line, columns, "weight").toDoubleOrNull() ?: 1.0
|
|
||||||
membersActive[name] = optional(line, columns, "active") != "0"
|
|
||||||
parseHexColor(optional(line, columns, "color"))?.let { membersColor[name] = it }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"categories" -> {
|
"categories" -> {
|
||||||
categories.add(DBCategory(0, line[columns["categoryid"]!!].toLong(), 0, line[columns["categoryname"]!!], optional(line, columns, "icon"), optional(line, columns, "color")))
|
categories.add(DBCategory(0, line[columns["categoryid"]!!].toLong(), 0, line[columns["categoryname"]!!], line[columns["icon"]!!], line[columns["color"]!!]))
|
||||||
}
|
}
|
||||||
"paymentmodes" -> {
|
"paymentmodes" -> {
|
||||||
paymentModes.add(DBPaymentMode(0, line[columns["paymentmodeid"]!!].toLong(), 0, line[columns["paymentmodename"]!!], optional(line, columns, "icon"), optional(line, columns, "color")))
|
paymentModes.add(DBPaymentMode(0, line[columns["categoryid"]!!].toLong(), 0, line[columns["categoryname"]!!], line[columns["icon"]!!], line[columns["color"]!!]))
|
||||||
}
|
}
|
||||||
"currencies" -> {
|
"currencies" -> {
|
||||||
val name = line[columns["currencyname"]!!]
|
val name = line[columns["currencyname"]!!]
|
||||||
@@ -131,22 +100,7 @@ object ProjectImportHelper {
|
|||||||
}
|
}
|
||||||
"bills" -> {
|
"bills" -> {
|
||||||
val what = if (columns.containsKey("what")) line[columns["what"]!!] else ""
|
val what = if (columns.containsKey("what")) line[columns["what"]!!] else ""
|
||||||
// Cospend url-encodes bill comments on export and marks trashed
|
val comment = if (columns.containsKey("comment")) line[columns["comment"]!!] else ""
|
||||||
// bills with a "deleted" column no other dialect has, so that column
|
|
||||||
// doubles as the marker for which comment encoding to expect.
|
|
||||||
val cospendDialect = columns.containsKey("deleted")
|
|
||||||
val comment = if (columns.containsKey("comment")) {
|
|
||||||
val raw = line[columns["comment"]!!]
|
|
||||||
if (cospendDialect) {
|
|
||||||
try {
|
|
||||||
URLDecoder.decode(raw, "UTF-8")
|
|
||||||
} catch (_: Exception) {
|
|
||||||
raw
|
|
||||||
}
|
|
||||||
} else raw
|
|
||||||
} else ""
|
|
||||||
val deleted = cospendDialect &&
|
|
||||||
line[columns["deleted"]!!].trim().let { it.isNotEmpty() && it != "0" }
|
|
||||||
val amount = if (columns.containsKey("amount")) line[columns["amount"]!!].toDouble() else 0.0
|
val amount = if (columns.containsKey("amount")) line[columns["amount"]!!].toDouble() else 0.0
|
||||||
val timestamp: Long = when {
|
val timestamp: Long = when {
|
||||||
columns.containsKey("timestamp") -> line[columns["timestamp"]!!].toLong()
|
columns.containsKey("timestamp") -> line[columns["timestamp"]!!].toLong()
|
||||||
@@ -167,11 +121,6 @@ object ProjectImportHelper {
|
|||||||
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toLong() else 0L
|
val catId = if (columns.containsKey("categoryid") && line[columns["categoryid"]!!].isNotEmpty()) line[columns["categoryid"]!!].toLong() else 0L
|
||||||
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toLong() else 0L
|
val pmId = if (columns.containsKey("paymentmodeid") && line[columns["paymentmodeid"]!!].isNotEmpty()) line[columns["paymentmodeid"]!!].toLong() else 0L
|
||||||
val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null
|
val pm = if (columns.containsKey("paymentmode")) line[columns["paymentmode"]!!] else null
|
||||||
// MoneyBuster's export only carries the legacy one-letter payment
|
|
||||||
// mode. Everything downstream keys off paymentModeId, so translate
|
|
||||||
// it back the same way the sync parser does.
|
|
||||||
val effectivePmId =
|
|
||||||
if (pmId != 0L) pmId else DBBill.oldPmIdToNew[pm] ?: DBBill.PAYMODE_ID_NONE
|
|
||||||
|
|
||||||
if (payerName.isNotEmpty()) {
|
if (payerName.isNotEmpty()) {
|
||||||
membersActive[payerName] = payerActive
|
membersActive[payerName] = payerActive
|
||||||
@@ -183,7 +132,7 @@ object ProjectImportHelper {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (what != "deleteMeIfYouWant" && !deleted) {
|
if (what != "deleteMeIfYouWant") {
|
||||||
billRemoteIdToOwerStr[row.toLong()] = owersStr
|
billRemoteIdToOwerStr[row.toLong()] = owersStr
|
||||||
val owersArray = owersStr.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
val owersArray = owersStr.split(",").map { it.trim() }.filter { it.isNotEmpty() }
|
||||||
for (ower in owersArray) {
|
for (ower in owersArray) {
|
||||||
@@ -191,7 +140,7 @@ object ProjectImportHelper {
|
|||||||
membersWeight[ower] = 1.0
|
membersWeight[ower] = 1.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bills.add(DBBill(0, row.toLong(), 0, 0, amount, timestamp, what, DBBill.STATE_OK, "n", pm, catId, comment, effectivePmId))
|
bills.add(DBBill(0, row.toLong(), 0, 0, amount, timestamp, what, DBBill.STATE_OK, "n", pm, catId, comment, pmId))
|
||||||
billRemoteIdToPayerName[row.toLong()] = payerName
|
billRemoteIdToPayerName[row.toLong()] = payerName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -219,20 +168,13 @@ object ProjectImportHelper {
|
|||||||
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
|
currencies.forEach { db.addCurrency(DBCurrency(0, 0, pid, it.name, it.exchangeRate, DBBill.STATE_OK)) }
|
||||||
|
|
||||||
membersWeight.keys.forEach { mName ->
|
membersWeight.keys.forEach { mName ->
|
||||||
val c = membersColor[mName]
|
memberNameToId[mName] = db.addMember(DBMember(0, 0, pid, mName, membersActive[mName] ?: true, membersWeight[mName] ?: 1.0, DBBill.STATE_OK, null, null, null, null, null))
|
||||||
memberNameToId[mName] = db.addMember(DBMember(0, 0, pid, mName, membersActive[mName] ?: true, membersWeight[mName] ?: 1.0, DBBill.STATE_OK, c?.first, c?.second, c?.third, null, null))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bills.forEach { b ->
|
bills.forEach { b ->
|
||||||
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
|
val payerId = memberNameToId[billRemoteIdToPayerName[b.remoteId]] ?: 0L
|
||||||
// Only custom labels are listed in the categories/paymentmodes sections. Built-in
|
val localCatId = catRemoteToLocal[b.categoryId] ?: 0L
|
||||||
// ones are referenced by their (negative) constant, which the UI resolves on its
|
val localPmId = pmRemoteToLocal[b.paymentModeId] ?: 0L
|
||||||
// own, so those have to be kept rather than reset to "none". Unmapped positive
|
|
||||||
// ids are dropped instead, as they would collide with local ids.
|
|
||||||
val localCatId = catRemoteToLocal[b.categoryId]
|
|
||||||
?: b.categoryId.takeIf { it < 0 } ?: 0L
|
|
||||||
val localPmId = pmRemoteToLocal[b.paymentModeId]
|
|
||||||
?: b.paymentModeId.takeIf { it < 0 } ?: 0L
|
|
||||||
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
|
val billId = db.addBill(DBBill(0, 0, pid, payerId, b.amount, b.timestamp, b.what, DBBill.STATE_OK, b.repeat, b.paymentMode, localCatId, b.comment, localPmId))
|
||||||
billRemoteIdToOwerStr[b.remoteId]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.forEach { ower ->
|
billRemoteIdToOwerStr[b.remoteId]?.split(",")?.map { it.trim() }?.filter { it.isNotEmpty() }?.forEach { ower ->
|
||||||
memberNameToId[ower]?.let { owerId -> db.addBillower(billId, owerId) }
|
memberNameToId[ower]?.let { owerId -> db.addBillower(billId, owerId) }
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import net.helcel.cowspent.model.ProjectType
|
|||||||
fun ProjectOptionsDialogContent(
|
fun ProjectOptionsDialogContent(
|
||||||
onEditProject: () -> Unit,
|
onEditProject: () -> Unit,
|
||||||
onRemoveProject: () -> Unit,
|
onRemoveProject: () -> Unit,
|
||||||
onForgetProject: () -> Unit = {},
|
|
||||||
onManageMembers: () -> Unit,
|
onManageMembers: () -> Unit,
|
||||||
onManageCurrencies: () -> Unit,
|
onManageCurrencies: () -> Unit,
|
||||||
onManageLabels: () -> Unit,
|
onManageLabels: () -> Unit,
|
||||||
@@ -37,7 +36,7 @@ fun ProjectOptionsDialogContent(
|
|||||||
projectType: ProjectType = ProjectType.LOCAL,
|
projectType: ProjectType = ProjectType.LOCAL,
|
||||||
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
|
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
|
||||||
isShareable: Boolean = true,
|
isShareable: Boolean = true,
|
||||||
showExtraFeatures: Boolean = false
|
showBetaFeatures: Boolean = false
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
shape = MaterialTheme.shapes.large,
|
shape = MaterialTheme.shapes.large,
|
||||||
@@ -74,15 +73,6 @@ fun ProjectOptionsDialogContent(
|
|||||||
val archiveLabel = if (isArchived) stringResource(R.string.action_unarchive) else stringResource(R.string.action_archive)
|
val archiveLabel = if (isArchived) stringResource(R.string.action_unarchive) else stringResource(R.string.action_archive)
|
||||||
val archiveIcon = if (isArchived) Icons.Default.Unarchive else Icons.Default.Archive
|
val archiveIcon = if (isArchived) Icons.Default.Unarchive else Icons.Default.Archive
|
||||||
row1.add(ProjectOption(archiveLabel, archiveIcon, onRemoveProject))
|
row1.add(ProjectOption(archiveLabel, archiveIcon, onRemoveProject))
|
||||||
if (isArchived) {
|
|
||||||
row1.add(
|
|
||||||
ProjectOption(
|
|
||||||
stringResource(R.string.action_forget),
|
|
||||||
Icons.Default.Delete,
|
|
||||||
onForgetProject
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
row1.add(ProjectOption(stringResource(R.string.action_delete), Icons.Default.Delete, onRemoveProject))
|
row1.add(ProjectOption(stringResource(R.string.action_delete), Icons.Default.Delete, onRemoveProject))
|
||||||
}
|
}
|
||||||
@@ -90,7 +80,7 @@ fun ProjectOptionsDialogContent(
|
|||||||
// Row 2: Manage Member, Manage Labels, Manage Currencies
|
// Row 2: Manage Member, Manage Labels, Manage Currencies
|
||||||
if (!isArchived && isMaintainer) {
|
if (!isArchived && isMaintainer) {
|
||||||
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
|
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
|
||||||
if (showExtraFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
|
if (showBetaFeatures && (projectType == ProjectType.LOCAL || projectType == ProjectType.COSPEND)) {
|
||||||
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
|
row2.add(ProjectOption(stringResource(R.string.action_labels), Icons.AutoMirrored.Filled.Label, onManageLabels))
|
||||||
}
|
}
|
||||||
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
|
row2.add(ProjectOption(stringResource(R.string.action_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
|
||||||
@@ -193,7 +183,7 @@ fun ProjectOptionsDialogPreview() {
|
|||||||
projectType = ProjectType.COSPEND,
|
projectType = ProjectType.COSPEND,
|
||||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||||
isShareable = true,
|
isShareable = true,
|
||||||
showExtraFeatures = true
|
showBetaFeatures = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -218,7 +208,7 @@ fun ProjectOptionsDialogPreview2() {
|
|||||||
projectType = ProjectType.COSPEND,
|
projectType = ProjectType.COSPEND,
|
||||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||||
isShareable = true,
|
isShareable = true,
|
||||||
showExtraFeatures = true
|
showBetaFeatures = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +232,7 @@ fun ProjectOptionsDialogPreview3() {
|
|||||||
projectType = ProjectType.LOCAL,
|
projectType = ProjectType.LOCAL,
|
||||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||||
isShareable = true,
|
isShareable = true,
|
||||||
showExtraFeatures = true
|
showBetaFeatures = true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import android.os.Bundle
|
|||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@@ -16,13 +15,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.app.NavUtils
|
import androidx.core.app.NavUtils
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import net.helcel.cowspent.R
|
|
||||||
import net.helcel.cowspent.android.about.AboutActivity
|
import net.helcel.cowspent.android.about.AboutActivity
|
||||||
import net.helcel.cowspent.android.account.AccountActivity
|
import net.helcel.cowspent.android.account.AccountActivity
|
||||||
import net.helcel.cowspent.android.helper.showToast
|
|
||||||
import net.helcel.cowspent.model.DBAccountProject
|
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
|
||||||
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
|
||||||
import net.helcel.cowspent.theme.ThemeUtils
|
import net.helcel.cowspent.theme.ThemeUtils
|
||||||
import net.helcel.cowspent.util.ColorUtils
|
import net.helcel.cowspent.util.ColorUtils
|
||||||
|
|
||||||
@@ -31,25 +25,6 @@ import net.helcel.cowspent.util.ColorUtils
|
|||||||
*/
|
*/
|
||||||
class PreferencesActivity : AppCompatActivity() {
|
class PreferencesActivity : AppCompatActivity() {
|
||||||
|
|
||||||
private val accountLauncher = registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) { result ->
|
|
||||||
if (result.resultCode == RESULT_OK) {
|
|
||||||
setResult(RESULT_OK, result.data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun restoreDeletedProjects(projects: List<DBAccountProject>) {
|
|
||||||
if (projects.isEmpty()) return
|
|
||||||
val syncHelper = CowspentSQLiteOpenHelper.getInstance(applicationContext).cowspentServerSyncHelper
|
|
||||||
syncHelper.restoreAccountProjects(projects)
|
|
||||||
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
|
|
||||||
syncHelper.runAccountProjectsSync()
|
|
||||||
}
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
showToast(this, getString(R.string.settings_restore_deleted_projects_done))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -84,14 +59,13 @@ class PreferencesActivity : AppCompatActivity() {
|
|||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
onBack = { NavUtils.navigateUpFromSameTask(this) },
|
onBack = { NavUtils.navigateUpFromSameTask(this) },
|
||||||
onAccountSettingsClick = {
|
onAccountSettingsClick = {
|
||||||
accountLauncher.launch(Intent(this, AccountActivity::class.java))
|
startActivity(Intent(this, AccountActivity::class.java))
|
||||||
},
|
},
|
||||||
onAboutClick = {
|
onAboutClick = {
|
||||||
startActivity(Intent(this, AboutActivity::class.java))
|
startActivity(Intent(this, AboutActivity::class.java))
|
||||||
},
|
},
|
||||||
onColorSelected = { appColor = it },
|
onColorSelected = { appColor = it },
|
||||||
onNightModeChanged = { nightMode = it },
|
onNightModeChanged = { nightMode = it }
|
||||||
onRestoreDeletedProjects = { restoreDeletedProjects(it) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import androidx.compose.material.MaterialTheme
|
|||||||
import androidx.compose.material.RadioButton
|
import androidx.compose.material.RadioButton
|
||||||
import androidx.compose.material.Scaffold
|
import androidx.compose.material.Scaffold
|
||||||
import androidx.compose.material.Slider
|
import androidx.compose.material.Slider
|
||||||
import androidx.compose.material.Checkbox
|
|
||||||
import androidx.compose.material.Switch
|
import androidx.compose.material.Switch
|
||||||
import androidx.compose.material.SwitchDefaults
|
import androidx.compose.material.SwitchDefaults
|
||||||
import androidx.compose.material.Text
|
import androidx.compose.material.Text
|
||||||
@@ -35,14 +34,11 @@ import androidx.compose.material.icons.filled.Brightness2
|
|||||||
import androidx.compose.material.icons.filled.Group
|
import androidx.compose.material.icons.filled.Group
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.Palette
|
import androidx.compose.material.icons.filled.Palette
|
||||||
import androidx.compose.material.icons.filled.Restore
|
|
||||||
import androidx.compose.material.icons.filled.Sync
|
import androidx.compose.material.icons.filled.Sync
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
@@ -51,7 +47,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.pluralStringResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -61,9 +56,6 @@ import androidx.core.content.edit
|
|||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
import net.helcel.cowspent.android.helper.ColorPicker
|
import net.helcel.cowspent.android.helper.ColorPicker
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.helcel.cowspent.model.DBAccountProject
|
|
||||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||||
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
|
||||||
import net.helcel.cowspent.util.ColorUtils
|
import net.helcel.cowspent.util.ColorUtils
|
||||||
@@ -76,8 +68,7 @@ fun SettingsScreen(
|
|||||||
onAccountSettingsClick: () -> Unit,
|
onAccountSettingsClick: () -> Unit,
|
||||||
onAboutClick: () -> Unit,
|
onAboutClick: () -> Unit,
|
||||||
onColorSelected: (Int) -> Unit,
|
onColorSelected: (Int) -> Unit,
|
||||||
onNightModeChanged: (String) -> Unit = {},
|
onNightModeChanged: (String) -> Unit = {}
|
||||||
onRestoreDeletedProjects: (List<DBAccountProject>) -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||||
@@ -93,24 +84,14 @@ fun SettingsScreen(
|
|||||||
val keyColor = stringResource(R.string.pref_key_color)
|
val keyColor = stringResource(R.string.pref_key_color)
|
||||||
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
|
val keyOfflineMode = stringResource(R.string.pref_key_offline_mode)
|
||||||
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
|
val keyShowArchived = stringResource(R.string.pref_key_show_archived)
|
||||||
val keyExtraFeatures = stringResource(R.string.pref_key_extra_features)
|
val keyBetaFeatures = stringResource(R.string.pref_key_beta_features)
|
||||||
val keyStatsIncludeDeactivated = stringResource(R.string.pref_key_stats_include_deactivated)
|
val keyStatsIncludeDeactivated = stringResource(R.string.pref_key_stats_include_deactivated)
|
||||||
val keyFullSyncDelay = stringResource(R.string.pref_key_full_sync_delay)
|
val keyAutoSyncOnOpen = stringResource(R.string.pref_key_auto_sync_on_open)
|
||||||
val keyFillNewBillFromLast = stringResource(R.string.pref_key_fill_new_bill_from_last)
|
val keyFillNewBillFromLast = stringResource(R.string.pref_key_fill_new_bill_from_last)
|
||||||
val keyLastAccountSync = stringResource(R.string.pref_key_last_account_sync_timestamp)
|
val keyLastAccountSync = stringResource(R.string.pref_key_last_account_sync_timestamp)
|
||||||
|
|
||||||
val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context)
|
val isNextcloudConfigured = CowspentServerSyncHelper.isNextcloudAccountConfigured(context)
|
||||||
|
|
||||||
var deletedProjects by remember { mutableStateOf(emptyList<DBAccountProject>()) }
|
|
||||||
var showRestoreDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
// Off the main thread: this reads the account projects table.
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
deletedProjects = withContext(Dispatchers.IO) {
|
|
||||||
CowspentSQLiteOpenHelper.getInstance(context).cowspentServerSyncHelper.deletedAccountProjects()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// States for preferences
|
// States for preferences
|
||||||
var nightMode by remember(keyNightMode) {
|
var nightMode by remember(keyNightMode) {
|
||||||
mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1")
|
mutableStateOf(sharedPreferences.getString(keyNightMode, "-1") ?: "-1")
|
||||||
@@ -147,8 +128,8 @@ fun SettingsScreen(
|
|||||||
var showArchived by remember(keyShowArchived) {
|
var showArchived by remember(keyShowArchived) {
|
||||||
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyShowArchived, false))
|
||||||
}
|
}
|
||||||
var extraFeatures by remember(keyExtraFeatures) {
|
var betaFeatures by remember(keyBetaFeatures) {
|
||||||
mutableStateOf(sharedPreferences.getBoolean(keyExtraFeatures, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyBetaFeatures, false))
|
||||||
}
|
}
|
||||||
var fillNewBillFromLast by remember(keyFillNewBillFromLast) {
|
var fillNewBillFromLast by remember(keyFillNewBillFromLast) {
|
||||||
mutableStateOf(sharedPreferences.getBoolean(keyFillNewBillFromLast, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyFillNewBillFromLast, false))
|
||||||
@@ -157,16 +138,16 @@ fun SettingsScreen(
|
|||||||
mutableStateOf(sharedPreferences.getBoolean(keyStatsIncludeDeactivated, false))
|
mutableStateOf(sharedPreferences.getBoolean(keyStatsIncludeDeactivated, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
val fullSyncDelays = SyncSettings.FULL_SYNC_DELAY_CHOICES_MINUTES
|
val syncIntervals = SyncSettings.INTERVAL_CHOICES_MINUTES
|
||||||
val fullSyncDelayLabels = listOf(
|
val syncIntervalLabels = listOf(
|
||||||
stringResource(R.string.pref_value_sync_always),
|
stringResource(R.string.pref_value_sync_1m),
|
||||||
|
stringResource(R.string.pref_value_sync_10m),
|
||||||
stringResource(R.string.pref_value_sync_1h),
|
stringResource(R.string.pref_value_sync_1h),
|
||||||
stringResource(R.string.pref_value_sync_1d),
|
stringResource(R.string.pref_value_sync_1d)
|
||||||
stringResource(R.string.pref_value_sync_1w)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var fullSyncDelay by remember(keyFullSyncDelay) {
|
var syncInterval by remember(keyAutoSyncOnOpen) {
|
||||||
mutableIntStateOf(SyncSettings.fullSyncDelayMinutes(context))
|
mutableIntStateOf(SyncSettings.intervalMinutes(context))
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -192,6 +173,28 @@ fun SettingsScreen(
|
|||||||
// Appearance
|
// Appearance
|
||||||
SettingsCategory(stringResource(R.string.settings_appearance))
|
SettingsCategory(stringResource(R.string.settings_appearance))
|
||||||
|
|
||||||
|
SettingsSwitchPreference(
|
||||||
|
title = stringResource(R.string.settings_show_archived),
|
||||||
|
icon = Icons.Default.Archive,
|
||||||
|
checked = showArchived,
|
||||||
|
onCheckedChange = {
|
||||||
|
showArchived = it
|
||||||
|
sharedPreferences.edit {
|
||||||
|
putBoolean(keyShowArchived, it)
|
||||||
|
if (!it) {
|
||||||
|
val selectedProjectId = sharedPreferences.getLong("selected_project", 0)
|
||||||
|
if (selectedProjectId != 0L) {
|
||||||
|
val db = CowspentSQLiteOpenHelper.getInstance(context)
|
||||||
|
val project = db.getProject(selectedProjectId)
|
||||||
|
if (project?.isArchived == true) {
|
||||||
|
putLong("selected_project", 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
SettingsListPreference(
|
SettingsListPreference(
|
||||||
title = stringResource(R.string.settings_night_mode),
|
title = stringResource(R.string.settings_night_mode),
|
||||||
icon = Icons.Default.Brightness2,
|
icon = Icons.Default.Brightness2,
|
||||||
@@ -276,71 +279,23 @@ fun SettingsScreen(
|
|||||||
onClick = onAccountSettingsClick
|
onClick = onAccountSettingsClick
|
||||||
)
|
)
|
||||||
|
|
||||||
// Nothing held back means nothing to undo, so the row is not offered at all.
|
|
||||||
if (deletedProjects.isNotEmpty()) {
|
|
||||||
SettingsPreference(
|
|
||||||
title = stringResource(R.string.settings_restore_deleted_projects),
|
|
||||||
summary = pluralStringResource(
|
|
||||||
R.plurals.settings_restore_deleted_projects_summary,
|
|
||||||
deletedProjects.size,
|
|
||||||
deletedProjects.size
|
|
||||||
),
|
|
||||||
icon = Icons.Default.Restore,
|
|
||||||
onClick = { showRestoreDialog = true }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showRestoreDialog) {
|
|
||||||
RestoreDeletedProjectsDialog(
|
|
||||||
projects = deletedProjects,
|
|
||||||
onDismiss = { showRestoreDialog = false },
|
|
||||||
onConfirm = { chosen ->
|
|
||||||
showRestoreDialog = false
|
|
||||||
onRestoreDeletedProjects(chosen)
|
|
||||||
deletedProjects = deletedProjects - chosen.toSet()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other
|
// Other
|
||||||
SettingsCategory(stringResource(R.string.settings_other))
|
SettingsCategory(stringResource(R.string.settings_other))
|
||||||
|
|
||||||
SettingsSwitchPreference(
|
SettingsSwitchPreference(
|
||||||
title = stringResource(R.string.settings_extra_features),
|
title = stringResource(R.string.settings_beta_features),
|
||||||
summary = stringResource(R.string.settings_extra_features_summary),
|
summary = stringResource(R.string.settings_beta_features_summary),
|
||||||
icon = Icons.Default.Info,
|
icon = Icons.Default.Info,
|
||||||
checked = extraFeatures,
|
checked = betaFeatures,
|
||||||
onCheckedChange = {
|
onCheckedChange = {
|
||||||
extraFeatures = it
|
betaFeatures = it
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putBoolean(keyExtraFeatures, it)
|
putBoolean(keyBetaFeatures, it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (extraFeatures) {
|
if (betaFeatures) {
|
||||||
SettingsSwitchPreference(
|
|
||||||
title = stringResource(R.string.settings_show_archived),
|
|
||||||
icon = Icons.Default.Archive,
|
|
||||||
checked = showArchived,
|
|
||||||
onCheckedChange = {
|
|
||||||
showArchived = it
|
|
||||||
sharedPreferences.edit {
|
|
||||||
putBoolean(keyShowArchived, it)
|
|
||||||
if (!it) {
|
|
||||||
val selectedProjectId = sharedPreferences.getLong("selected_project", 0)
|
|
||||||
if (selectedProjectId != 0L) {
|
|
||||||
val db = CowspentSQLiteOpenHelper.getInstance(context)
|
|
||||||
val project = db.getProject(selectedProjectId)
|
|
||||||
if (project?.isArchived == true) {
|
|
||||||
putLong("selected_project", 0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
SettingsSwitchPreference(
|
SettingsSwitchPreference(
|
||||||
title = stringResource(R.string.settings_fill_new_bill_from_last),
|
title = stringResource(R.string.settings_fill_new_bill_from_last),
|
||||||
summary = stringResource(R.string.settings_fill_new_bill_from_last_summary),
|
summary = stringResource(R.string.settings_fill_new_bill_from_last_summary),
|
||||||
@@ -367,18 +322,18 @@ fun SettingsScreen(
|
|||||||
)
|
)
|
||||||
|
|
||||||
SettingsSliderPreference(
|
SettingsSliderPreference(
|
||||||
title = stringResource(R.string.settings_full_sync_delay),
|
title = stringResource(R.string.settings_auto_sync_on_open),
|
||||||
summary = stringResource(R.string.settings_full_sync_delay_summary),
|
summary = stringResource(R.string.settings_auto_sync_on_open_summary),
|
||||||
icon = Icons.Default.Sync,
|
icon = Icons.Default.Sync,
|
||||||
value = fullSyncDelay,
|
value = syncInterval,
|
||||||
values = fullSyncDelays,
|
values = syncIntervals,
|
||||||
labels = fullSyncDelayLabels,
|
labels = syncIntervalLabels,
|
||||||
onValueChange = { newDelay ->
|
onValueChange = { newInterval ->
|
||||||
// Slider reports every drag delta, not just the snapped steps.
|
// Slider reports every drag delta, not just the snapped steps.
|
||||||
if (newDelay != fullSyncDelay) {
|
if (newInterval != syncInterval) {
|
||||||
fullSyncDelay = newDelay
|
syncInterval = newInterval
|
||||||
sharedPreferences.edit {
|
sharedPreferences.edit {
|
||||||
putInt(keyFullSyncDelay, newDelay)
|
putInt(keyAutoSyncOnOpen, newInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,60 +349,6 @@ fun SettingsScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun RestoreDeletedProjectsDialog(
|
|
||||||
projects: List<DBAccountProject>,
|
|
||||||
onDismiss: () -> Unit,
|
|
||||||
onConfirm: (List<DBAccountProject>) -> Unit
|
|
||||||
) {
|
|
||||||
val selected = remember(projects) { mutableStateListOf<DBAccountProject>().apply { addAll(projects) } }
|
|
||||||
|
|
||||||
AlertDialog(
|
|
||||||
onDismissRequest = onDismiss,
|
|
||||||
title = { Text(stringResource(R.string.settings_restore_deleted_projects)) },
|
|
||||||
text = {
|
|
||||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
|
||||||
projects.forEach { project ->
|
|
||||||
val isSelected = project in selected
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.selectable(
|
|
||||||
selected = isSelected,
|
|
||||||
onClick = {
|
|
||||||
if (isSelected) selected.remove(project) else selected.add(project)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Checkbox(checked = isSelected, onCheckedChange = null)
|
|
||||||
Spacer(Modifier.width(16.dp))
|
|
||||||
Column {
|
|
||||||
Text(
|
|
||||||
text = project.name.ifEmpty { project.remoteId },
|
|
||||||
style = MaterialTheme.typography.subtitle1
|
|
||||||
)
|
|
||||||
Text(text = project.ncUrl, style = MaterialTheme.typography.caption)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = { onConfirm(selected.toList()) },
|
|
||||||
enabled = selected.isNotEmpty()
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.settings_restore_deleted_projects_confirm))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
dismissButton = {
|
|
||||||
TextButton(onClick = onDismiss) { Text(stringResource(R.string.simple_cancel)) }
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsCategory(title: String) {
|
fun SettingsCategory(title: String) {
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ContentValues
|
import android.content.ContentValues
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.database.DatabaseUtils
|
|
||||||
import android.database.sqlite.SQLiteDatabase
|
import android.database.sqlite.SQLiteDatabase
|
||||||
import android.database.sqlite.SQLiteOpenHelper
|
import android.database.sqlite.SQLiteOpenHelper
|
||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
@@ -239,7 +238,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBProject> {
|
private fun getProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBProject> {
|
||||||
return queryAll(db, table_projects, columnsProjects, selection, selectionArgs, orderBy, ::getProjectFromCursor)
|
val cursor = db.query(table_projects, columnsProjects, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val projects: MutableList<DBProject> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
projects.add(getProjectFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return projects
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -300,17 +305,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
fun deleteProject(id: Long) {
|
fun deleteProject(id: Long) {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
val projectId = arrayOf(id.toString())
|
for (b in getBillsOfProject(id)) {
|
||||||
inTransaction {
|
deleteBill(b.id)
|
||||||
db.delete(
|
|
||||||
table_billowers,
|
|
||||||
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $key_projectid = ?)",
|
|
||||||
projectId
|
|
||||||
)
|
|
||||||
db.delete(table_bills, "$key_projectid = ?", projectId)
|
|
||||||
db.delete(table_members, "$key_projectid = ?", projectId)
|
|
||||||
db.delete(table_projects, "$key_id = ?", projectId)
|
|
||||||
}
|
}
|
||||||
|
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
|
||||||
|
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
|
||||||
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
|
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,7 +413,14 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getMembersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBMember> {
|
private fun getMembersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBMember> {
|
||||||
return queryAll(readableDatabase, table_members, columnsMembers, selection, selectionArgs, orderBy, ::getMemberFromCursor)
|
val db = readableDatabase
|
||||||
|
val cursor = db.query(table_members, columnsMembers, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val members: MutableList<DBMember> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
members.add(getMemberFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return members
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -442,26 +448,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
// --- Bills logic ---
|
// --- Bills logic ---
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs [block] as one database transaction.
|
|
||||||
*
|
|
||||||
* Every write here commits on its own otherwise, which for a sync means a committed
|
|
||||||
* transaction per bill and another per ower - the dominant cost of a large project's first
|
|
||||||
* sync. Callers batch in chunks rather than wrapping everything: a sync interrupted halfway
|
|
||||||
* then keeps the chunks it already committed instead of rolling the lot back.
|
|
||||||
*/
|
|
||||||
fun <T> inTransaction(block: () -> T): T {
|
|
||||||
val db = writableDatabase
|
|
||||||
db.beginTransaction()
|
|
||||||
try {
|
|
||||||
val result = block()
|
|
||||||
db.setTransactionSuccessful()
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
db.endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBill(b: DBBill): Long {
|
fun addBill(b: DBBill): Long {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
val values = ContentValues()
|
val values = ContentValues()
|
||||||
@@ -555,14 +541,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
return getBillsCustom("$key_projectid = ?", arrayOf(projId.toString()), "$key_timestamp ASC")
|
return getBillsCustom("$key_projectid = ?", arrayOf(projId.toString()), "$key_timestamp ASC")
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun countUnsyncedBills(projId: Long): Int = DatabaseUtils.queryNumEntries(
|
|
||||||
readableDatabase,
|
|
||||||
table_bills,
|
|
||||||
"$key_projectid = ? AND $key_state != ?",
|
|
||||||
arrayOf(projId.toString(), DBBill.STATE_OK.toString())
|
|
||||||
).toInt()
|
|
||||||
|
|
||||||
fun getBillsOfProjectWithState(projId: Long, state: Int): List<DBBill> {
|
fun getBillsOfProjectWithState(projId: Long, state: Int): List<DBBill> {
|
||||||
return getBillsCustom(
|
return getBillsCustom(
|
||||||
"$key_projectid = ? AND $key_state = ?",
|
"$key_projectid = ? AND $key_state = ?",
|
||||||
@@ -646,20 +624,15 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getBillsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBill> {
|
private fun getBillsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBill> {
|
||||||
val db = readableDatabase
|
val db = readableDatabase
|
||||||
return db.transactionally {
|
val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy)
|
||||||
val bills = queryAll(db, table_bills, columnsBills, selection, selectionArgs, orderBy, ::getBillFromCursor)
|
val bills: MutableList<DBBill> = ArrayList()
|
||||||
if (bills.isNotEmpty()) {
|
while (cursor.moveToNext()) {
|
||||||
val owersByBill = queryAll(
|
val bill = getBillFromCursor(cursor)
|
||||||
db, table_billowers, columnsBillowers,
|
bill.billOwers = getBillowersOfBill(bill.id)
|
||||||
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $selection)",
|
bills.add(bill)
|
||||||
selectionArgs, null, ::getBillOwerFromCursor
|
|
||||||
).groupBy { it.billId }
|
|
||||||
for (bill in bills) {
|
|
||||||
bill.billOwers = owersByBill[bill.id].orEmpty()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
bills
|
|
||||||
}
|
}
|
||||||
|
cursor.close()
|
||||||
|
return bills
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -707,7 +680,14 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getBillOwersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBillOwer> {
|
private fun getBillOwersCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBBillOwer> {
|
||||||
return queryAll(readableDatabase, table_billowers, columnsBillowers, selection, selectionArgs, orderBy, ::getBillOwerFromCursor)
|
val db = readableDatabase
|
||||||
|
val cursor = db.query(table_billowers, columnsBillowers, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val billOwers: MutableList<DBBillOwer> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
billOwers.add(getBillOwerFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return billOwers
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -774,7 +754,14 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> {
|
private fun getCategoriesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?): List<DBCategory> {
|
||||||
return queryAll(readableDatabase, table_categories, columnsCategories, selection, selectionArgs, orderBy, ::getCategoryFromCursor)
|
val db = readableDatabase
|
||||||
|
val cursor = db.query(table_categories, columnsCategories, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val categories: MutableList<DBCategory> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
categories.add(getCategoryFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return categories
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -883,7 +870,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> {
|
private fun getPaymentModesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBPaymentMode> {
|
||||||
return queryAll(db, table_payment_modes, columnsPaymentModes, selection, selectionArgs, orderBy, ::getPaymentModeFromCursor)
|
val cursor = db.query(table_payment_modes, columnsPaymentModes, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val paymentModes: MutableList<DBPaymentMode> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
paymentModes.add(getPaymentModeFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return paymentModes
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -993,7 +986,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> {
|
private fun getCurrenciesCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBCurrency> {
|
||||||
return queryAll(db, table_currencies, columnsCurrencies, selection, selectionArgs, orderBy, ::getCurrencyFromCursor)
|
val cursor = db.query(table_currencies, columnsCurrencies, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val currencies: MutableList<DBCurrency> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
currencies.add(getCurrencyFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return currencies
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
@@ -1041,36 +1040,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
// --- Common Helpers ---
|
// --- Common Helpers ---
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
private fun <T> queryAll(
|
|
||||||
db: SQLiteDatabase,
|
|
||||||
table: String,
|
|
||||||
columns: Array<String>,
|
|
||||||
selection: String?,
|
|
||||||
selectionArgs: Array<String>?,
|
|
||||||
orderBy: String?,
|
|
||||||
fromCursor: (Cursor) -> T
|
|
||||||
): List<T> = db.transactionally {
|
|
||||||
val items: MutableList<T> = ArrayList()
|
|
||||||
db.query(table, columns, selection, selectionArgs, null, null, orderBy).use { cursor ->
|
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
items.add(fromCursor(cursor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
items
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> SQLiteDatabase.transactionally(block: () -> T): T {
|
|
||||||
beginTransaction()
|
|
||||||
try {
|
|
||||||
val result = block()
|
|
||||||
setTransactionSuccessful()
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun syncIfRemote(proj: DBProject) {
|
fun syncIfRemote(proj: DBProject) {
|
||||||
if (!proj.isLocal) {
|
if (!proj.isLocal) {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
@@ -1143,7 +1112,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> {
|
private fun getAccountProjectsCustom(selection: String, selectionArgs: Array<String>, orderBy: String?, db: SQLiteDatabase): List<DBAccountProject> {
|
||||||
return queryAll(db, table_account_projects, columnsAccountProjects, selection, selectionArgs, orderBy, ::getAccountProjectFromCursor)
|
val cursor = db.query(table_account_projects, columnsAccountProjects, selection, selectionArgs, null, null, orderBy)
|
||||||
|
val accountProjects: MutableList<DBAccountProject> = ArrayList()
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
accountProjects.add(getAccountProjectFromCursor(cursor))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return accountProjects
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("Range")
|
@SuppressLint("Range")
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.content.SharedPreferences
|
|||||||
import android.net.ConnectivityManager
|
import android.net.ConnectivityManager
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.annotation.WorkerThread
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.preference.PreferenceManager
|
import androidx.preference.PreferenceManager
|
||||||
@@ -15,7 +14,6 @@ import com.nextcloud.android.sso.api.NextcloudAPI
|
|||||||
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
|
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
|
||||||
import com.nextcloud.android.sso.exceptions.TokenMismatchException
|
import com.nextcloud.android.sso.exceptions.TokenMismatchException
|
||||||
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
||||||
import kotlinx.coroutines.CancellationException
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -28,7 +26,6 @@ import net.helcel.cowspent.R
|
|||||||
import net.helcel.cowspent.android.account.AccountActivity
|
import net.helcel.cowspent.android.account.AccountActivity
|
||||||
import net.helcel.cowspent.android.main.BillsListViewActivity
|
import net.helcel.cowspent.android.main.BillsListViewActivity
|
||||||
import net.helcel.cowspent.android.main.MainConstants
|
import net.helcel.cowspent.android.main.MainConstants
|
||||||
import net.helcel.cowspent.model.DBAccountProject
|
|
||||||
import net.helcel.cowspent.model.DBBill
|
import net.helcel.cowspent.model.DBBill
|
||||||
import net.helcel.cowspent.model.DBMember
|
import net.helcel.cowspent.model.DBMember
|
||||||
import net.helcel.cowspent.model.DBProject
|
import net.helcel.cowspent.model.DBProject
|
||||||
@@ -53,6 +50,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
private var syncActive = false
|
private var syncActive = false
|
||||||
private var syncAccountProjectsActive = false
|
private var syncAccountProjectsActive = false
|
||||||
|
|
||||||
|
private var callbacksPush: MutableList<ICallback> = ArrayList()
|
||||||
private var callbacksPull: MutableList<ICallback> = ArrayList()
|
private var callbacksPull: MutableList<ICallback> = ArrayList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -103,6 +101,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
if (project != null) {
|
if (project != null) {
|
||||||
Log.d(TAG, "... starting now")
|
Log.d(TAG, "... starting now")
|
||||||
val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
|
val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
|
||||||
|
syncTask.addCallbacks(callbacksPush)
|
||||||
|
callbacksPush = ArrayList()
|
||||||
if (!onlyLocalChanges) {
|
if (!onlyLocalChanges) {
|
||||||
syncTask.addCallbacks(callbacksPull)
|
syncTask.addCallbacks(callbacksPull)
|
||||||
callbacksPull = ArrayList()
|
callbacksPull = ArrayList()
|
||||||
@@ -114,8 +114,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
} else if (!onlyLocalChanges) {
|
} else if (!onlyLocalChanges) {
|
||||||
Log.d(TAG, "... scheduled")
|
Log.d(TAG, "... scheduled")
|
||||||
projectIdsToSync.add(projId)
|
projectIdsToSync.add(projId)
|
||||||
|
for (callback in callbacksPush) {
|
||||||
|
callback.onScheduled()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "... do nothing")
|
Log.d(TAG, "... do nothing")
|
||||||
|
for (callback in callbacksPush) {
|
||||||
|
callback.onScheduled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -147,12 +153,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
* walk it is empty, and nothing may be deleted locally on the strength of it.
|
* walk it is empty, and nothing may be deleted locally on the strength of it.
|
||||||
*/
|
*/
|
||||||
val allIds: List<Long>,
|
val allIds: List<Long>,
|
||||||
val syncTimestamp: Long?,
|
val syncTimestamp: Long?
|
||||||
/**
|
|
||||||
* A bill referenced a member, category or payment mode this device does not know. A
|
|
||||||
* partial sync skips re-reading the project, so this is what tells it to go back for it.
|
|
||||||
*/
|
|
||||||
val hasUnresolvedReferences: Boolean = false
|
|
||||||
)
|
)
|
||||||
|
|
||||||
inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject, private val forceFullSync: Boolean = false) {
|
inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject, private val forceFullSync: Boolean = false) {
|
||||||
@@ -179,22 +180,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun execute(): SyncTask {
|
fun execute(): SyncTask {
|
||||||
syncActive = true
|
|
||||||
deferred = scope.async {
|
deferred = scope.async {
|
||||||
try {
|
syncActive = true
|
||||||
val status = withContext(Dispatchers.IO) {
|
val status = withContext(Dispatchers.IO) {
|
||||||
doWork()
|
doWork()
|
||||||
}
|
|
||||||
onPostExecute(status)
|
|
||||||
status
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
syncActive = false
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Sync failed for ${project.remoteId}", e)
|
|
||||||
syncActive = false
|
|
||||||
LoginStatus.CONNECTION_FAILED
|
|
||||||
}
|
}
|
||||||
|
onPostExecute(status)
|
||||||
|
syncActive = false
|
||||||
|
status
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -204,18 +197,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
if (project.type == ProjectType.COSPEND) {
|
if (project.type == ProjectType.COSPEND) {
|
||||||
nextcloudClient = createNextcloudClient()
|
nextcloudClient = createNextcloudClient()
|
||||||
if (nextcloudClient != null) {
|
if (nextcloudClient != null) {
|
||||||
// The server's version only moves when the server itself is upgraded, and
|
try {
|
||||||
// asking costs a whole round trip before any project data moves. Remember it
|
val response = nextcloudClient!!.getCapabilities(project)
|
||||||
// per server, and re-read it whenever a full sync is asked for.
|
version = response.cospendVersion
|
||||||
version = if (forceFullSync) null else cachedCospendVersion()
|
} catch (e: Exception) {
|
||||||
if (version == null) {
|
Log.i(TAG, "Failed to get cospend version when syncing: $e")
|
||||||
try {
|
|
||||||
val response = nextcloudClient!!.getCapabilities(project)
|
|
||||||
version = response.cospendVersion
|
|
||||||
rememberCospendVersion(version)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.i(TAG, "Failed to get cospend version when syncing: $e")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) {
|
} else if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) {
|
||||||
return LoginStatus.SSO_TOKEN_MISMATCH
|
return LoginStatus.SSO_TOKEN_MISMATCH
|
||||||
@@ -241,53 +227,35 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
deferred?.await() ?: LoginStatus.CONNECTION_FAILED
|
deferred?.await() ?: LoginStatus.CONNECTION_FAILED
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Keyed by server rather than by project, since several projects can share one. */
|
|
||||||
private fun versionKey() = "cospend_version_" + (project.serverUrl ?: "")
|
|
||||||
|
|
||||||
private fun cachedCospendVersion(): String? =
|
|
||||||
preferences.getString(versionKey(), null)
|
|
||||||
|
|
||||||
private fun rememberCospendVersion(version: String?) {
|
|
||||||
// A failed lookup is not an answer, and caching it would pin the wrong API dialect
|
|
||||||
// until the next forced sync.
|
|
||||||
if (version.isNullOrEmpty()) return
|
|
||||||
preferences.edit { putString(versionKey(), version) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun pushLocalChanges(): LoginStatus {
|
private fun pushLocalChanges(): LoginStatus {
|
||||||
Log.d(TAG, "PUSH LOCAL CHANGES")
|
Log.d(TAG, "PUSH LOCAL CHANGES")
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
// The remote member list is only needed to match members waiting to be created,
|
val membersResponse = client!!.getMembers(project)
|
||||||
// and pullRemoteChanges fetches them again as part of the project anyway, so it
|
val remoteMembers = membersResponse.getMembers(project.id)
|
||||||
// is not worth a round trip of its own when there is nothing to add.
|
val remoteMembersNames = remoteMembers.map { it.name }
|
||||||
val membersToAdd = dbHelper.getMembersOfProjectWithState(project.id, DBBill.STATE_ADDED)
|
|
||||||
if (membersToAdd.isNotEmpty()) {
|
|
||||||
val membersResponse = client!!.getMembers(project)
|
|
||||||
val remoteMembers = membersResponse.getMembers(project.id)
|
|
||||||
val remoteMembersNames = remoteMembers.map { it.name }
|
|
||||||
|
|
||||||
for (mToAdd in membersToAdd) {
|
val membersToAdd = dbHelper.getMembersOfProjectWithState(project.id, DBBill.STATE_ADDED)
|
||||||
val searchIndex = remoteMembersNames.indexOf(mToAdd.name)
|
for (mToAdd in membersToAdd) {
|
||||||
if (searchIndex != -1) {
|
val searchIndex = remoteMembersNames.indexOf(mToAdd.name)
|
||||||
val remoteMember = remoteMembers[searchIndex]
|
if (searchIndex != -1) {
|
||||||
|
val remoteMember = remoteMembers[searchIndex]
|
||||||
|
dbHelper.updateMember(
|
||||||
|
mToAdd.id, null,
|
||||||
|
remoteMember.weight, remoteMember.isActivated,
|
||||||
|
DBBill.STATE_OK, remoteMember.remoteId, remoteMember.r,
|
||||||
|
remoteMember.g, remoteMember.b,
|
||||||
|
remoteMember.ncUserId, ""
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
val createRemoteMemberResponse = client!!.createRemoteMember(project, mToAdd)
|
||||||
|
val newRemoteId = createRemoteMemberResponse.remoteMemberId
|
||||||
|
if (newRemoteId > 0) {
|
||||||
dbHelper.updateMember(
|
dbHelper.updateMember(
|
||||||
mToAdd.id, null,
|
mToAdd.id, null,
|
||||||
remoteMember.weight, remoteMember.isActivated,
|
null, null, DBBill.STATE_OK, newRemoteId,
|
||||||
DBBill.STATE_OK, remoteMember.remoteId, remoteMember.r,
|
null, null, null, null, null
|
||||||
remoteMember.g, remoteMember.b,
|
|
||||||
remoteMember.ncUserId, ""
|
|
||||||
)
|
)
|
||||||
} else {
|
|
||||||
val createRemoteMemberResponse = client!!.createRemoteMember(project, mToAdd)
|
|
||||||
val newRemoteId = createRemoteMemberResponse.remoteMemberId
|
|
||||||
if (newRemoteId > 0) {
|
|
||||||
dbHelper.updateMember(
|
|
||||||
mToAdd.id, null,
|
|
||||||
null, null, DBBill.STATE_OK, newRemoteId,
|
|
||||||
null, null, null, null, null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -304,8 +272,12 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
null, null, null, null, null
|
null, null, null, null, null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: IOException) {
|
||||||
Log.e(TAG, "EDIT MEMBER FAILED for ${mToEdit.name}", e)
|
if (e.message == "{\"message\": \"Internal Server Error\"}") {
|
||||||
|
Log.d(TAG, "EDIT MEMBER FAILED : it does not exist remotely")
|
||||||
|
} else {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -576,54 +548,33 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Reads the project's own row and each of its collections. One request, and everything it
|
|
||||||
* carries is applied as it goes, so a failure part way through leaves the earlier steps
|
|
||||||
* written. Returns the members it saw, keyed by remote id.
|
|
||||||
*/
|
|
||||||
private fun pullProject(): Map<Long, DBMember> {
|
|
||||||
val projResponse = client!!.getProject(project, 0, null)
|
|
||||||
updateLocalProject(projResponse)
|
|
||||||
syncPaymentModes(projResponse)
|
|
||||||
syncCategories(projResponse)
|
|
||||||
syncCurrencies(projResponse)
|
|
||||||
return syncMembers(projResponse)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Brings the local copy of the project in line with the server: its own row, then each of
|
* Brings the local copy of the project in line with the server: its own row, then each of
|
||||||
* its collections, then its bills. Each step is applied as it goes, so a failure part way
|
* its collections, then its bills. Each step is applied as it goes, so a failure part way
|
||||||
* through leaves the earlier steps written - the next sync picks up where this one stopped.
|
* through leaves the earlier steps written - the next sync picks up where this one stopped.
|
||||||
*
|
|
||||||
* A partial sync is meant to cost one request. A project's members and labels change far
|
|
||||||
* less often than its bills, so it skips reading them and goes straight for the bills; it
|
|
||||||
* only goes back for the project when a bill turns out to name something this device has
|
|
||||||
* never seen. What that trades away is noticing a rename or a deletion that no bill
|
|
||||||
* refers to - those land on the next full sync.
|
|
||||||
*/
|
*/
|
||||||
private fun pullRemoteChanges(): LoginStatus {
|
private fun pullRemoteChanges(): LoginStatus {
|
||||||
Log.d(TAG, "pullRemoteChanges($project)")
|
Log.d(TAG, "pullRemoteChanges($project)")
|
||||||
return try {
|
return try {
|
||||||
val localBills = dbHelper.getBillsOfProject(project.id)
|
val projResponse = client!!.getProject(project, 0, null)
|
||||||
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
|
|
||||||
val partial = !forceFullSync && localBillsByRemoteId.isNotEmpty()
|
|
||||||
|
|
||||||
var remoteMembersByRemoteId: Map<Long, DBMember>? = null
|
updateLocalProject(projResponse)
|
||||||
if (!partial) remoteMembersByRemoteId = pullProject()
|
syncPaymentModes(projResponse)
|
||||||
|
syncCategories(projResponse)
|
||||||
|
syncCurrencies(projResponse)
|
||||||
|
val remoteMembersByRemoteId = syncMembers(projResponse)
|
||||||
|
|
||||||
// Bills arrive with the server's ids for their member, category and payment mode,
|
// Bills arrive with the server's ids for their member, category and payment mode,
|
||||||
// so the maps have to be built after those collections are in place.
|
// so the maps have to be built after those collections are in place.
|
||||||
var pulled = fetchRemoteBills(buildRemoteIdMaps(), localBillsByRemoteId)
|
val idMaps = buildRemoteIdMaps()
|
||||||
|
|
||||||
if (pulled.hasUnresolvedReferences && remoteMembersByRemoteId == null) {
|
val localBills = dbHelper.getBillsOfProject(project.id)
|
||||||
Log.d(TAG, "Bill referenced an unknown member or label; re-reading the project")
|
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
|
||||||
remoteMembersByRemoteId = pullProject()
|
val pulled = fetchRemoteBills(idMaps, localBillsByRemoteId)
|
||||||
pulled = fetchRemoteBills(buildRemoteIdMaps(), localBillsByRemoteId)
|
|
||||||
}
|
|
||||||
|
|
||||||
applyRemoteBills(pulled.bills, localBillsByRemoteId)
|
applyRemoteBills(pulled.bills, localBillsByRemoteId)
|
||||||
deleteVanishedBills(pulled, localBills)
|
deleteVanishedBills(pulled, localBills)
|
||||||
remoteMembersByRemoteId?.let { deleteVanishedMembers(it) }
|
deleteVanishedMembers(remoteMembersByRemoteId)
|
||||||
|
|
||||||
dbHelper.updateProject(
|
dbHelper.updateProject(
|
||||||
projId = project.id,
|
projId = project.id,
|
||||||
@@ -857,9 +808,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
if (usePagedWalk) {
|
if (usePagedWalk) {
|
||||||
walkBillPages(idMaps, localBillsByRemoteId)?.let { return it }
|
walkBillPages(idMaps, localBillsByRemoteId)?.let { return it }
|
||||||
}
|
}
|
||||||
// A project with nothing stored has no cursor worth sending, so it asks for everything.
|
return fetchAllBills(idMaps)
|
||||||
val since = if (forceFullSync || localBillsByRemoteId.isEmpty()) 0L else null
|
|
||||||
return fetchAllBills(idMaps, since)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -883,7 +832,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
var syncTimestamp = project.lastSyncedTimestamp
|
var syncTimestamp = project.lastSyncedTimestamp
|
||||||
var offset = 0
|
var offset = 0
|
||||||
var previousPageIds: List<Long>? = null
|
var previousPageIds: List<Long>? = null
|
||||||
var unresolved = false
|
|
||||||
// Counted across pages, not restarted at each one: a run that begins near the end of
|
// Counted across pages, not restarted at each one: a run that begins near the end of
|
||||||
// a page still finishes on the next.
|
// a page still finishes on the next.
|
||||||
var unchangedRun = 0
|
var unchangedRun = 0
|
||||||
@@ -893,7 +841,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
val page = response.getBillsCospend(
|
val page = response.getBillsCospend(
|
||||||
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
||||||
)
|
)
|
||||||
if (response.hasUnresolvedReferences) unresolved = true
|
|
||||||
if (page.isEmpty()) break
|
if (page.isEmpty()) break
|
||||||
|
|
||||||
if (page.first().timestamp < page.last().timestamp) {
|
if (page.first().timestamp < page.last().timestamp) {
|
||||||
@@ -933,31 +880,24 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A walk reports no allIds, so it never causes a local deletion.
|
// A walk reports no allIds, so it never causes a local deletion.
|
||||||
return RemoteBills(bills, emptyList(), syncTimestamp, unresolved)
|
return RemoteBills(bills, emptyList(), syncTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun fetchAllBills(idMaps: RemoteIdMaps): RemoteBills {
|
||||||
* One request for the whole collection. [since] 0 asks for everything; the project cursor
|
Log.d(TAG, "Starting full sync for project ${project.remoteId}")
|
||||||
* asks only for what changed. Either way the response carries the full id list, which is
|
val response = client!!.getBills(project)
|
||||||
* what lets vanished bills be removed locally.
|
|
||||||
*/
|
|
||||||
private fun fetchAllBills(idMaps: RemoteIdMaps, since: Long?): RemoteBills {
|
|
||||||
Log.d(TAG, "Fetching bills for ${project.remoteId} (since=${since ?: project.lastSyncedTimestamp})")
|
|
||||||
val response = client!!.getBills(project, since = since)
|
|
||||||
return if (project.type == ProjectType.IHATEMONEY) {
|
return if (project.type == ProjectType.IHATEMONEY) {
|
||||||
val bills = response.getBillsIHM(
|
val bills = response.getBillsIHM(
|
||||||
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
||||||
)
|
)
|
||||||
RemoteBills(bills, bills.map { it.remoteId }, 0L, response.hasUnresolvedReferences)
|
RemoteBills(bills, bills.map { it.remoteId }, 0L)
|
||||||
} else {
|
} else {
|
||||||
val bills = response.getBillsCospend(
|
|
||||||
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
|
||||||
)
|
|
||||||
RemoteBills(
|
RemoteBills(
|
||||||
bills,
|
response.getBillsCospend(
|
||||||
|
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
||||||
|
),
|
||||||
response.allBillIds,
|
response.allBillIds,
|
||||||
response.syncTimestamp,
|
response.syncTimestamp
|
||||||
response.hasUnresolvedReferences
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -965,18 +905,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
private fun applyRemoteBills(
|
private fun applyRemoteBills(
|
||||||
remoteBills: List<DBBill>,
|
remoteBills: List<DBBill>,
|
||||||
localBillsByRemoteId: Map<Long, DBBill>
|
localBillsByRemoteId: Map<Long, DBBill>
|
||||||
) {
|
|
||||||
// Committed in chunks: one transaction per chunk turns a per-row commit into one write
|
|
||||||
// for the whole batch, while still leaving finished chunks on disk if the sync is cut
|
|
||||||
// short - a project part-way through is far better than one that rolled back.
|
|
||||||
remoteBills.chunked(BILL_APPLY_CHUNK).forEach { chunk ->
|
|
||||||
dbHelper.inTransaction { applyBillChunk(chunk, localBillsByRemoteId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyBillChunk(
|
|
||||||
remoteBills: List<DBBill>,
|
|
||||||
localBillsByRemoteId: Map<Long, DBBill>
|
|
||||||
) {
|
) {
|
||||||
for (remoteBill in remoteBills) {
|
for (remoteBill in remoteBills) {
|
||||||
val localBill = localBillsByRemoteId[remoteBill.remoteId]
|
val localBill = localBillsByRemoteId[remoteBill.remoteId]
|
||||||
@@ -1038,10 +966,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (localBill in localBills) {
|
for (localBill in localBills) {
|
||||||
// A bill still waiting to be pushed has no remote id yet, so it is absent from the
|
|
||||||
// server list for the ordinary reason that the server has never seen it. Deleting
|
|
||||||
// it here would throw away a bill the user added while the sync was running.
|
|
||||||
if (localBill.state != DBBill.STATE_OK) continue
|
|
||||||
if (localBill.remoteId !in stillRemote) {
|
if (localBill.remoteId !in stillRemote) {
|
||||||
dbHelper.deleteBill(localBill.id)
|
dbHelper.deleteBill(localBill.id)
|
||||||
nbPulledDeletedBills++
|
nbPulledDeletedBills++
|
||||||
@@ -1055,8 +979,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
for (localMember in dbHelper.getMembersOfProject(project.id, null)) {
|
for (localMember in dbHelper.getMembersOfProject(project.id, null)) {
|
||||||
if (remoteMembersByRemoteId.containsKey(localMember.remoteId)) continue
|
if (remoteMembersByRemoteId.containsKey(localMember.remoteId)) continue
|
||||||
|
|
||||||
if (localMember.state != DBBill.STATE_OK) continue
|
|
||||||
|
|
||||||
// A member still named by a bill cannot be removed without orphaning it.
|
// A member still named by a bill cannot be removed without orphaning it.
|
||||||
if (dbHelper.getBillsOfMember(localMember.id).isEmpty() &&
|
if (dbHelper.getBillsOfMember(localMember.id).isEmpty() &&
|
||||||
dbHelper.getBillowersOfMember(localMember.id).isEmpty()
|
dbHelper.getBillowersOfMember(localMember.id).isEmpty()
|
||||||
@@ -1477,21 +1399,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
|
||||||
fun deletedAccountProjects(): List<DBAccountProject> {
|
|
||||||
val forgotten = forgottenAccountProjects(preferences)
|
|
||||||
if (forgotten.isEmpty()) return emptyList()
|
|
||||||
return dbHelper.accountProjects.filter { accountProjectKey(it.remoteId, it.ncUrl) in forgotten }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun restoreAccountProjects(projects: List<DBAccountProject>) {
|
|
||||||
if (projects.isEmpty()) return
|
|
||||||
val restored = projects.map { accountProjectKey(it.remoteId, it.ncUrl) }.toSet()
|
|
||||||
preferences.edit {
|
|
||||||
putStringSet(FORGOTTEN_ACCOUNT_PROJECTS, forgottenAccountProjects(preferences) - restored)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runAccountProjectsSync() {
|
fun runAccountProjectsSync() {
|
||||||
Log.d(TAG, "Account projects sync requested; ${if (syncAccountProjectsActive) "sync active" else "sync NOT active"}) ...")
|
Log.d(TAG, "Account projects sync requested; ${if (syncAccountProjectsActive) "sync active" else "sync NOT active"}) ...")
|
||||||
updateNetworkStatus()
|
updateNetworkStatus()
|
||||||
@@ -1538,18 +1445,11 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
fun execute(): SyncAccountProjectsTask {
|
fun execute(): SyncAccountProjectsTask {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
syncAccountProjectsActive = true
|
syncAccountProjectsActive = true
|
||||||
try {
|
val status = withContext(Dispatchers.IO) {
|
||||||
val status = withContext(Dispatchers.IO) {
|
doWork()
|
||||||
doWork()
|
|
||||||
}
|
|
||||||
onPostExecute(status)
|
|
||||||
} catch (e: CancellationException) {
|
|
||||||
throw e
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Account projects sync failed", e)
|
|
||||||
} finally {
|
|
||||||
syncAccountProjectsActive = false
|
|
||||||
}
|
}
|
||||||
|
onPostExecute(status)
|
||||||
|
syncAccountProjectsActive = false
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
}
|
}
|
||||||
@@ -1578,25 +1478,20 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
|
|
||||||
val response = client.getAccountProjects(useOcsApi)
|
val response = client.getAccountProjects(useOcsApi)
|
||||||
val remoteAccountProjects = response.getAccountProjects(url)
|
val remoteAccountProjects = response.getAccountProjects(url)
|
||||||
val forgotten = forgottenAccountProjects(preferences)
|
|
||||||
dbHelper.clearAccountProjects()
|
dbHelper.clearAccountProjects()
|
||||||
for (remoteAccountProject in remoteAccountProjects) {
|
for (remoteAccountProject in remoteAccountProjects) {
|
||||||
dbHelper.addAccountProject(remoteAccountProject)
|
dbHelper.addAccountProject(remoteAccountProject)
|
||||||
Log.v(TAG, "received account project $remoteAccountProject")
|
Log.v(TAG, "received account project $remoteAccountProject")
|
||||||
val existingProj = localProjects.find {
|
val existingProj = localProjects.find {
|
||||||
it.remoteId == remoteAccountProject.remoteId &&
|
it.remoteId == remoteAccountProject.remoteId &&
|
||||||
it.serverUrl?.replace("/+$".toRegex(), "") == remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + COSPEND_PATH
|
it.serverUrl?.replace("/+$".toRegex(), "") == remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend"
|
||||||
}
|
}
|
||||||
if (existingProj == null) {
|
if (existingProj == null) {
|
||||||
if (accountProjectKey(remoteAccountProject.remoteId, remoteAccountProject.ncUrl) in forgotten) {
|
|
||||||
Log.d(TAG, "skipping ${remoteAccountProject.remoteId}, deleted on this device")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
val newProj = DBProject(0,
|
val newProj = DBProject(0,
|
||||||
remoteAccountProject.remoteId,
|
remoteAccountProject.remoteId,
|
||||||
"",
|
"",
|
||||||
remoteAccountProject.name,
|
remoteAccountProject.name,
|
||||||
remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + COSPEND_PATH,
|
remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend",
|
||||||
"",
|
"",
|
||||||
null,
|
null,
|
||||||
ProjectType.COSPEND,
|
ProjectType.COSPEND,
|
||||||
@@ -1906,9 +1801,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
*/
|
*/
|
||||||
private const val UNCHANGED_RUN_TO_SETTLE = 25
|
private const val UNCHANGED_RUN_TO_SETTLE = 25
|
||||||
|
|
||||||
/** Bills written per transaction while applying a pull. */
|
|
||||||
private const val BILL_APPLY_CHUNK = 500
|
|
||||||
|
|
||||||
private var instance: CowspentServerSyncHelper? = null
|
private var instance: CowspentServerSyncHelper? = null
|
||||||
private val projectIdsToSync: MutableList<Long> = ArrayList()
|
private val projectIdsToSync: MutableList<Long> = ArrayList()
|
||||||
|
|
||||||
@@ -1932,34 +1824,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)
|
preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The path a Cospend project's URL carries on top of its Nextcloud server URL. */
|
|
||||||
const val COSPEND_PATH = "/index.php/apps/cospend"
|
|
||||||
|
|
||||||
private const val FORGOTTEN_ACCOUNT_PROJECTS = "forgottenAccountProjects"
|
|
||||||
|
|
||||||
private fun trimSlashes(url: String) = url.replace("/+$".toRegex(), "")
|
|
||||||
|
|
||||||
/** Names one project an account offers, by server and remote id, with no local row needed. */
|
|
||||||
private fun accountProjectKey(remoteId: String, ncUrl: String) = "${trimSlashes(ncUrl)}|$remoteId"
|
|
||||||
|
|
||||||
/** The same key for a stored project, or null when it is not one an account can offer. */
|
|
||||||
private fun accountProjectKey(project: DBProject): String? {
|
|
||||||
val url = trimSlashes(project.serverUrl.orEmpty())
|
|
||||||
if (!url.endsWith(COSPEND_PATH)) return null
|
|
||||||
return accountProjectKey(project.remoteId, url.removeSuffix(COSPEND_PATH))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun forgottenAccountProjects(preferences: SharedPreferences): Set<String> =
|
|
||||||
preferences.getStringSet(FORGOTTEN_ACCOUNT_PROJECTS, emptySet()).orEmpty()
|
|
||||||
|
|
||||||
fun forgetAccountProject(context: Context, project: DBProject) {
|
|
||||||
val key = accountProjectKey(project) ?: return
|
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
preferences.edit {
|
|
||||||
putStringSet(FORGOTTEN_ACCOUNT_PROJECTS, forgottenAccountProjects(preferences) + key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getNextcloudAccountServerUrl(context: Context): String {
|
fun getNextcloudAccountServerUrl(context: Context): String {
|
||||||
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
return if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) {
|
return if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) {
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
|||||||
|
|
||||||
object ExportUtil {
|
object ExportUtil {
|
||||||
|
|
||||||
/** Quotes a field the way RFC4180 (and opencsv, and Cospend) expect. */
|
|
||||||
private fun q(value: String?): String = "\"" + (value ?: "").replace("\"", "\"\"") + "\""
|
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun createExportContent(db: CowspentSQLiteOpenHelper, projectId: Long): String {
|
fun createExportContent(db: CowspentSQLiteOpenHelper, projectId: Long): String {
|
||||||
var fileContent = ""
|
var fileContent = ""
|
||||||
@@ -21,8 +18,8 @@ object ExportUtil {
|
|||||||
}
|
}
|
||||||
val bills = db.getBillsOfProject(projectId).toMutableList()
|
val bills = db.getBillsOfProject(projectId).toMutableList()
|
||||||
|
|
||||||
// write header.
|
// write header
|
||||||
fileContent += "what,amount,date,timestamp,payer_name,payer_weight,payer_active,owers,repeat,categoryid,paymentmode,paymentmodeid,comment\n"
|
fileContent += "what,amount,date,timestamp,payer_name,payer_weight,payer_active,owers,repeat,categoryid,paymentmode\n"
|
||||||
|
|
||||||
// write members
|
// write members
|
||||||
for (m in members) {
|
for (m in members) {
|
||||||
@@ -44,11 +41,15 @@ object ExportUtil {
|
|||||||
val payerName = payer.name
|
val payerName = payer.name
|
||||||
val payerWeight = payer.weight
|
val payerWeight = payer.weight
|
||||||
val payerActive = if (payer.isActivated) 1 else 0
|
val payerActive = if (payer.isActivated) 1 else 0
|
||||||
val owersTxt = b.billOwers.mapNotNull { membersById[it.memberId]?.name }.joinToString(",")
|
val billOwers = b.billOwers
|
||||||
fileContent += "${q(b.what)},${b.amount},${b.date},${b.timestamp},${q(payerName)}," +
|
var owersTxt = ""
|
||||||
"$payerWeight,$payerActive,${q(owersTxt)},${b.repeat ?: DBBill.NON_REPEATED}," +
|
for (bo in billOwers) {
|
||||||
"${b.categoryId},${b.paymentMode ?: DBBill.PAYMODE_NONE},${b.paymentModeId}," +
|
owersTxt += membersById[bo.memberId]?.name + ","
|
||||||
"${q(b.comment)}\n"
|
}
|
||||||
|
owersTxt = owersTxt.replace(",$".toRegex(), "")
|
||||||
|
fileContent += "\"${b.what}\",${b.amount},${b.date},${b.timestamp},\"$payerName\"," +
|
||||||
|
"$payerWeight,$payerActive,\"$owersTxt\",${b.repeat},${b.categoryId}," +
|
||||||
|
"${b.paymentMode}\n"
|
||||||
}
|
}
|
||||||
|
|
||||||
// write categories
|
// write categories
|
||||||
@@ -56,16 +57,7 @@ object ExportUtil {
|
|||||||
if (cats.isNotEmpty()) {
|
if (cats.isNotEmpty()) {
|
||||||
fileContent += "\ncategoryname,categoryid,icon,color\n"
|
fileContent += "\ncategoryname,categoryid,icon,color\n"
|
||||||
for (cat in cats) {
|
for (cat in cats) {
|
||||||
fileContent += "${q(cat.name)},${cat.id},${q(cat.icon)},${q(cat.color)}\n"
|
fileContent += "\"${cat.name}\",${cat.id},\"${cat.icon}\",\"${cat.color}\"\n"
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// write payment modes
|
|
||||||
val pms = db.getPaymentModes(projectId)
|
|
||||||
if (pms.isNotEmpty()) {
|
|
||||||
fileContent += "\npaymentmodename,paymentmodeid,icon,color\n"
|
|
||||||
for (pm in pms) {
|
|
||||||
fileContent += "${q(pm.name)},${pm.id},${q(pm.icon)},${q(pm.color)}\n"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,9 +67,9 @@ object ExportUtil {
|
|||||||
project.currencyName!!.isNotEmpty() && project.currencyName != "null"
|
project.currencyName!!.isNotEmpty() && project.currencyName != "null"
|
||||||
) {
|
) {
|
||||||
fileContent += "\ncurrencyname,exchange_rate\n"
|
fileContent += "\ncurrencyname,exchange_rate\n"
|
||||||
fileContent += "${q(project.currencyName)},1\n"
|
fileContent += "\"${project.currencyName}\",1\n"
|
||||||
for (cur in curs) {
|
for (cur in curs) {
|
||||||
fileContent += "${q(cur.name)},${cur.exchangeRate}\n"
|
fileContent += "\"${cur.name}\",${cur.exchangeRate}\n"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,33 +28,6 @@ open class ServerResponse(
|
|||||||
) {
|
) {
|
||||||
private val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
private val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.ROOT)
|
||||||
|
|
||||||
/**
|
|
||||||
* Set while parsing bills when one referenced a member, category or payment mode that none of
|
|
||||||
* the supplied maps knew about. A partial sync does not re-read the project, so this is how it
|
|
||||||
* learns that its copy of those collections has gone stale and has to be refreshed.
|
|
||||||
*/
|
|
||||||
var hasUnresolvedReferences = false
|
|
||||||
private set
|
|
||||||
|
|
||||||
/** Members are only ever addressed by the server's own id, so an unknown one is stale state. */
|
|
||||||
private fun resolveMember(map: Map<Long, Long>, remoteId: Long): Long {
|
|
||||||
map[remoteId]?.let { return it }
|
|
||||||
if (remoteId != 0L) hasUnresolvedReferences = true
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Built-in labels are negative constants with no row of their own on a remote project, so they
|
|
||||||
* never appear in these maps and are carried through as-is. An unknown *positive* id is a real
|
|
||||||
* gap - it is dropped rather than kept, as it would collide with a local id.
|
|
||||||
*/
|
|
||||||
private fun resolveLabel(map: Map<Long, Long>, remoteId: Long): Long {
|
|
||||||
map[remoteId]?.let { return it }
|
|
||||||
if (remoteId < 0) return remoteId
|
|
||||||
if (remoteId > 0) hasUnresolvedReferences = true
|
|
||||||
return 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
class NotModifiedException : IOException()
|
class NotModifiedException : IOException()
|
||||||
|
|
||||||
protected val content: String
|
protected val content: String
|
||||||
@@ -771,10 +744,10 @@ open class ServerResponse(
|
|||||||
}
|
}
|
||||||
if (!json.isNull("payer_id")) {
|
if (!json.isNull("payer_id")) {
|
||||||
payerRemoteId = json.getLong("payer_id")
|
payerRemoteId = json.getLong("payer_id")
|
||||||
payerId = resolveMember(memberRemoteIdToId, payerRemoteId)
|
payerId = memberRemoteIdToId[payerRemoteId] ?: 0
|
||||||
} else if (!json.isNull("payer")) {
|
} else if (!json.isNull("payer")) {
|
||||||
payerRemoteId = json.getLong("payer")
|
payerRemoteId = json.getLong("payer")
|
||||||
payerId = resolveMember(memberRemoteIdToId, payerRemoteId)
|
payerId = memberRemoteIdToId[payerRemoteId] ?: 0
|
||||||
}
|
}
|
||||||
if (!json.isNull("amount")) {
|
if (!json.isNull("amount")) {
|
||||||
amount = json.getDouble("amount")
|
amount = json.getDouble("amount")
|
||||||
@@ -821,8 +794,8 @@ open class ServerResponse(
|
|||||||
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
|
paymentModeRemoteId = DBBill.oldPmIdToNew[paymentMode] ?: DBBill.PAYMODE_ID_NONE
|
||||||
}
|
}
|
||||||
|
|
||||||
val categoryId = resolveLabel(catRemoteIdToId, categoryRemoteId)
|
val categoryId = catRemoteIdToId[categoryRemoteId] ?: 0L
|
||||||
val paymentModeId = resolveLabel(pmRemoteIdToId, paymentModeRemoteId)
|
val paymentModeId = pmRemoteIdToId[paymentModeRemoteId] ?: 0L
|
||||||
|
|
||||||
val bill = DBBill(
|
val bill = DBBill(
|
||||||
0, remoteId, projId, payerId, amount, timestamp, what,
|
0, remoteId, projId, payerId, amount, timestamp, what,
|
||||||
@@ -849,7 +822,7 @@ open class ServerResponse(
|
|||||||
} else {
|
} else {
|
||||||
jsonOs.getLong(i)
|
jsonOs.getLong(i)
|
||||||
}
|
}
|
||||||
val memberLocalId = resolveMember(memberRemoteIdToId, memberRemoteId)
|
val memberLocalId = memberRemoteIdToId[memberRemoteId] ?: 0
|
||||||
billOwers.add(DBBillOwer(0, 0, memberLocalId))
|
billOwers.add(DBBillOwer(0, 0, memberLocalId))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,45 +5,29 @@ import androidx.preference.PreferenceManager
|
|||||||
import net.helcel.cowspent.R
|
import net.helcel.cowspent.R
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The two independent delays that govern syncing.
|
* The SyncOnOpen preference: how often opening the app refreshes the account and every project.
|
||||||
*
|
*
|
||||||
* They used to be one setting, which meant the app-open throttle and the full-sync delay could
|
* The choices and the default live here rather than in the settings screen so that the screen and
|
||||||
* not be tuned apart even though they answer different questions. The choices and defaults live
|
* the sync trigger cannot disagree about what is in effect.
|
||||||
* here rather than in the settings screen so the screen and the sync triggers cannot disagree
|
|
||||||
* about what is in effect.
|
|
||||||
*/
|
*/
|
||||||
object SyncSettings {
|
object SyncSettings {
|
||||||
|
|
||||||
/**
|
/** Steps offered by the slider, in minutes. */
|
||||||
* How long opening the app waits before refreshing the account and every project again, so
|
val INTERVAL_CHOICES_MINUTES = listOf(1, 10, 60, 1440)
|
||||||
* that resuming within the interval does not repeat the work. Not user-facing: it governs
|
|
||||||
* background catch-up, not anything the user asked for.
|
const val DEFAULT_INTERVAL_MINUTES = 10
|
||||||
*/
|
|
||||||
const val OPEN_SYNC_INTERVAL_MINUTES = 10
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Steps offered for the full sync delay, in minutes. 0 means every manual refresh is a full
|
* The configured interval in minutes. A stored value that is not one of the offered steps —
|
||||||
* sync; anything else lets a refresh inside the window settle for fetching just the changes.
|
* from a restored backup, or a build that changed the steps — falls back to the default
|
||||||
|
* rather than being displayed as the first step while a different value drives the sync.
|
||||||
*/
|
*/
|
||||||
val FULL_SYNC_DELAY_CHOICES_MINUTES = listOf(0, 60, 1440, 10080)
|
fun intervalMinutes(context: Context): Int {
|
||||||
|
|
||||||
/**
|
|
||||||
* Only consulted once Extra Features is on - with it off every pull-to-refresh is a full
|
|
||||||
* sync, whatever is stored here.
|
|
||||||
*/
|
|
||||||
const val DEFAULT_FULL_SYNC_DELAY_MINUTES = 1440
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The configured delay in minutes. A stored value that is not one of the offered steps - from
|
|
||||||
* a restored backup, or a build that changed the steps - falls back to the default rather
|
|
||||||
* than being displayed as one step while a different value drives the sync.
|
|
||||||
*/
|
|
||||||
fun fullSyncDelayMinutes(context: Context): Int {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
val stored = prefs.getInt(
|
val stored = prefs.getInt(
|
||||||
context.getString(R.string.pref_key_full_sync_delay),
|
context.getString(R.string.pref_key_auto_sync_on_open),
|
||||||
DEFAULT_FULL_SYNC_DELAY_MINUTES
|
DEFAULT_INTERVAL_MINUTES
|
||||||
)
|
)
|
||||||
return if (stored in FULL_SYNC_DELAY_CHOICES_MINUTES) stored else DEFAULT_FULL_SYNC_DELAY_MINUTES
|
return if (stored in INTERVAL_CHOICES_MINUTES) stored else DEFAULT_INTERVAL_MINUTES
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -689,18 +689,12 @@ class VersatileProjectSyncClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
||||||
/**
|
|
||||||
* [since] overrides the project cursor sent as lastChanged. Passing 0 asks the server for the
|
|
||||||
* whole project rather than the changes since the last sync; omitting it keeps the cursor, so
|
|
||||||
* the server returns only what has moved.
|
|
||||||
*/
|
|
||||||
fun getBills(
|
fun getBills(
|
||||||
project: DBProject,
|
project: DBProject,
|
||||||
offset: Int? = null,
|
offset: Int? = null,
|
||||||
limit: Int? = null,
|
limit: Int? = null,
|
||||||
reverse: Boolean? = null,
|
reverse: Boolean? = null,
|
||||||
deleted: Int? = null,
|
deleted: Int? = null
|
||||||
since: Long? = null
|
|
||||||
): ServerResponse.BillsResponse {
|
): ServerResponse.BillsResponse {
|
||||||
var target: String
|
var target: String
|
||||||
var username: String?
|
var username: String?
|
||||||
@@ -712,7 +706,7 @@ class VersatileProjectSyncClient(
|
|||||||
val paramValues: MutableList<String> = ArrayList()
|
val paramValues: MutableList<String> = ArrayList()
|
||||||
|
|
||||||
if (ProjectType.COSPEND == project.type) {
|
if (ProjectType.COSPEND == project.type) {
|
||||||
val tsLastSync = since ?: project.lastSyncedTimestamp
|
val tsLastSync = project.lastSyncedTimestamp
|
||||||
if (offset == null) {
|
if (offset == null) {
|
||||||
if (cospendVersionGT161) {
|
if (cospendVersionGT161) {
|
||||||
paramKeys.add("lastChanged")
|
paramKeys.add("lastChanged")
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -138,8 +138,14 @@
|
|||||||
<string name="settings_color_custom">Eigene Farbe</string>
|
<string name="settings_color_custom">Eigene Farbe</string>
|
||||||
<string name="settings_color_mode">Farbauswahl</string>
|
<string name="settings_color_mode">Farbauswahl</string>
|
||||||
<string name="settings_show_archived">Archivierte Projekte anzeigen</string>
|
<string name="settings_show_archived">Archivierte Projekte anzeigen</string>
|
||||||
|
<string name="settings_beta_features">Beta-Funktionen</string>
|
||||||
|
<string name="settings_beta_features_summary">Experimentelle Funktionen aktivieren. Benutzung auf eigene Gefahr.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Aus letzter Rechnung vorausfüllen</string>
|
<string name="settings_fill_new_bill_from_last">Aus letzter Rechnung vorausfüllen</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Zahler, Kategorie, Zahlungsart und Beteiligte aus der zuletzt erstellten Rechnung des Projekts übernehmen.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Zahler, Kategorie, Zahlungsart und Beteiligte aus der zuletzt erstellten Rechnung des Projekts übernehmen.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Synchronisierungsintervall</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">Wie oft Konto und alle Projekte beim Öffnen der App aktualisiert werden.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 Minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 Minuten</string>
|
||||||
<string name="pref_value_sync_1h">1 Stunde</string>
|
<string name="pref_value_sync_1h">1 Stunde</string>
|
||||||
<string name="pref_value_sync_1d">1 Tag</string>
|
<string name="pref_value_sync_1d">1 Tag</string>
|
||||||
<string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"https\".</string>
|
<string name="settings_url_warn_http">WARNUNG: \"http\" ist unsicher. Verwende \"https\".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -137,8 +137,14 @@
|
|||||||
<string name="settings_color_custom">Color personalizado</string>
|
<string name="settings_color_custom">Color personalizado</string>
|
||||||
<string name="settings_color_mode">Selección de color</string>
|
<string name="settings_color_mode">Selección de color</string>
|
||||||
<string name="settings_show_archived">Mostrar los proyectos archivados</string>
|
<string name="settings_show_archived">Mostrar los proyectos archivados</string>
|
||||||
|
<string name="settings_beta_features">Funciones beta</string>
|
||||||
|
<string name="settings_beta_features_summary">Activar las funciones experimentales. Úsalas bajo tu propia responsabilidad.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Rellenar desde la última factura</string>
|
<string name="settings_fill_new_bill_from_last">Rellenar desde la última factura</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Reutilizar el pagador, la categoría, el modo y los participantes de la última factura creada en el proyecto.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Reutilizar el pagador, la categoría, el modo y los participantes de la última factura creada en el proyecto.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Intervalo de sincronización</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">Con qué frecuencia se actualizan la cuenta y todos los proyectos al abrir la aplicación.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minuto</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutos</string>
|
||||||
<string name="pref_value_sync_1h">1 hora</string>
|
<string name="pref_value_sync_1h">1 hora</string>
|
||||||
<string name="pref_value_sync_1d">1 día</string>
|
<string name="pref_value_sync_1d">1 día</string>
|
||||||
<string name="settings_url_warn_http">ADVERTENCIA: \"http\" no es seguro. Usa \"https\".</string>
|
<string name="settings_url_warn_http">ADVERTENCIA: \"http\" no es seguro. Usa \"https\".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -138,8 +138,14 @@
|
|||||||
<string name="settings_color_custom">Couleur personnalisée</string>
|
<string name="settings_color_custom">Couleur personnalisée</string>
|
||||||
<string name="settings_color_mode">Sélection de la couleur</string>
|
<string name="settings_color_mode">Sélection de la couleur</string>
|
||||||
<string name="settings_show_archived">Afficher les projets archivés</string>
|
<string name="settings_show_archived">Afficher les projets archivés</string>
|
||||||
|
<string name="settings_beta_features">Fonctionnalités bêta</string>
|
||||||
|
<string name="settings_beta_features_summary">Activer les fonctionnalités expérimentales. À utiliser à vos risques et périls.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Pré-remplir depuis la dernière facture</string>
|
<string name="settings_fill_new_bill_from_last">Pré-remplir depuis la dernière facture</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Reprendre le payeur, la catégorie, le mode et les participants de la dernière facture créée dans le projet.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Reprendre le payeur, la catégorie, le mode et les participants de la dernière facture créée dans le projet.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Intervalle de synchronisation</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">Fréquence de rafraîchissement du compte et de tous les projets à l\'ouverture de l\'application.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 heure</string>
|
<string name="pref_value_sync_1h">1 heure</string>
|
||||||
<string name="pref_value_sync_1d">1 jour</string>
|
<string name="pref_value_sync_1d">1 jour</string>
|
||||||
<string name="settings_url_warn_http">AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\".</string>
|
<string name="settings_url_warn_http">AVERTISSEMENT : \"http\" n\'est pas sûr. Utilisez \"https\".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -144,8 +144,14 @@
|
|||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
<string name="simple_back">Back</string>
|
<string name="simple_back">Back</string>
|
||||||
<string name="action_archive">Archive</string>
|
<string name="action_archive">Archive</string>
|
||||||
<string name="action_unarchive">Unarchive</string>
|
<string name="action_unarchive">Unarchive</string>
|
||||||
<string name="action_forget">Delete locally</string>
|
|
||||||
<string name="action_export">Export</string>
|
<string name="action_export">Export</string>
|
||||||
<string name="action_stats">Stats</string>
|
<string name="action_stats">Stats</string>
|
||||||
<string name="action_settle">Settle</string>
|
<string name="action_settle">Settle</string>
|
||||||
@@ -25,8 +24,6 @@
|
|||||||
<string name="action_settings">Settings</string>
|
<string name="action_settings">Settings</string>
|
||||||
<string name="action_label_bills">Label missing categories</string>
|
<string name="action_label_bills">Label missing categories</string>
|
||||||
<string name="action_logout">Logout</string>
|
<string name="action_logout">Logout</string>
|
||||||
<string name="logout_confirm_title">Log out?</string>
|
|
||||||
<string name="logout_confirm_no_projects">You can sign back in at any time.</string>
|
|
||||||
<string name="action_connect">Connect</string>
|
<string name="action_connect">Connect</string>
|
||||||
<string name="action_discard">Discard</string>
|
<string name="action_discard">Discard</string>
|
||||||
<string name="action_members">Members</string>
|
<string name="action_members">Members</string>
|
||||||
@@ -139,23 +136,20 @@
|
|||||||
<string name="settings_night_mode">Theme</string>
|
<string name="settings_night_mode">Theme</string>
|
||||||
<string name="settings_offline_mode">Offline mode</string>
|
<string name="settings_offline_mode">Offline mode</string>
|
||||||
<string name="settings_offline_mode_summary">Only sync manually.</string>
|
<string name="settings_offline_mode_summary">Only sync manually.</string>
|
||||||
<string name="settings_restore_deleted_projects">Restore deleted projects</string>
|
|
||||||
<string name="settings_restore_deleted_projects_done">Restoring from the account</string>
|
|
||||||
<string name="settings_restore_deleted_projects_confirm">Restore</string>
|
|
||||||
<string name="settings_color_custom">Custom color</string>
|
<string name="settings_color_custom">Custom color</string>
|
||||||
<string name="settings_color_mode">Color Selection</string>
|
<string name="settings_color_mode">Color Selection</string>
|
||||||
<string name="settings_show_archived">Show archived projects</string>
|
<string name="settings_show_archived">Show archived projects</string>
|
||||||
<string name="settings_extra_features">Extra Features</string>
|
<string name="settings_beta_features">Beta Features</string>
|
||||||
<string name="settings_extra_features_summary">Experimental and advanced settings. Use at your own risk.</string>
|
<string name="settings_beta_features_summary">Enable experimental features. Use at your own risk.</string>
|
||||||
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
<string name="settings_fill_new_bill_from_last">Auto-fill from last bill</string>
|
||||||
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
<string name="settings_fill_new_bill_from_last_summary">Pre-fill payer, category, mode and owers from the last bill created in the project.</string>
|
||||||
<string name="settings_stats_include_deactivated">Include deactivated members in stats</string>
|
<string name="settings_stats_include_deactivated">Include deactivated members in stats</string>
|
||||||
<string name="settings_full_sync_delay">Full sync delay</string>
|
<string name="settings_auto_sync_on_open">Sync interval</string>
|
||||||
<string name="settings_full_sync_delay_summary">How long a pull-to-refresh waits between two full syncs. In between, it only fetches what changed, which is much faster.</string>
|
<string name="settings_auto_sync_on_open_summary">How often to refresh the account and all projects when opening the app.</string>
|
||||||
<string name="pref_value_sync_always">Always</string>
|
<string name="pref_value_sync_1m">1 minute</string>
|
||||||
|
<string name="pref_value_sync_10m">10 minutes</string>
|
||||||
<string name="pref_value_sync_1h">1 hour</string>
|
<string name="pref_value_sync_1h">1 hour</string>
|
||||||
<string name="pref_value_sync_1d">1 day</string>
|
<string name="pref_value_sync_1d">1 day</string>
|
||||||
<string name="pref_value_sync_1w">1 week</string>
|
|
||||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||||
<string name="settings_colorpicker_title">Choose Color</string>
|
<string name="settings_colorpicker_title">Choose Color</string>
|
||||||
|
|
||||||
@@ -176,9 +170,9 @@
|
|||||||
<string name="pref_key_color_mode" translatable="false">colorMode</string>
|
<string name="pref_key_color_mode" translatable="false">colorMode</string>
|
||||||
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
|
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
|
||||||
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
||||||
<string name="pref_key_extra_features" translatable="false">betaFeatures</string>
|
<string name="pref_key_beta_features" translatable="false">betaFeatures</string>
|
||||||
<string name="pref_key_stats_include_deactivated" translatable="false">statsIncludeDeactivated</string>
|
<string name="pref_key_stats_include_deactivated" translatable="false">statsIncludeDeactivated</string>
|
||||||
<string name="pref_key_full_sync_delay" translatable="false">fullSyncDelayMinutes</string>
|
<string name="pref_key_auto_sync_on_open" translatable="false">autoSyncOnOpen</string>
|
||||||
<string name="pref_key_last_account_sync_timestamp" translatable="false">lastAccountSyncTimestamp</string>
|
<string name="pref_key_last_account_sync_timestamp" translatable="false">lastAccountSyncTimestamp</string>
|
||||||
<string name="pref_key_fill_new_bill_from_last" translatable="false">fillNewBillFromLast</string>
|
<string name="pref_key_fill_new_bill_from_last" translatable="false">fillNewBillFromLast</string>
|
||||||
<string name="pref_value_night_mode_no" translatable="false">1</string>
|
<string name="pref_value_night_mode_no" translatable="false">1</string>
|
||||||
@@ -296,19 +290,4 @@
|
|||||||
<string name="share_intent_title">Project %1$s</string>
|
<string name="share_intent_title">Project %1$s</string>
|
||||||
<string name="share_chooser_title">Share %1$s</string>
|
<string name="share_chooser_title">Share %1$s</string>
|
||||||
|
|
||||||
|
|
||||||
<plurals name="settings_restore_deleted_projects_summary">
|
|
||||||
<item quantity="one">%d project the account offers is kept off this device.</item>
|
|
||||||
<item quantity="other">%d projects the account offers are kept off this device.</item>
|
|
||||||
</plurals>
|
|
||||||
|
|
||||||
<plurals name="logout_confirm_projects">
|
|
||||||
<item quantity="one">%d project from this account will be removed from this device. It comes back when you sign in again.</item>
|
|
||||||
<item quantity="other">%d projects from this account will be removed from this device. They come back when you sign in again.</item>
|
|
||||||
</plurals>
|
|
||||||
|
|
||||||
<plurals name="warning_unsynced_bills">
|
|
||||||
<item quantity="one">%d bill has not reached the server yet and will be lost.</item>
|
|
||||||
<item quantity="other">%d bills have not reached the server yet and will be lost.</item>
|
|
||||||
</plurals>
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
Reference in New Issue
Block a user