Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
7581985abd
|
|||
|
810ca20201
|
|||
|
678a90ead9
|
|||
|
88469fa96c
|
|||
|
119ae757b9
|
|||
|
c3d380ddb4
|
|||
|
a8ba00269b
|
|||
|
756a37de23
|
|||
|
5f7266013d
|
|||
|
e98dc4a86b
|
|||
|
afb14ff7b9
|
|||
|
1bee1dabbe
|
|||
|
84432fe68d
|
|||
|
7e00b14d27
|
|||
|
ec4d60897a
|
@@ -52,7 +52,8 @@ jobs:
|
||||
- name: Build APK
|
||||
run: |
|
||||
VERSION_CODE=$(git rev-list --count HEAD)
|
||||
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE
|
||||
VERSION_NAME=$(git describe --tags --always)
|
||||
./gradlew assembleSignedRelease -PVERSION_CODE=$VERSION_CODE -PVERSION_NAME=$VERSION_NAME
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v3
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<!--suppress ALL -->
|
||||
<div align="center">
|
||||
<h1>Cowspent</h1>
|
||||
<img width="100px" src="./metadata/en-US/images/icon.webp" alt="Logo">
|
||||
<img width="100px" src="./metadata/en-US/images/icon.png" alt="Logo">
|
||||
|
||||
<p>
|
||||
|
||||
|
||||
+19
-5
@@ -14,7 +14,7 @@ android {
|
||||
applicationId "net.helcel.cowspent"
|
||||
minSdk = 26
|
||||
targetSdk = 37
|
||||
versionName "1.0b"
|
||||
versionName project.hasProperty('VERSION_NAME') ? project.property('VERSION_NAME') : "1"
|
||||
versionCode project.hasProperty('VERSION_CODE') ? project.property('VERSION_CODE').toInteger() : 1
|
||||
}
|
||||
|
||||
@@ -82,11 +82,22 @@ android {
|
||||
androidResources {
|
||||
generateLocaleConfig = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
jniLibs.keepDebugSymbols.add("**/*.so")
|
||||
}
|
||||
|
||||
dependenciesInfo {
|
||||
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
|
||||
includeInApk = false
|
||||
// Disables dependency metadata when building Android App Bundles (for Google Play)
|
||||
includeInBundle = false
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.compose.foundation:foundation:1.11.2'
|
||||
implementation 'androidx.compose.runtime:runtime:1.11.2'
|
||||
implementation 'androidx.compose.foundation:foundation:1.11.4'
|
||||
implementation 'androidx.compose.runtime:runtime:1.11.4'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.5'
|
||||
|
||||
implementation 'androidx.preference:preference-ktx:1.2.1'
|
||||
@@ -99,7 +110,7 @@ dependencies {
|
||||
implementation 'com.github.nextcloud:Android-SingleSignOn:1.1.0'
|
||||
implementation 'com.opencsv:opencsv:5.12.0'
|
||||
|
||||
implementation platform('androidx.compose:compose-bom:2026.05.01')
|
||||
implementation platform('androidx.compose:compose-bom:2026.06.01')
|
||||
implementation 'androidx.compose.ui:ui'
|
||||
implementation 'androidx.compose.material:material'
|
||||
implementation 'androidx.compose.material:material-icons-extended'
|
||||
@@ -107,5 +118,8 @@ dependencies {
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling'
|
||||
implementation 'androidx.activity:activity-compose:1.13.0'
|
||||
implementation 'androidx.activity:activity-ktx:1.13.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0'
|
||||
implementation 'androidx.security:security-crypto:1.1.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0'
|
||||
implementation "androidx.datastore:datastore-preferences:1.2.1"
|
||||
implementation "com.google.crypto.tink:tink-android:1.22.0"
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.account.AccountActivity"
|
||||
android:label="@string/settings_server_settings"
|
||||
android:label="@string/title_account"
|
||||
android:parentActivityName="net.helcel.cowspent.android.settings.PreferencesActivity"
|
||||
android:windowSoftInputMode="adjustResize|stateVisible"
|
||||
android:launchMode="standard"
|
||||
@@ -67,7 +67,7 @@
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.project.create.NewProjectActivity"
|
||||
android:label="@string/simple_add_project"
|
||||
android:label="@string/title_add_project"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="standard"
|
||||
@@ -94,7 +94,7 @@
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.project.edit.EditProjectActivity"
|
||||
android:label="@string/simple_edit_project"
|
||||
android:label="@string/title_edit_project"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="standard">
|
||||
@@ -102,7 +102,7 @@
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.bill_edit.EditBillActivity"
|
||||
android:label="@string/simple_edit_bill"
|
||||
android:label="@string/action_edit"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:windowSoftInputMode="adjustResize"
|
||||
android:launchMode="standard">
|
||||
@@ -110,20 +110,32 @@
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.about.AboutActivity"
|
||||
android:label="@string/simple_about"
|
||||
android:label="@string/title_about"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity" />
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.statistics.ProjectStatisticsActivity"
|
||||
android:label="@string/statistic_title"
|
||||
android:label="@string/title_stats"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.bill_label.LabelBillsActivity"
|
||||
android:label="@string/label_bills_title"
|
||||
android:label="@string/title_label_bills"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:theme="@style/AppTheme.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.label.LabelManagementActivity"
|
||||
android:label="@string/title_labels"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<activity
|
||||
android:name="net.helcel.cowspent.android.project.member.MemberManagementActivity"
|
||||
android:label="@string/action_members"
|
||||
android:parentActivityName="net.helcel.cowspent.android.main.BillsListViewActivity"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -45,7 +45,7 @@ fun AboutScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.simple_about)) },
|
||||
title = { Text(stringResource(R.string.title_about)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
|
||||
@@ -13,8 +13,8 @@ import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.foundation.layout.Box
|
||||
@@ -33,7 +33,6 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nextcloud.android.sso.AccountImporter
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
||||
import com.nextcloud.android.sso.model.SingleSignOnAccount
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@@ -42,6 +41,7 @@ import net.helcel.cowspent.android.main.MainConstants
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import net.helcel.cowspent.util.CospendClientUtil
|
||||
import net.helcel.cowspent.util.CospendClientUtil.LoginStatus
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
import java.net.URLDecoder
|
||||
import java.util.Locale
|
||||
|
||||
@@ -69,7 +69,6 @@ class AccountActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
private lateinit var preferences: SharedPreferences
|
||||
private var oldPassword = ""
|
||||
private var useWebLogin = true
|
||||
private var showLoginDialog by mutableStateOf(false)
|
||||
|
||||
@@ -129,8 +128,6 @@ class AccountActivity : AppCompatActivity() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
oldPassword = preferences.getString(SETTINGS_PASSWORD, DEFAULT_SETTINGS) ?: ""
|
||||
viewModel.validateUrl()
|
||||
}
|
||||
|
||||
@@ -224,11 +221,7 @@ class AccountActivity : AppCompatActivity() {
|
||||
private fun legacyLogin() {
|
||||
val url = CospendClientUtil.formatURL(viewModel.serverUrl.trim())
|
||||
val username = viewModel.username
|
||||
var password = viewModel.password
|
||||
|
||||
if (password.isEmpty()) {
|
||||
password = oldPassword
|
||||
}
|
||||
val password = viewModel.password
|
||||
|
||||
performLogin(url, username, password)
|
||||
}
|
||||
@@ -311,10 +304,10 @@ class AccountActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
if (status == LoginStatus.OK) {
|
||||
SecureStorage.savePassword(applicationContext, SETTINGS_PASSWORD, password)
|
||||
preferences.edit {
|
||||
putString(SETTINGS_URL, url)
|
||||
putString(SETTINGS_USERNAME, username)
|
||||
putString(SETTINGS_PASSWORD, password)
|
||||
remove(SETTINGS_KEY_ETAG)
|
||||
remove(SETTINGS_KEY_LAST_MODIFIED)
|
||||
}
|
||||
|
||||
@@ -4,39 +4,18 @@
|
||||
|
||||
package net.helcel.cowspent.android.account
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Switch
|
||||
import androidx.compose.material.SwitchDefaults
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.Lock
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.PasswordVisualTransformation
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Dp
|
||||
@@ -99,7 +78,7 @@ fun AccountScreenContent(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.settings_server_settings)) },
|
||||
title = { Text(stringResource(R.string.title_account)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
@@ -123,30 +102,70 @@ fun AccountScreenContent(
|
||||
}
|
||||
} else if (isLoggedIn) {
|
||||
Text(
|
||||
text = stringResource(R.string.account_logged_in_as, username),
|
||||
style = MaterialTheme.typography.h6
|
||||
)
|
||||
Text(
|
||||
text = serverUrl,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
text = "CURRENT ACCOUNT",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Person,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Column {
|
||||
Text(
|
||||
text = stringResource(R.string.msg_logged_in_as, username),
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
Text(
|
||||
text = serverUrl,
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(
|
||||
onClick = onLogout,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text(stringResource(R.string.account_logout))
|
||||
Text(stringResource(R.string.action_logout))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
if (!isValidatingLogin) {
|
||||
Text(
|
||||
text = "CONNECTION SETTINGS",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp, top = if (isLoggedIn) 16.dp else 0.dp)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(vertical = 8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.use_sso_toggle), modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
Icons.Default.Sync,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Text(stringResource(R.string.label_use_sso), modifier = Modifier.weight(1f), style = MaterialTheme.typography.subtitle1)
|
||||
Switch(
|
||||
checked = useSso,
|
||||
onCheckedChange = { onSsoClick(it) },
|
||||
@@ -163,7 +182,7 @@ fun AccountScreenContent(
|
||||
OutlinedTextField(
|
||||
value = serverUrl,
|
||||
onValueChange = onServerUrlChange,
|
||||
label = { Text(stringResource(R.string.settings_url)) },
|
||||
placeholder = { Text(stringResource(R.string.label_url)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) },
|
||||
trailingIcon = {
|
||||
@@ -192,7 +211,7 @@ fun AccountScreenContent(
|
||||
OutlinedTextField(
|
||||
value = username,
|
||||
onValueChange = onUsernameChange,
|
||||
label = { Text(stringResource(R.string.settings_username)) },
|
||||
placeholder = { Text(stringResource(R.string.label_username)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Person, contentDescription = null) },
|
||||
singleLine = true
|
||||
@@ -204,7 +223,7 @@ fun AccountScreenContent(
|
||||
OutlinedTextField(
|
||||
value = password,
|
||||
onValueChange = onPasswordChange,
|
||||
label = { Text(stringResource(R.string.settings_password)) },
|
||||
placeholder = { Text(stringResource(R.string.label_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) },
|
||||
visualTransformation = PasswordVisualTransformation(),
|
||||
@@ -221,7 +240,7 @@ fun AccountScreenContent(
|
||||
if (isSubmitting) {
|
||||
CustomCircularProgressIndicator(size = 24.dp, color = Color.White)
|
||||
} else {
|
||||
Text(stringResource(R.string.settings_submit))
|
||||
Text(stringResource(R.string.action_connect))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.cowspent.util.CospendClientUtil
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
|
||||
class AccountViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val preferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
|
||||
@@ -60,14 +61,15 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
} else {
|
||||
preferences.getString(AccountActivity.SETTINGS_USERNAME, "")
|
||||
}
|
||||
val password = if (useSso) {
|
||||
""
|
||||
} else {
|
||||
preferences.getString(AccountActivity.SETTINGS_PASSWORD, "")
|
||||
}
|
||||
|
||||
if (!url.isNullOrEmpty() && !username.isNullOrEmpty()) {
|
||||
viewModelScope.launch {
|
||||
val password = if (useSso) {
|
||||
""
|
||||
} else {
|
||||
SecureStorage.getPassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
||||
}
|
||||
|
||||
isValidatingLogin = true
|
||||
isLoggedIn = withContext(Dispatchers.IO) {
|
||||
if (useSso) {
|
||||
@@ -87,6 +89,9 @@ class AccountViewModel(application: Application) : AndroidViewModel(application)
|
||||
}
|
||||
|
||||
fun logout() {
|
||||
viewModelScope.launch {
|
||||
SecureStorage.removePassword(getApplication(), AccountActivity.SETTINGS_PASSWORD)
|
||||
}
|
||||
preferences.edit {
|
||||
remove(AccountActivity.SETTINGS_USE_SSO)
|
||||
remove(AccountActivity.SETTINGS_SSO_URL)
|
||||
|
||||
@@ -15,6 +15,7 @@ import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -65,8 +66,10 @@ fun LoginDialogContent(
|
||||
) {
|
||||
Column(modifier = Modifier.padding(24.dp)) {
|
||||
Text(
|
||||
text = "Login",
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = "LOGIN",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
|
||||
@@ -187,7 +187,12 @@ class EditBillActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
calendar.timeInMillis = bill.timestamp * 1000
|
||||
val members = db.getMembersOfProject(bill.projectId, null)
|
||||
val allMembers = db.getMembersOfProject(bill.projectId, null)
|
||||
val billOwerIds = bill.billOwersIds
|
||||
val members = allMembers.filter { m ->
|
||||
m.isActivated || m.id == bill.payerId || billOwerIds.contains(m.id)
|
||||
}
|
||||
|
||||
val project = db.getProject(bill.projectId)
|
||||
val currencies = db.getCurrencies(bill.projectId)
|
||||
withContext(Dispatchers.Main) {
|
||||
@@ -250,22 +255,22 @@ class EditBillActivity : AppCompatActivity() {
|
||||
return
|
||||
}
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.save_or_discard_bill_dialog_title),
|
||||
message = getString(R.string.save_or_discard_bill_dialog_message),
|
||||
positiveText = getString(R.string.save_or_discard_bill_dialog_save),
|
||||
title = getString(R.string.dialog_unsaved_changes_title),
|
||||
message = getString(R.string.dialog_unsaved_changes_msg),
|
||||
positiveText = getString(R.string.action_save),
|
||||
onConfirm = { saveBillAsked() },
|
||||
negativeText = getString(R.string.save_or_discard_bill_dialog_discard),
|
||||
negativeText = getString(R.string.action_discard),
|
||||
onCancel = { finish() }
|
||||
)
|
||||
}
|
||||
|
||||
private fun saveBillAsked() {
|
||||
val validationError = viewModel.getValidationError(
|
||||
getString(R.string.error_invalid_bill_what),
|
||||
getString(R.string.error_invalid_bill_name),
|
||||
getString(R.string.error_invalid_bill_date),
|
||||
getString(R.string.error_invalid_bill_payerid),
|
||||
getString(R.string.error_invalid_bill_payer),
|
||||
getString(R.string.error_invalid_bill_owers),
|
||||
getString(R.string.simple_error)
|
||||
getString(R.string.error_generic)
|
||||
)
|
||||
|
||||
if (validationError != null) {
|
||||
@@ -283,7 +288,7 @@ class EditBillActivity : AppCompatActivity() {
|
||||
|
||||
private fun deleteBillAsked() {
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.confirm_remove_project_dialog_title),
|
||||
title = getString(R.string.title_confirm),
|
||||
message = bill.what,
|
||||
positiveText = getString(R.string.action_delete),
|
||||
onConfirm = {
|
||||
|
||||
@@ -21,6 +21,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
@@ -60,7 +61,7 @@ fun EditBillScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(if (viewModel.isNewBill) R.string.simple_new_bill else R.string.simple_edit_bill)) },
|
||||
title = { Text(stringResource(if (viewModel.isNewBill) R.string.action_new_bill else R.string.action_edit)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
@@ -89,11 +90,11 @@ fun EditBillScreen(
|
||||
},
|
||||
floatingActionButton = {
|
||||
if (canEdit) {
|
||||
val errorWhat = stringResource(R.string.error_invalid_bill_what)
|
||||
val errorWhat = stringResource(R.string.error_invalid_bill_name)
|
||||
val errorDate = stringResource(R.string.error_invalid_bill_date)
|
||||
val errorPayer = stringResource(R.string.error_invalid_bill_payerid)
|
||||
val errorPayer = stringResource(R.string.error_invalid_bill_payer)
|
||||
val errorOwers = stringResource(R.string.error_invalid_bill_owers)
|
||||
val errorInvalidForm = stringResource(R.string.simple_error)
|
||||
val errorInvalidForm = stringResource(R.string.error_generic)
|
||||
|
||||
FloatingActionButton(onClick = {
|
||||
val validationError = viewModel.getValidationError(
|
||||
@@ -107,7 +108,7 @@ fun EditBillScreen(
|
||||
}) {
|
||||
Icon(
|
||||
Icons.Default.Done,
|
||||
contentDescription = stringResource(R.string.action_save_bill)
|
||||
contentDescription = stringResource(R.string.action_save)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -117,6 +118,7 @@ fun EditBillScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.padding(16.dp)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
@@ -163,6 +165,14 @@ fun BillBasicInfoSection(
|
||||
onDateClick: () -> Unit,
|
||||
onTimeClick: () -> Unit
|
||||
) {
|
||||
Text(
|
||||
text = "GENERAL",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
val context = LocalContext.current
|
||||
val currencyDialogTitle =
|
||||
stringResource(R.string.currency_dialog_title, viewModel.mainCurrencyName)
|
||||
@@ -171,7 +181,7 @@ fun BillBasicInfoSection(
|
||||
value = viewModel.what,
|
||||
onValueChange = { viewModel.what = it },
|
||||
enabled = canEdit,
|
||||
placeholder = { Text(stringResource(R.string.setting_what)) },
|
||||
placeholder = { Text(stringResource(R.string.label_what)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
|
||||
)
|
||||
@@ -188,7 +198,9 @@ fun BillBasicInfoSection(
|
||||
placeholder = { Text("0") },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty { viewModel.mainCurrencyName }
|
||||
val currencyToShow = viewModel.selectedCurrencyName.ifEmpty {
|
||||
viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||
}
|
||||
TextIconDisplay(
|
||||
textIcon = TextIcon.Symbol(currencyToShow),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
@@ -198,7 +210,7 @@ fun BillBasicInfoSection(
|
||||
IconButton(
|
||||
enabled = canEdit,
|
||||
onClick = {
|
||||
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "Main" }
|
||||
val mainLabel = viewModel.mainCurrencyName.ifEmpty { "$" }
|
||||
val options = listOf("$mainLabel | Base") + viewModel.currencies.map {
|
||||
"${it.name} | 1 $mainLabel = ${it.exchangeRate} ${it.name}"
|
||||
}
|
||||
@@ -259,13 +271,13 @@ fun PayerSection(
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedPayer?.name ?: "",
|
||||
placeholder = stringResource(R.string.setting_payer),
|
||||
placeholder = stringResource(R.string.label_payer),
|
||||
expanded = payerExpanded,
|
||||
onExpandedChange = { payerExpanded = it },
|
||||
onDismissRequest = { payerExpanded = false },
|
||||
enabled = canEdit,
|
||||
leadingIcon = {
|
||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (selectedPayer != null) {
|
||||
MemberAvatar(
|
||||
member = selectedPayer,
|
||||
@@ -337,8 +349,10 @@ fun OwerSelectionSection(
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
stringResource(R.string.setting_owers), fontSize = 12.sp,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
text = stringResource(R.string.label_owers).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
@@ -444,19 +458,27 @@ fun BillAdditionalDetailsSection(
|
||||
paymentModes: List<DBPaymentMode>,
|
||||
canEdit: Boolean
|
||||
) {
|
||||
Text(
|
||||
text = "DETAILS",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp, top = 16.dp)
|
||||
)
|
||||
|
||||
var categoryExpanded by remember { mutableStateOf(false) }
|
||||
val selectedCategory =
|
||||
categories.find { it.remoteId.toInt() == viewModel.categoryRemoteId }
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedCategory?.name ?: "",
|
||||
placeholder = stringResource(R.string.setting_category),
|
||||
placeholder = stringResource(R.string.label_category),
|
||||
expanded = categoryExpanded,
|
||||
onExpandedChange = { categoryExpanded = it },
|
||||
onDismissRequest = { categoryExpanded = false },
|
||||
enabled = canEdit,
|
||||
leadingIcon = {
|
||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (selectedCategory != null) {
|
||||
Text(text = selectedCategory.icon, fontSize = 20.sp)
|
||||
} else {
|
||||
@@ -494,13 +516,13 @@ fun BillAdditionalDetailsSection(
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedPm?.name ?: "",
|
||||
placeholder = stringResource(R.string.setting_payment_mode),
|
||||
placeholder = stringResource(R.string.label_mode),
|
||||
expanded = pmExpanded,
|
||||
onExpandedChange = { pmExpanded = it },
|
||||
onDismissRequest = { pmExpanded = false },
|
||||
enabled = canEdit,
|
||||
leadingIcon = {
|
||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (selectedPm != null) {
|
||||
Text(text = selectedPm.icon, fontSize = 20.sp)
|
||||
} else {
|
||||
@@ -545,7 +567,7 @@ fun BillAdditionalDetailsSection(
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedRepeat?.second ?: "",
|
||||
placeholder = stringResource(R.string.setting_project_repetition),
|
||||
placeholder = stringResource(R.string.label_repeat),
|
||||
expanded = repeatExpanded,
|
||||
onExpandedChange = { repeatExpanded = it },
|
||||
onDismissRequest = { repeatExpanded = false },
|
||||
@@ -569,7 +591,7 @@ fun BillAdditionalDetailsSection(
|
||||
value = viewModel.comment,
|
||||
onValueChange = { viewModel.comment = it },
|
||||
enabled = canEdit,
|
||||
placeholder = { Text(stringResource(R.string.setting_comment)) },
|
||||
placeholder = { Text(stringResource(R.string.label_comment)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = false,
|
||||
leadingIcon = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package net.helcel.cowspent.android.bill_edit
|
||||
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableDoubleStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
@@ -10,6 +11,7 @@ import androidx.lifecycle.ViewModel
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.android.helper.DialogState
|
||||
import net.helcel.cowspent.android.helper.parseAmount
|
||||
import net.helcel.cowspent.util.SupportUtil
|
||||
import net.helcel.cowspent.util.evalMath
|
||||
|
||||
@@ -30,7 +32,7 @@ class EditBillViewModel : ViewModel() {
|
||||
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
||||
var mainCurrencyName by mutableStateOf("")
|
||||
var selectedCurrencyName by mutableStateOf("")
|
||||
var selectedCurrencyRate by mutableStateOf(1.0)
|
||||
var selectedCurrencyRate by mutableDoubleStateOf(1.0)
|
||||
var members by mutableStateOf<List<DBMember>>(emptyList())
|
||||
|
||||
var owersSelection = mutableStateMapOf<Long, Boolean>()
|
||||
@@ -41,13 +43,8 @@ class EditBillViewModel : ViewModel() {
|
||||
|
||||
val amountAsDouble: Double
|
||||
get() {
|
||||
val amountStr = amount.replace(',', '.')
|
||||
return try {
|
||||
if (amountStr.matches("[0-9.]+".toRegex())) {
|
||||
amountStr.toDouble()
|
||||
} else {
|
||||
evalMath(amountStr)
|
||||
}
|
||||
return parseAmount(amount) ?: try {
|
||||
evalMath(amount.replace(',', '.'))
|
||||
} catch (_: Exception) {
|
||||
0.0
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ fun LabelBillsScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.label_bills_title)) },
|
||||
title = { Text(stringResource(R.string.title_label_bills)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
@@ -64,10 +64,11 @@ fun LabelBillsScreen(
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_bills_suggested),
|
||||
text = stringResource(R.string.label_bills_suggested).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
@@ -93,7 +94,7 @@ fun LabelBillsScreen(
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = stringResource(R.string.label_bills_no_suggestions),
|
||||
text = stringResource(R.string.msg_no_suggestions),
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -105,10 +106,11 @@ fun LabelBillsScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.setting_category),
|
||||
text = stringResource(R.string.label_category).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth().padding(top = 8.dp)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -141,7 +143,7 @@ fun LabelBillsScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
} else {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text(stringResource(R.string.label_bills_no_more))
|
||||
Text(stringResource(R.string.msg_bill_labeled_done))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-11
@@ -34,7 +34,7 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
||||
showToast(this@ManageCurrenciesActivity,getString(R.string.currency_saved_success), Toast.LENGTH_LONG)
|
||||
} else {
|
||||
viewModel.showDialog(title=getString(R.string.error_edit_remote_project_helper, message),
|
||||
message=getString(R.string.currency_manager),
|
||||
message=getString(R.string.action_currencies),
|
||||
positiveText = getString(android.R.string.ok))
|
||||
}
|
||||
}
|
||||
@@ -67,8 +67,10 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
||||
viewModel = viewModel,
|
||||
onBack = { finish() },
|
||||
onSaveMain = { saveMainCurrency() },
|
||||
onAdd = { addCurrency() },
|
||||
onDelete = { deleteCurrency(it) }
|
||||
onAdd = { addOrUpdateCurrency() },
|
||||
onDelete = { deleteCurrency(it) },
|
||||
onEdit = { startEditing(it) },
|
||||
onCancelEdit = { cancelEditing() }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -105,22 +107,44 @@ class ManageCurrenciesActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun addCurrency() {
|
||||
private fun addOrUpdateCurrency() {
|
||||
val exchangeRate = try { viewModel.newCurrencyRate.toDouble() } catch (_: Exception) { 0.0 }
|
||||
val newCurrency = DBCurrency(
|
||||
0, 0, selectedProjectID,
|
||||
viewModel.newCurrencyName, exchangeRate, DBBill.STATE_ADDED
|
||||
)
|
||||
val currencyName = viewModel.newCurrencyName
|
||||
val editingId = viewModel.editingCurrencyId
|
||||
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db!!.addCurrencyAndSync(newCurrency)
|
||||
if (editingId != null) {
|
||||
db!!.updateCurrency(editingId, currencyName, exchangeRate)
|
||||
val currency = db!!.getCurrency(editingId)
|
||||
if (currency != null) {
|
||||
db!!.setCurrencyStateSync(editingId, DBBill.STATE_EDITED)
|
||||
}
|
||||
} else {
|
||||
val newCurrency = DBCurrency(
|
||||
0, 0, selectedProjectID,
|
||||
currencyName, exchangeRate, DBBill.STATE_ADDED
|
||||
)
|
||||
db!!.addCurrencyAndSync(newCurrency)
|
||||
}
|
||||
}
|
||||
viewModel.newCurrencyName = ""
|
||||
viewModel.newCurrencyRate = ""
|
||||
cancelEditing()
|
||||
updateCurrenciesList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startEditing(currency: DBCurrency) {
|
||||
viewModel.editingCurrencyId = currency.id
|
||||
viewModel.newCurrencyName = currency.name ?: ""
|
||||
viewModel.newCurrencyRate = currency.exchangeRate.toString()
|
||||
}
|
||||
|
||||
private fun cancelEditing() {
|
||||
viewModel.editingCurrencyId = null
|
||||
viewModel.newCurrencyName = ""
|
||||
viewModel.newCurrencyRate = ""
|
||||
}
|
||||
|
||||
private fun deleteCurrency(currency: DBCurrency) {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
||||
+229
-98
@@ -1,24 +1,57 @@
|
||||
package net.helcel.cowspent.android.currencies
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.imePadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.LocalTextStyle
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.OutlinedTextField
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextFieldDefaults
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Done
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import kotlinx.coroutines.delay
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.AlertDialog
|
||||
import net.helcel.cowspent.android.helper.formatAmount
|
||||
import net.helcel.cowspent.model.DBCurrency
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
@Composable
|
||||
fun ManageCurrenciesScreen(
|
||||
@@ -26,7 +59,9 @@ fun ManageCurrenciesScreen(
|
||||
onBack: () -> Unit,
|
||||
onSaveMain: () -> Unit,
|
||||
onAdd: () -> Unit,
|
||||
onDelete: (DBCurrency) -> Unit
|
||||
onDelete: (DBCurrency) -> Unit,
|
||||
onEdit: (DBCurrency) -> Unit,
|
||||
onCancelEdit: () -> Unit
|
||||
) {
|
||||
val dialogState = viewModel.dialogState
|
||||
if (dialogState != null) {
|
||||
@@ -61,7 +96,7 @@ fun ManageCurrenciesScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.currency_manager)) },
|
||||
title = { Text(stringResource(R.string.action_currencies)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
@@ -75,78 +110,152 @@ fun ManageCurrenciesScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.padding(16.dp)
|
||||
.imePadding()
|
||||
.fillMaxSize()
|
||||
.background(MaterialTheme.colors.onSurface.copy(alpha = 0.02f))
|
||||
) {
|
||||
Text(stringResource(R.string.main_currency), style = MaterialTheme.typography.h6)
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.mainCurrencyName,
|
||||
onValueChange = { viewModel.mainCurrencyName = it },
|
||||
label = { Text(stringResource(R.string.currency_edit_name)) },
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Button(onClick = onSaveMain, enabled = viewModel.mainCurrencyName.isNotEmpty()) {
|
||||
Text(stringResource(R.string.save_or_discard_bill_dialog_save))
|
||||
LaunchedEffect(viewModel.mainCurrencyName) {
|
||||
if (viewModel.mainCurrencyName.isNotEmpty()) {
|
||||
delay(500.milliseconds)
|
||||
onSaveMain()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
Text(stringResource(R.string.add_currency_title), style = MaterialTheme.typography.h6)
|
||||
|
||||
// Visual relationship indicator
|
||||
Surface(
|
||||
color = MaterialTheme.colors.primary.copy(alpha = 0.05f),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.Center
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
// Section 1: Main Currency
|
||||
Text(
|
||||
text = stringResource(R.string.main_currency).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = 2.dp,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Text("1 ", style = MaterialTheme.typography.body1)
|
||||
Text(viewModel.mainCurrencyName.ifEmpty { "Base" }, fontWeight = FontWeight.Bold)
|
||||
Text(" = ", style = MaterialTheme.typography.h6)
|
||||
Text(viewModel.newCurrencyRate.ifEmpty { "0.0" }, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.primary)
|
||||
Text(" ")
|
||||
Text(viewModel.newCurrencyName.ifEmpty { "Currency" }, fontWeight = FontWeight.Bold)
|
||||
OutlinedTextField(
|
||||
value = viewModel.mainCurrencyName,
|
||||
onValueChange = { viewModel.mainCurrencyName = it },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
placeholder = { Text("e.g. EUR", fontSize = 14.sp) },
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = Color.Transparent,
|
||||
unfocusedBorderColor = Color.Transparent
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Section 2: Exchange Rates
|
||||
val isEditing = viewModel.editingCurrencyId != null
|
||||
Text(
|
||||
text = (if (isEditing) "Edit exchange rate" else "Add exchange rate").uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
// Integrated Add/Edit Ribbon
|
||||
Surface(
|
||||
color = MaterialTheme.colors.surface,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = 2.dp
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text("1 ", style = MaterialTheme.typography.body2, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
|
||||
Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
text = " = ",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.newCurrencyRate,
|
||||
onValueChange = { viewModel.newCurrencyRate = it },
|
||||
modifier = Modifier.weight(1.2f).height(52.dp),
|
||||
placeholder = { Text("1", fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.ExtraBold, fontSize = 14.sp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
|
||||
)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
OutlinedTextField(
|
||||
value = viewModel.newCurrencyName,
|
||||
onValueChange = { viewModel.newCurrencyName = it },
|
||||
modifier = Modifier.weight(1f).height(52.dp),
|
||||
placeholder = { Text(viewModel.mainCurrencyName.ifEmpty { "$" }, fontSize = 12.sp) },
|
||||
singleLine = true,
|
||||
textStyle = LocalTextStyle.current.copy(fontWeight = FontWeight.Bold, fontSize = 14.sp),
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
colors = TextFieldDefaults.outlinedTextFieldColors(
|
||||
focusedBorderColor = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
unfocusedBorderColor = MaterialTheme.colors.onSurface.copy(alpha = 0.12f),
|
||||
backgroundColor = MaterialTheme.colors.onSurface.copy(alpha = 0.02f)
|
||||
)
|
||||
)
|
||||
|
||||
Row(modifier = Modifier.padding(start = 4.dp)) {
|
||||
IconButton(
|
||||
onClick = onAdd,
|
||||
enabled = viewModel.isAddEnabled()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isEditing) Icons.Default.Done else Icons.Default.Add,
|
||||
contentDescription = null,
|
||||
tint = if (viewModel.isAddEnabled()) {
|
||||
if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary
|
||||
} else MaterialTheme.colors.onSurface.copy(alpha = 0.2f)
|
||||
)
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
IconButton(onClick = onCancelEdit) {
|
||||
Icon(Icons.Default.Close, contentDescription = null, tint = MaterialTheme.colors.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
Text(
|
||||
text = "SAVED RATES",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
OutlinedTextField(
|
||||
value = viewModel.newCurrencyName,
|
||||
onValueChange = { viewModel.newCurrencyName = it },
|
||||
label = { Text(stringResource(R.string.currency_edit_name)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("e.g. USD") }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
OutlinedTextField(
|
||||
value = viewModel.newCurrencyRate,
|
||||
onValueChange = { viewModel.newCurrencyRate = it },
|
||||
label = { Text(stringResource(R.string.currency_rate)) },
|
||||
modifier = Modifier.weight(1f),
|
||||
placeholder = { Text("1.0") }
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(
|
||||
onClick = onAdd,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
enabled = viewModel.isAddEnabled()
|
||||
LazyColumn(
|
||||
modifier = Modifier.weight(1f).fillMaxWidth(),
|
||||
contentPadding = PaddingValues(16.dp, 0.dp, 16.dp, 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Text(stringResource(R.string.simple_add))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
LazyColumn(modifier = Modifier.weight(1f)) {
|
||||
items(viewModel.currencies) { currency ->
|
||||
CurrencyRow(currency, viewModel.mainCurrencyName, onDelete = { onDelete(currency) })
|
||||
CurrencyRow(
|
||||
currency = currency,
|
||||
mainCurrencyName = viewModel.mainCurrencyName,
|
||||
isEditing = viewModel.editingCurrencyId == currency.id,
|
||||
onEdit = { onEdit(currency) },
|
||||
onDelete = { onDelete(currency) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -154,52 +263,70 @@ fun ManageCurrenciesScreen(
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CurrencyRow(currency: DBCurrency, mainCurrencyName: String, onDelete: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
fun CurrencyRow(
|
||||
currency: DBCurrency,
|
||||
mainCurrencyName: String,
|
||||
isEditing: Boolean,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
Card(
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
elevation = if (isEditing) 4.dp else 1.dp,
|
||||
border = if (isEditing) BorderStroke(1.dp, MaterialTheme.colors.secondary.copy(alpha = 0.5f)) else null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
// Table-like layout
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = onEdit)
|
||||
.padding(16.dp, 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = "1 ",
|
||||
style = MaterialTheme.typography.body1,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = mainCurrencyName,
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = " = ",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.weight(1f),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = currency.exchangeRate.toString(),
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.ExtraBold
|
||||
text = "1 ",
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Text(
|
||||
text = currency.name ?: "",
|
||||
style = MaterialTheme.typography.caption,
|
||||
text = mainCurrencyName.ifEmpty { "$" },
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = " = ",
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
Text(
|
||||
text = formatAmount(currency.exchangeRate),
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.ExtraBold,
|
||||
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.onSurface
|
||||
)
|
||||
Text(
|
||||
text = " ${currency.name ?: ""}",
|
||||
style = MaterialTheme.typography.body1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error)
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.error.copy(alpha = 0.7f),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Divider()
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@@ -209,6 +336,8 @@ fun CurrencyRowPreview() {
|
||||
CurrencyRow(
|
||||
currency = DBCurrency(1, 0, 0, "USD", 1.0, 0),
|
||||
mainCurrencyName = "EUR",
|
||||
isEditing = false,
|
||||
onEdit = {},
|
||||
onDelete = {}
|
||||
)
|
||||
}
|
||||
@@ -230,7 +359,9 @@ fun ManageCurrenciesScreenPreview() {
|
||||
onBack = {},
|
||||
onSaveMain = {},
|
||||
onAdd = {},
|
||||
onDelete = {}
|
||||
onDelete = {},
|
||||
onEdit = {},
|
||||
onCancelEdit = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ class ManageCurrenciesViewModel : ViewModel() {
|
||||
var newCurrencyName by mutableStateOf("")
|
||||
var newCurrencyRate by mutableStateOf("")
|
||||
|
||||
var editingCurrencyId by mutableStateOf<Long?>(null)
|
||||
|
||||
var currencies by mutableStateOf<List<DBCurrency>>(emptyList())
|
||||
|
||||
var dialogState by mutableStateOf<DialogState?>(null)
|
||||
|
||||
@@ -100,8 +100,11 @@ fun Drawer(
|
||||
|
||||
// Members Section
|
||||
val membersState = rememberLazyListState()
|
||||
val sortedMembers = remember(members, memberBalances) {
|
||||
members.sortedWith(compareBy<DBMember> {
|
||||
val sortedMembers = remember(members, memberBalances, selectedMemberId) {
|
||||
members.filter {
|
||||
val balance = memberBalances[it.id] ?: 0.0
|
||||
it.isActivated || balance > 0.01 || balance < -0.01 || it.id == selectedMemberId
|
||||
}.sortedWith(compareBy<DBMember> {
|
||||
val balance = memberBalances[it.id] ?: 0.0
|
||||
when {
|
||||
balance > 0.01 -> 0
|
||||
@@ -157,20 +160,27 @@ private fun DrawerHeader(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colors.primary)
|
||||
.padding(16.dp, 4.dp)
|
||||
.padding(4.dp, 4.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(48.dp)
|
||||
)
|
||||
Box(modifier = Modifier.size(48.dp)) {
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_background),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
Image(
|
||||
painter = painterResource(id = R.drawable.ic_launcher_foreground),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
stringResource(id = R.string.app_name),
|
||||
text = stringResource(id = R.string.app_name),
|
||||
color = MaterialTheme.colors.onPrimary,
|
||||
fontSize = 18.sp,
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
@@ -197,6 +207,7 @@ private fun DrawerHeader(
|
||||
}
|
||||
if (lastSyncText.isNotEmpty()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Spacer(Modifier.width(16.dp))
|
||||
Icon(
|
||||
Icons.Default.Sync,
|
||||
contentDescription = null,
|
||||
@@ -288,7 +299,7 @@ fun DrawerItem(
|
||||
Icon(icon, contentDescription = null, tint = contentColor.copy(alpha = 0.6f * alpha))
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
|
||||
val itemText = text ?: member?.name ?: ""
|
||||
Text(
|
||||
|
||||
@@ -230,7 +230,12 @@ fun AlertDialogContent(
|
||||
Spacer(Modifier.width(8.dp))
|
||||
}
|
||||
if (title != null) {
|
||||
Text(text = title, style = MaterialTheme.typography.h6)
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,12 +188,13 @@ fun ColorPicker(
|
||||
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
|
||||
val hueBrush = remember {
|
||||
Brush.horizontalGradient(
|
||||
listOf(
|
||||
Color.Red, Color.Yellow, Color.Green, Color.Cyan, Color.Blue, Color.Magenta, Color.Red
|
||||
)
|
||||
)
|
||||
val hueBrush = remember(lightness, chroma) {
|
||||
val hueSteps = 36 // More steps for smoother gradient
|
||||
val colors = (0..hueSteps).map { step ->
|
||||
val h = (step.toFloat() / hueSteps) * 360f
|
||||
Color(mLCHtoRBG(lightness, chroma, h))
|
||||
}
|
||||
Brush.horizontalGradient(colors)
|
||||
}
|
||||
LchSlider(
|
||||
label = "H",
|
||||
|
||||
@@ -1,20 +1,80 @@
|
||||
package net.helcel.cowspent.android.helper
|
||||
|
||||
import net.helcel.cowspent.util.SupportUtil
|
||||
import java.text.NumberFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.round
|
||||
|
||||
/**
|
||||
* Formats amount using k/M notation (e.g., 1.5k for 1500).
|
||||
*/
|
||||
fun formatShortValue(value: Double): String {
|
||||
val absValue = abs(value)
|
||||
val sign = if (value < 0) "-" else ""
|
||||
return when {
|
||||
value >= 1_000_000 -> String.format(Locale.ROOT, "%.1fM", value / 1_000_000).replace(".0", "")
|
||||
value >= 1_000 -> String.format(Locale.ROOT, "%.1fk", value / 1_000).replace(".0", "")
|
||||
else -> String.format(Locale.ROOT, "%.0f", value)
|
||||
absValue >= 1_000_000 -> sign + formatAmount(absValue / 1_000_000) + "M"
|
||||
absValue >= 1_000 -> sign + formatAmount(absValue / 1_000) + "k"
|
||||
else -> sign + formatAmount(absValue)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats balance with sign and locale-aware number formatting.
|
||||
*/
|
||||
fun formatBalance(balance: Double): String {
|
||||
val rbalance = round(abs(balance) * 100.0) / 100.0
|
||||
val balanceSign = if (balance > 0.01) "+" else if (balance < -0.01) "-" else ""
|
||||
return if (rbalance == 0.0) "" else "$balanceSign${SupportUtil.normalNumberFormat.format(rbalance)}"
|
||||
return if (rbalance == 0.0) "" else "$balanceSign${formatAmount(rbalance)}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats amount using system locale or specified locale.
|
||||
*/
|
||||
fun formatAmount(value: Double, locale: Locale = Locale.getDefault()): String {
|
||||
val formatter = NumberFormat.getNumberInstance(locale)
|
||||
formatter.maximumFractionDigits = 2
|
||||
formatter.minimumFractionDigits = 0
|
||||
return formatter.format(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses amount from string robustly.
|
||||
* Tries common separators and cleans up non-numeric characters (except separators).
|
||||
*/
|
||||
fun parseAmount(input: String?): Double? {
|
||||
if (input.isNullOrBlank()) return null
|
||||
|
||||
// Remove non-numeric characters except for delimiters and minus sign
|
||||
val cleaned = input.replace(Regex("[^0-9,.-]"), "").trim()
|
||||
|
||||
if (cleaned.isEmpty()) return null
|
||||
|
||||
// If there's both a comma and a dot, we assume the last one is the decimal separator
|
||||
val lastDot = cleaned.lastIndexOf('.')
|
||||
val lastComma = cleaned.lastIndexOf(',')
|
||||
|
||||
return if (lastDot != -1 && lastComma != -1) {
|
||||
if (lastDot > lastComma) {
|
||||
// Dot is decimal, comma is thousands
|
||||
cleaned.replace(",", "").toDoubleOrNull()
|
||||
} else {
|
||||
// Comma is decimal, dot is thousands
|
||||
cleaned.replace(".", "").replace(",", ".").toDoubleOrNull()
|
||||
}
|
||||
} else if (lastComma != -1) {
|
||||
// Only comma. Could be decimal or thousands.
|
||||
if (cleaned.count { it == ',' } == 1) {
|
||||
cleaned.replace(',', '.').toDoubleOrNull()
|
||||
} else {
|
||||
// Multiple commas -> thousands
|
||||
cleaned.replace(",", "").toDoubleOrNull()
|
||||
}
|
||||
} else {
|
||||
// Only dots or no separators
|
||||
if (cleaned.count { it == '.' } > 1) {
|
||||
cleaned.replace(".", "").toDoubleOrNull()
|
||||
} else {
|
||||
cleaned.toDoubleOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,37 +4,49 @@ import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.util.Log
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import androidx.camera.core.*
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ExperimentalGetImage
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TopAppBar
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.zxing.*
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import com.google.zxing.BarcodeFormat
|
||||
import com.google.zxing.BinaryBitmap
|
||||
import com.google.zxing.DecodeHintType
|
||||
import com.google.zxing.MultiFormatReader
|
||||
import com.google.zxing.PlanarYUVLuminanceSource
|
||||
import com.google.zxing.Result
|
||||
import com.google.zxing.common.HybridBinarizer
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.main.MainConstants
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@@ -122,7 +134,7 @@ fun QrCodeScannerScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.scan_qrcode)) },
|
||||
title = { Text(stringResource(R.string.action_scan_qrcode)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package net.helcel.cowspent.android.label
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
|
||||
class LabelManagementActivity : AppCompatActivity() {
|
||||
private val viewModel: LabelManagementViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, 0L)
|
||||
if (projectId == 0L) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.loadLabels(projectId)
|
||||
|
||||
setContent {
|
||||
ThemeUtils.CowspentTheme {
|
||||
LabelManagementScreen(
|
||||
viewModel = viewModel,
|
||||
onBack = { finish() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PROJECT_ID = "EXTRA_PROJECT_ID"
|
||||
|
||||
fun createIntent(context: Context, projectId: Long): Intent {
|
||||
return Intent(context, LabelManagementActivity::class.java).apply {
|
||||
putExtra(EXTRA_PROJECT_ID, projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
package net.helcel.cowspent.android.label
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.graphics.toColorInt
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.ColorPicker
|
||||
import net.helcel.cowspent.android.helper.StatefulAlertDialog
|
||||
import net.helcel.cowspent.model.DBCategory
|
||||
import net.helcel.cowspent.model.DBPaymentMode
|
||||
|
||||
@Composable
|
||||
fun LabelManagementScreen(
|
||||
viewModel: LabelManagementViewModel,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
LabelManagementScreenContent(
|
||||
categories = viewModel.categories,
|
||||
paymentModes = viewModel.paymentModes,
|
||||
dialogState = viewModel.dialogState,
|
||||
onBack = onBack,
|
||||
onAddCategory = viewModel::addCategory,
|
||||
onUpdateCategory = viewModel::updateCategory,
|
||||
onDeleteCategory = viewModel::deleteCategory,
|
||||
onAddPaymentMode = viewModel::addPaymentMode,
|
||||
onUpdatePaymentMode = viewModel::updatePaymentMode,
|
||||
onDeletePaymentMode = viewModel::deletePaymentMode,
|
||||
onDismissDialog = { viewModel.dialogState = null },
|
||||
onShowDialog = { viewModel.dialogState = it }
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelManagementScreenContent(
|
||||
categories: List<DBCategory>,
|
||||
paymentModes: List<DBPaymentMode>,
|
||||
dialogState: net.helcel.cowspent.android.helper.DialogState?,
|
||||
onBack: () -> Unit,
|
||||
onAddCategory: (String, String, String) -> Unit,
|
||||
onUpdateCategory: (Long, String, String, String) -> Unit,
|
||||
onDeleteCategory: (Long) -> Unit,
|
||||
onAddPaymentMode: (String, String, String) -> Unit,
|
||||
onUpdatePaymentMode: (Long, String, String, String) -> Unit,
|
||||
onDeletePaymentMode: (Long) -> Unit,
|
||||
onDismissDialog: () -> Unit,
|
||||
onShowDialog: (net.helcel.cowspent.android.helper.DialogState) -> Unit,
|
||||
initialTab: Int = 0
|
||||
) {
|
||||
var selectedTab by remember { mutableIntStateOf(initialTab) }
|
||||
val tabs = listOf(
|
||||
stringResource(R.string.label_categories),
|
||||
stringResource(R.string.label_payment_modes)
|
||||
)
|
||||
|
||||
val deleteLabelTitle = stringResource(R.string.delete_label_confirmation_title)
|
||||
val deleteLabelMessage = stringResource(R.string.delete_label_confirmation_message)
|
||||
val yesText = stringResource(R.string.simple_yes)
|
||||
val noText = stringResource(R.string.simple_no)
|
||||
|
||||
var showEditDialog by remember { mutableStateOf(false) }
|
||||
var editingCategory by remember { mutableStateOf<DBCategory?>(null) }
|
||||
var editingPaymentMode by remember { mutableStateOf<DBPaymentMode?>(null) }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.title_labels)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
elevation = 0.dp
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = {
|
||||
if (selectedTab == 0) {
|
||||
editingCategory = null
|
||||
showEditDialog = true
|
||||
} else {
|
||||
editingPaymentMode = null
|
||||
showEditDialog = true
|
||||
}
|
||||
}) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(modifier = Modifier.padding(padding)) {
|
||||
TabRow(selectedTabIndex = selectedTab) {
|
||||
tabs.forEachIndexed { index, title ->
|
||||
Tab(
|
||||
selected = selectedTab == index,
|
||||
onClick = { selectedTab = index },
|
||||
text = { Text(title) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedTab == 0) {
|
||||
CategoryList(
|
||||
categories = categories,
|
||||
onEdit = {
|
||||
editingCategory = it
|
||||
showEditDialog = true
|
||||
},
|
||||
onDelete = { category ->
|
||||
onShowDialog(net.helcel.cowspent.android.helper.DialogState(
|
||||
title = deleteLabelTitle,
|
||||
message = deleteLabelMessage,
|
||||
positiveText = yesText,
|
||||
negativeText = noText,
|
||||
onConfirm = { onDeleteCategory(category.id) }
|
||||
))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
PaymentModeList(
|
||||
paymentModes = paymentModes,
|
||||
onEdit = {
|
||||
editingPaymentMode = it
|
||||
showEditDialog = true
|
||||
},
|
||||
onDelete = { pm ->
|
||||
onShowDialog(net.helcel.cowspent.android.helper.DialogState(
|
||||
title = deleteLabelTitle,
|
||||
message = deleteLabelMessage,
|
||||
positiveText = yesText,
|
||||
negativeText = noText,
|
||||
onConfirm = { onDeletePaymentMode(pm.id) }
|
||||
))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (showEditDialog) {
|
||||
if (selectedTab == 0) {
|
||||
EditLabelDialog(
|
||||
title = if (editingCategory == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit),
|
||||
initialName = editingCategory?.name ?: "",
|
||||
initialIcon = editingCategory?.icon ?: "",
|
||||
initialColor = editingCategory?.color ?: "#FF0000",
|
||||
onDismiss = { showEditDialog = false },
|
||||
onSave = { name, icon, color ->
|
||||
if (editingCategory == null) {
|
||||
onAddCategory(name, icon, color)
|
||||
} else {
|
||||
onUpdateCategory(editingCategory!!.id, name, icon, color)
|
||||
}
|
||||
showEditDialog = false
|
||||
}
|
||||
)
|
||||
} else {
|
||||
EditLabelDialog(
|
||||
title = if (editingPaymentMode == null) stringResource(R.string.action_add_project) else stringResource(R.string.action_edit),
|
||||
initialName = editingPaymentMode?.name ?: "",
|
||||
initialIcon = editingPaymentMode?.icon ?: "",
|
||||
initialColor = editingPaymentMode?.color ?: "#00FF00",
|
||||
onDismiss = { showEditDialog = false },
|
||||
onSave = { name, icon, color ->
|
||||
if (editingPaymentMode == null) {
|
||||
onAddPaymentMode(name, icon, color)
|
||||
} else {
|
||||
onUpdatePaymentMode(editingPaymentMode!!.id, name, icon, color)
|
||||
}
|
||||
showEditDialog = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
StatefulAlertDialog(
|
||||
state = dialogState,
|
||||
onDismissRequest = onDismissDialog
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CategoryList(
|
||||
categories: List<DBCategory>,
|
||||
onEdit: (DBCategory) -> Unit,
|
||||
onDelete: (DBCategory) -> Unit
|
||||
) {
|
||||
LazyColumn {
|
||||
items(categories) { category ->
|
||||
LabelItem(
|
||||
name = category.name ?: "",
|
||||
icon = category.icon,
|
||||
color = category.color,
|
||||
onEdit = { onEdit(category) },
|
||||
onDelete = { onDelete(category) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PaymentModeList(
|
||||
paymentModes: List<DBPaymentMode>,
|
||||
onEdit: (DBPaymentMode) -> Unit,
|
||||
onDelete: (DBPaymentMode) -> Unit
|
||||
) {
|
||||
LazyColumn {
|
||||
items(paymentModes) { pm ->
|
||||
LabelItem(
|
||||
name = pm.name ?: "",
|
||||
icon = pm.icon,
|
||||
color = pm.color,
|
||||
onEdit = { onEdit(pm) },
|
||||
onDelete = { onDelete(pm) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun LabelItem(
|
||||
name: String,
|
||||
icon: String,
|
||||
color: String,
|
||||
onEdit: () -> Unit,
|
||||
onDelete: () -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onEdit() }
|
||||
.padding(16.dp, 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.background(parseColor(color)),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(text = icon, fontSize = 12.sp)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Text(text = name, modifier = Modifier.weight(1f), style = MaterialTheme.typography.subtitle1)
|
||||
IconButton(onClick = onEdit) {
|
||||
Icon(Icons.Default.Edit, contentDescription = null, tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
|
||||
}
|
||||
IconButton(onClick = onDelete) {
|
||||
Icon(Icons.Default.Delete, contentDescription = null, tint = MaterialTheme.colors.error.copy(alpha = 0.6f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EditLabelDialog(
|
||||
title: String,
|
||||
initialName: String,
|
||||
initialIcon: String,
|
||||
initialColor: String,
|
||||
onDismiss: () -> Unit,
|
||||
onSave: (String, String, String) -> Unit
|
||||
) {
|
||||
var name by remember { mutableStateOf(initialName) }
|
||||
var icon by remember { mutableStateOf(initialIcon) }
|
||||
var color by remember { mutableStateOf(initialColor) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
title = { Text(title) },
|
||||
text = {
|
||||
Column(modifier = Modifier.verticalScroll(rememberScrollState())) {
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
placeholder = { Text(stringResource(R.string.label_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
OutlinedTextField(
|
||||
value = icon,
|
||||
onValueChange = { if (it.length <= 2) icon = it },
|
||||
placeholder = { Text(stringResource(R.string.label_icon)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
singleLine = true
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(text = stringResource(R.string.label_color), style = MaterialTheme.typography.caption)
|
||||
ColorPicker(
|
||||
initialColor = parseColorInt(color),
|
||||
onColorChanged = { color = String.format("#%06X", 0xFFFFFF and it) }
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { onSave(name, icon, color) },
|
||||
enabled = name.isNotBlank()
|
||||
) {
|
||||
Text(stringResource(R.string.action_save))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.simple_cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun parseColor(colorString: String): Color {
|
||||
return try {
|
||||
Color(colorString.toColorInt())
|
||||
} catch (_: Exception) {
|
||||
Color.Gray
|
||||
}
|
||||
}
|
||||
|
||||
fun parseColorInt(colorString: String): Int {
|
||||
return try {
|
||||
colorString.toColorInt()
|
||||
} catch (_: Exception) {
|
||||
Color.Gray.toArgb()
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LabelManagementCategoriesPreview() {
|
||||
MaterialTheme {
|
||||
LabelManagementScreenContent(
|
||||
categories = listOf(
|
||||
DBCategory(1, 1, 1, "Groceries", "🛒", "#FF0000"),
|
||||
DBCategory(2, 2, 1, "Rent", "🏠", "#00FF00")
|
||||
),
|
||||
paymentModes = emptyList(),
|
||||
dialogState = null,
|
||||
onBack = {},
|
||||
onAddCategory = { _, _, _ -> },
|
||||
onUpdateCategory = { _, _, _, _ -> },
|
||||
onDeleteCategory = { _ -> },
|
||||
onAddPaymentMode = { _, _, _ -> },
|
||||
onUpdatePaymentMode = { _, _, _, _ -> },
|
||||
onDeletePaymentMode = { _ -> },
|
||||
onDismissDialog = {},
|
||||
onShowDialog = {},
|
||||
initialTab = 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun LabelManagementPaymentModesPreview() {
|
||||
MaterialTheme {
|
||||
LabelManagementScreenContent(
|
||||
categories = emptyList(),
|
||||
paymentModes = listOf(
|
||||
DBPaymentMode(1, 1, 1, "Cash", "💵", "#0000FF"),
|
||||
DBPaymentMode(2, 2, 1, "Credit Card", "💳", "#FFFF00")
|
||||
),
|
||||
dialogState = null,
|
||||
onBack = {},
|
||||
onAddCategory = { _, _, _ -> },
|
||||
onUpdateCategory = { _, _, _, _ -> },
|
||||
onDeleteCategory = { _ -> },
|
||||
onAddPaymentMode = { _, _, _ -> },
|
||||
onUpdatePaymentMode = { _, _, _, _ -> },
|
||||
onDeletePaymentMode = { _ -> },
|
||||
onDismissDialog = {},
|
||||
onShowDialog = {},
|
||||
initialTab = 1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package net.helcel.cowspent.android.label
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.cowspent.android.helper.DialogState
|
||||
import net.helcel.cowspent.model.DBCategory
|
||||
import net.helcel.cowspent.model.DBPaymentMode
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import net.helcel.cowspent.util.CategoryUtils
|
||||
|
||||
class LabelManagementViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val db = CowspentSQLiteOpenHelper.getInstance(application)
|
||||
|
||||
var projectId by mutableLongStateOf(0L)
|
||||
var categories by mutableStateOf<List<DBCategory>>(emptyList())
|
||||
var paymentModes by mutableStateOf<List<DBPaymentMode>>(emptyList())
|
||||
|
||||
var dialogState by mutableStateOf<DialogState?>(null)
|
||||
|
||||
fun loadLabels(projId: Long) {
|
||||
projectId = projId
|
||||
viewModelScope.launch {
|
||||
val cats = withContext(Dispatchers.IO) { db.getCategories(projId) }
|
||||
val pms = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
|
||||
|
||||
if (cats.isEmpty()) {
|
||||
val defaults = CategoryUtils.getDefaultCategories(getApplication(), projId)
|
||||
withContext(Dispatchers.IO) {
|
||||
defaults.forEach { db.addCategory(it) }
|
||||
}
|
||||
categories = withContext(Dispatchers.IO) { db.getCategories(projId) }
|
||||
} else {
|
||||
categories = cats
|
||||
}
|
||||
|
||||
if (pms.isEmpty()) {
|
||||
val defaults = CategoryUtils.getDefaultPaymentModes(getApplication(), projId)
|
||||
withContext(Dispatchers.IO) {
|
||||
defaults.forEach { db.addPaymentMode(it) }
|
||||
}
|
||||
paymentModes = withContext(Dispatchers.IO) { db.getPaymentModes(projId) }
|
||||
} else {
|
||||
paymentModes = pms
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun addCategory(name: String, icon: String, color: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.addCategory(DBCategory(0, 0, projectId, name, icon, color))
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateCategory(id: Long, name: String, icon: String, color: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.updateCategory(id, name, icon, color)
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteCategory(id: Long) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deleteCategory(id)
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPaymentMode(name: String, icon: String, color: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.addPaymentMode(DBPaymentMode(0, 0, projectId, name, icon, color))
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
fun updatePaymentMode(id: Long, name: String, icon: String, color: String) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.updatePaymentMode(id, name, icon, color)
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
|
||||
fun deletePaymentMode(id: Long) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deletePaymentMode(id)
|
||||
}
|
||||
loadLabels(projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,25 @@ package net.helcel.cowspent.android.main
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.Button
|
||||
import androidx.compose.material.Divider
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Receipt
|
||||
import androidx.compose.material.icons.filled.Repeat
|
||||
@@ -13,6 +29,7 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.helcel.cowspent.R
|
||||
@@ -28,15 +45,25 @@ fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Un
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.no_projects_title), style = MaterialTheme.typography.h6)
|
||||
Text(
|
||||
text = stringResource(R.string.error_no_projects).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(stringResource(R.string.no_projects_text))
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Button(onClick = onConfigureNextcloud) {
|
||||
Text(
|
||||
text = stringResource(R.string.no_projects_text),
|
||||
style = MaterialTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = onConfigureNextcloud, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.configure_account_choice))
|
||||
}
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Button(onClick = onAddManually) {
|
||||
Button(onClick = onAddManually, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.add_project_choice))
|
||||
}
|
||||
}
|
||||
@@ -49,9 +76,19 @@ fun EmptyMembersState() {
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.no_members_title), style = MaterialTheme.typography.h6)
|
||||
Text(
|
||||
text = stringResource(R.string.error_no_members).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(stringResource(R.string.no_members_text))
|
||||
Text(
|
||||
text = stringResource(R.string.no_members_text),
|
||||
style = MaterialTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,9 +99,19 @@ fun EmptyBillsState() {
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6)
|
||||
Text(
|
||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(stringResource(R.string.no_bills_text))
|
||||
Text(
|
||||
text = stringResource(R.string.no_bills_text),
|
||||
style = MaterialTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,15 +153,21 @@ fun BillItemRow(bill: DBBill, payer: DBMember?, onClick: () -> Unit) {
|
||||
Icons.Default.Receipt,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(40.dp),
|
||||
tint = MaterialTheme.colors.primary
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
Text(bill.formattedWhat.ifEmpty { bill.what }, fontWeight = FontWeight.Bold)
|
||||
Text(
|
||||
text = bill.formattedWhat.ifEmpty { bill.what },
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Text(
|
||||
text = bill.formattedSubtitle.ifEmpty { bill.comment ?: "" },
|
||||
style = MaterialTheme.typography.caption,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
@@ -135,13 +188,16 @@ fun SectionHeader(title: String) {
|
||||
color = MaterialTheme.colors.background,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Divider(thickness = 2.dp)
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||
style = MaterialTheme.typography.caption.copy(fontWeight = FontWeight.Bold),
|
||||
color = MaterialTheme.colors.primary
|
||||
)
|
||||
Column {
|
||||
Divider(thickness = 1.dp, color = MaterialTheme.colors.onSurface.copy(alpha = 0.08f))
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,7 +208,18 @@ fun EmptyState() {
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(stringResource(R.string.no_bills_title), style = MaterialTheme.typography.h6)
|
||||
Text(stringResource(R.string.no_bills_text), modifier = Modifier.padding(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface
|
||||
)
|
||||
Text(
|
||||
text = stringResource(R.string.no_bills_text),
|
||||
modifier = Modifier.padding(16.dp),
|
||||
style = MaterialTheme.typography.body2,
|
||||
textAlign = TextAlign.Center,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package net.helcel.cowspent.android.main
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
@@ -46,6 +45,7 @@ import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.RectangleShape
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
@@ -57,14 +57,12 @@ import net.helcel.cowspent.android.drawer.Drawer
|
||||
import net.helcel.cowspent.android.helper.StatefulAlertDialog
|
||||
import net.helcel.cowspent.android.project.ProjectOptionsDialogContent
|
||||
import net.helcel.cowspent.android.project.ProjectShareDialogContent
|
||||
import net.helcel.cowspent.android.project.member.MemberAddDialogContent
|
||||
import net.helcel.cowspent.android.project.member.MemberEditDialogContent
|
||||
import net.helcel.cowspent.android.project.member.MemberManagementDialogContent
|
||||
import net.helcel.cowspent.android.project.settle.ProjectSettlementDialogContent
|
||||
import net.helcel.cowspent.android.statistics.ProjectStatisticsActivity
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.model.DBProject
|
||||
import net.helcel.cowspent.model.ProjectType
|
||||
import net.helcel.cowspent.model.SectionItem
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import net.helcel.cowspent.util.IRefreshBillsListCallback
|
||||
@@ -103,7 +101,6 @@ fun BillsListScreen(
|
||||
|
||||
val pullRefreshState = rememberPullRefreshState(viewModel.isRefreshing, onRefresh)
|
||||
val context = LocalContext.current
|
||||
val memberAlreadyExistsError = stringResource(R.string.member_already_exists)
|
||||
val sharedPreferences = remember { PreferenceManager.getDefaultSharedPreferences(context) }
|
||||
val showArchived = sharedPreferences.getBoolean(stringResource(R.string.pref_key_show_archived), false)
|
||||
|
||||
@@ -137,6 +134,10 @@ fun BillsListScreen(
|
||||
onProjectAction(projectOptionsProjectId, 3)
|
||||
viewModel.showProjectOptionsDialogByProjectId = null
|
||||
},
|
||||
onManageLabels = {
|
||||
onProjectAction(projectOptionsProjectId, 8)
|
||||
viewModel.showProjectOptionsDialogByProjectId = null
|
||||
},
|
||||
onStatistics = {
|
||||
onProjectAction(projectOptionsProjectId, 4)
|
||||
viewModel.showProjectOptionsDialogByProjectId = null
|
||||
@@ -155,6 +156,7 @@ fun BillsListScreen(
|
||||
},
|
||||
onDismiss = { viewModel.showProjectOptionsDialogByProjectId = null },
|
||||
isArchived = proj?.isArchived == true,
|
||||
projectType = proj?.type ?: ProjectType.LOCAL,
|
||||
accessLevel = proj?.myAccessLevel ?: DBProject.ACCESS_LEVEL_ADMIN,
|
||||
isShareable = proj?.isShareable() ?: true
|
||||
)
|
||||
@@ -208,114 +210,17 @@ fun BillsListScreen(
|
||||
|
||||
val manageMembersProjectId = viewModel.showMemberManagementDialogByProjectId
|
||||
if (manageMembersProjectId != null) {
|
||||
val members = viewModel.members // Use members from ViewModel as they are already loaded
|
||||
Dialog(
|
||||
onDismissRequest = { viewModel.showMemberManagementDialogByProjectId = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
MemberManagementDialogContent(
|
||||
members = members,
|
||||
onAddMember = {
|
||||
viewModel.showAddMemberDialogByProjectId = manageMembersProjectId
|
||||
viewModel.showMemberManagementDialogByProjectId = null
|
||||
},
|
||||
onEditMember = { member ->
|
||||
viewModel.showEditMemberDialogByProjectId = member.id
|
||||
viewModel.showMemberManagementDialogByProjectId = null
|
||||
},
|
||||
onDismiss = { viewModel.showMemberManagementDialogByProjectId = null }
|
||||
)
|
||||
}
|
||||
viewModel.showMemberManagementDialogByProjectId = null
|
||||
}
|
||||
|
||||
val addMemberProjectId = viewModel.showAddMemberDialogByProjectId
|
||||
if (addMemberProjectId != null) {
|
||||
Dialog(
|
||||
onDismissRequest = { viewModel.showAddMemberDialogByProjectId = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
MemberAddDialogContent(
|
||||
onAdd = { memberName ->
|
||||
val memberNames =
|
||||
db.getMembersOfProject(addMemberProjectId, null).map { it.name }
|
||||
if (memberNames.contains(memberName)) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
memberAlreadyExistsError,
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
} else {
|
||||
val color = net.helcel.cowspent.android.helper.TextDrawable.getColorFromName(memberName)
|
||||
db.addMemberAndSync(
|
||||
DBMember(
|
||||
0,
|
||||
0,
|
||||
addMemberProjectId,
|
||||
memberName,
|
||||
true,
|
||||
1.0,
|
||||
DBBill.STATE_ADDED,
|
||||
android.graphics.Color.red(color),
|
||||
android.graphics.Color.green(color),
|
||||
android.graphics.Color.blue(color),
|
||||
null,
|
||||
null
|
||||
)
|
||||
)
|
||||
refreshCallback.refreshLists(false)
|
||||
viewModel.showAddMemberDialogByProjectId = null
|
||||
viewModel.showMemberManagementDialogByProjectId = addMemberProjectId
|
||||
}
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.showAddMemberDialogByProjectId = null
|
||||
viewModel.showMemberManagementDialogByProjectId = addMemberProjectId
|
||||
}
|
||||
)
|
||||
}
|
||||
viewModel.showAddMemberDialogByProjectId = null
|
||||
}
|
||||
|
||||
val editMemberId = viewModel.showEditMemberDialogByProjectId
|
||||
if (editMemberId != null) {
|
||||
val memberToEdit = remember(editMemberId, viewModel.members) {
|
||||
viewModel.members.find { it.id == editMemberId }
|
||||
}
|
||||
if (memberToEdit != null) {
|
||||
Dialog(
|
||||
onDismissRequest = { viewModel.showEditMemberDialogByProjectId = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
MemberEditDialogContent(
|
||||
member = memberToEdit,
|
||||
onSave = { name, weight, isActivated, r, g, b ->
|
||||
db.updateMemberAndSync(
|
||||
memberToEdit,
|
||||
name,
|
||||
weight,
|
||||
isActivated,
|
||||
r,
|
||||
g,
|
||||
b,
|
||||
"",
|
||||
""
|
||||
)
|
||||
refreshCallback.refreshLists(false)
|
||||
viewModel.showEditMemberDialogByProjectId = null
|
||||
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
|
||||
},
|
||||
onDelete = {
|
||||
db.deleteMember(editMemberId)
|
||||
refreshCallback.refreshLists(false)
|
||||
viewModel.showEditMemberDialogByProjectId = null
|
||||
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
|
||||
},
|
||||
onDismiss = {
|
||||
viewModel.showEditMemberDialogByProjectId = null
|
||||
viewModel.showMemberManagementDialogByProjectId = memberToEdit.projectId
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
viewModel.showEditMemberDialogByProjectId = null
|
||||
}
|
||||
|
||||
val shareProjectId = viewModel.showShareDialogByProjectId
|
||||
@@ -324,8 +229,8 @@ fun BillsListScreen(
|
||||
viewModel.projects.find { it.id == shareProjectId }
|
||||
}
|
||||
if (proj != null) {
|
||||
val shareIntentTitle = stringResource(R.string.share_share_intent_title, proj.name)
|
||||
val shareChooserTitle = stringResource(R.string.share_share_chooser_title, proj.name)
|
||||
val shareIntentTitle = stringResource(R.string.share_intent_title, proj.name)
|
||||
val shareChooserTitle = stringResource(R.string.share_chooser_title, proj.name)
|
||||
Dialog(
|
||||
onDismissRequest = { viewModel.showShareDialogByProjectId = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
@@ -358,7 +263,13 @@ fun BillsListScreen(
|
||||
TextField(
|
||||
value = viewModel.searchQuery,
|
||||
onValueChange = { viewModel.searchQuery = it },
|
||||
placeholder = { Text(stringResource(R.string.action_search), color = MaterialTheme.colors.onPrimary.copy(alpha = 0.7f)) },
|
||||
placeholder = {
|
||||
Text(
|
||||
text = stringResource(R.string.action_search),
|
||||
color = MaterialTheme.colors.onPrimary.copy(alpha = 0.7f),
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
},
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
@@ -381,8 +292,20 @@ fun BillsListScreen(
|
||||
)
|
||||
} else {
|
||||
Column {
|
||||
if (viewModel.title.isNotEmpty()) Text(viewModel.title)
|
||||
else Text(stringResource(R.string.app_name))
|
||||
if (viewModel.title.isNotEmpty()) {
|
||||
Text(
|
||||
text = viewModel.title,
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
else {
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -425,7 +348,7 @@ fun BillsListScreen(
|
||||
val selectedProject = viewModel.projects.find { it.id == viewModel.selectedProjectId }
|
||||
if (selectedProject != null && !selectedProject.isArchived) {
|
||||
FloatingActionButton(onClick = onAddBillClick) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_create_bill))
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_new_bill))
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -65,11 +65,11 @@ object BillsListUtils {
|
||||
memberIdToName: Map<Long, String>
|
||||
) {
|
||||
val projectName = proj.name.ifEmpty { proj.remoteId }
|
||||
var text = context.getString(R.string.share_settle_intro, projectName) + "\n"
|
||||
var text = context.getString(R.string.msg_settle_intro, projectName) + "\n"
|
||||
for (t in transactions) {
|
||||
val amount = round(t.amount * 100.0) / 100.0
|
||||
text += "\n" + context.getString(
|
||||
R.string.share_settle_sentence,
|
||||
R.string.msg_settle_sentence,
|
||||
memberIdToName[t.owerMemberId],
|
||||
memberIdToName[t.receiverMemberId],
|
||||
amount
|
||||
@@ -80,12 +80,12 @@ object BillsListUtils {
|
||||
shareIntent.type = "text/plain"
|
||||
shareIntent.putExtra(
|
||||
Intent.EXTRA_SUBJECT,
|
||||
context.getString(R.string.share_settle_title, projectName)
|
||||
context.getString(R.string.title_settle)
|
||||
)
|
||||
shareIntent.putExtra(Intent.EXTRA_TEXT, text)
|
||||
val chooserIntent = Intent.createChooser(
|
||||
shareIntent,
|
||||
context.getString(R.string.share_settle_title, projectName)
|
||||
context.getString(R.string.title_settle)
|
||||
)
|
||||
context.startActivity(chooserIntent)
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package net.helcel.cowspent.android.main
|
||||
|
||||
import android.Manifest
|
||||
import android.app.SearchManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
@@ -26,10 +24,8 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircleOutline
|
||||
import androidx.compose.material.icons.filled.Sync
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.nextcloud.android.sso.helper.SingleAccountHelper
|
||||
@@ -123,12 +119,12 @@ class BillsListViewActivity :
|
||||
val title: String
|
||||
if (created) {
|
||||
Log.e(TAG, "CREATED !!!")
|
||||
title = getString(R.string.project_create_success_title)
|
||||
message = getString(R.string.project_create_success_message, addedProj?.remoteId)
|
||||
title = getString(R.string.msg_project_added)
|
||||
message = getString(R.string.msg_project_added, addedProj?.remoteId)
|
||||
} else {
|
||||
Log.e(TAG, "ADDED !!!")
|
||||
title = getString(R.string.project_add_success_title)
|
||||
message = getString(R.string.project_add_success_message, addedProj?.remoteId)
|
||||
title = getString(R.string.msg_project_added)
|
||||
message = getString(R.string.msg_project_added, addedProj?.remoteId)
|
||||
}
|
||||
showDialog(message, title, Icons.Default.AddCircleOutline)
|
||||
}
|
||||
@@ -225,7 +221,7 @@ class BillsListViewActivity :
|
||||
lifecycleScope.launch {
|
||||
val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) }
|
||||
if (members.isEmpty()) {
|
||||
showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member))
|
||||
showToast(this@BillsListViewActivity, getString(R.string.error_no_members))
|
||||
} else {
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) }
|
||||
val createIntent = Intent(applicationContext, EditBillActivity::class.java).apply {
|
||||
@@ -259,13 +255,23 @@ class BillsListViewActivity :
|
||||
onProjectAction = { pid, actionIndex ->
|
||||
when (actionIndex) {
|
||||
0 -> onEditProjectClick(pid)
|
||||
1 -> onRemoveProjectClick(pid)
|
||||
1 -> {
|
||||
val proj = db.getProject(pid)
|
||||
if (proj?.type == ProjectType.COSPEND) {
|
||||
onArchiveProjectClick(pid)
|
||||
} else {
|
||||
onRemoveProjectClick(pid)
|
||||
}
|
||||
}
|
||||
2 -> onManageMembersClick(pid)
|
||||
3 -> onManageCurrenciesClick(pid)
|
||||
4 -> onProjectStatisticsClick(pid)
|
||||
5 -> onSettleProjectClick(pid)
|
||||
6 -> onShareProjectClick(pid)
|
||||
7 -> onExportProjectClick(pid)
|
||||
8 -> {
|
||||
startActivity(net.helcel.cowspent.android.label.LabelManagementActivity.createIntent(this@BillsListViewActivity, pid))
|
||||
}
|
||||
}
|
||||
},
|
||||
onAccountSwitcherClick = {
|
||||
@@ -387,17 +393,10 @@ class BillsListViewActivity :
|
||||
|
||||
private fun onEditProjectClick(projectId: Long) {
|
||||
if (projectId == 0L) return
|
||||
lifecycleScope.launch {
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
|
||||
if (proj?.isLocal == false) {
|
||||
val intent = Intent(applicationContext, EditProjectActivity::class.java).apply {
|
||||
putExtra(EditProjectActivity.PARAM_PROJECT_ID, projectId)
|
||||
}
|
||||
editProjectLauncher.launch(intent)
|
||||
} else {
|
||||
showToast(this@BillsListViewActivity, getString(R.string.edit_project_local_impossible))
|
||||
}
|
||||
val intent = Intent(applicationContext, EditProjectActivity::class.java).apply {
|
||||
putExtra(EditProjectActivity.PARAM_PROJECT_ID, projectId)
|
||||
}
|
||||
editProjectLauncher.launch(intent)
|
||||
}
|
||||
|
||||
private fun onRemoveProjectClick(projectId: Long) {
|
||||
@@ -406,8 +405,8 @@ class BillsListViewActivity :
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
|
||||
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.confirm_remove_project_dialog_title),
|
||||
message = if (!proj.isLocal) getString(R.string.confirm_remove_project_dialog_message) else null,
|
||||
title = getString(R.string.title_confirm),
|
||||
message = if (!proj.isLocal) getString(R.string.dialog_confirm_remove_project_msg) else null,
|
||||
positiveText = getString(R.string.simple_yes),
|
||||
onConfirm = {
|
||||
lifecycleScope.launch {
|
||||
@@ -428,6 +427,38 @@ class BillsListViewActivity :
|
||||
}
|
||||
}
|
||||
|
||||
private fun onArchiveProjectClick(projectId: Long) {
|
||||
if (projectId == 0L) return
|
||||
lifecycleScope.launch {
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
|
||||
val isArchiving = !proj.isArchived
|
||||
|
||||
val newArchivedTs = if (isArchiving) System.currentTimeMillis() / 1000 else 0L
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
db.updateProject(
|
||||
projId = projectId,
|
||||
newName = null,
|
||||
newEmail = null,
|
||||
newPassword = null,
|
||||
newLastPayerId = null,
|
||||
newLastSyncedTimestamp = null,
|
||||
newCurrencyName = null,
|
||||
newDeletionDisabled = null,
|
||||
newMyAccessLevel = null,
|
||||
newBearerToken = null,
|
||||
newArchivedTs = newArchivedTs
|
||||
)
|
||||
}
|
||||
|
||||
setupDrawerProjects()
|
||||
refreshLists()
|
||||
|
||||
val msgRes = if (isArchiving) R.string.action_archive else R.string.action_unarchive
|
||||
showToast(this@BillsListViewActivity, getString(msgRes))
|
||||
}
|
||||
}
|
||||
|
||||
fun onManageMembersClick(projectId: Long) {
|
||||
if (projectId == 0L) return
|
||||
lifecycleScope.launch {
|
||||
@@ -437,7 +468,7 @@ class BillsListViewActivity :
|
||||
return@launch
|
||||
}
|
||||
|
||||
viewModel.showMemberManagementDialogByProjectId = projectId
|
||||
startActivity(net.helcel.cowspent.android.project.member.MemberManagementActivity.createIntent(this@BillsListViewActivity, projectId))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +495,7 @@ class BillsListViewActivity :
|
||||
lifecycleScope.launch {
|
||||
val proj = withContext(Dispatchers.IO) { db.getProject(projectId) }
|
||||
if (projectId != 0L && proj?.isShareable() == true) viewModel.showShareDialogByProjectId = projectId
|
||||
else showToast(this@BillsListViewActivity, getString(R.string.share_impossible), Toast.LENGTH_LONG)
|
||||
else showToast(this@BillsListViewActivity, getString(R.string.error_share_impossible), Toast.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +596,7 @@ class BillsListViewActivity :
|
||||
} else {
|
||||
val lastSyncTimestamp = proj.lastSyncedTimestamp ?: 0
|
||||
val cal = Calendar.getInstance().apply { timeInMillis = lastSyncTimestamp * 1000 }
|
||||
val text = getString(R.string.drawer_last_sync_text, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
|
||||
val text = getString(R.string.drawer_last_sync, cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE))
|
||||
viewModel.lastSyncText = text
|
||||
}
|
||||
}
|
||||
@@ -663,7 +694,7 @@ class BillsListViewActivity :
|
||||
lifecycleScope.launch {
|
||||
val members = withContext(Dispatchers.IO) { db.getActivatedMembersOfProject(selectedProjectId) }
|
||||
if (members.isEmpty()) {
|
||||
showToast(this@BillsListViewActivity, getString(R.string.add_bill_impossible_no_member))
|
||||
showToast(this@BillsListViewActivity, getString(R.string.error_no_member))
|
||||
} else {
|
||||
val projType = withContext(Dispatchers.IO) { db.getProject(selectedProjectId)?.type?.id }
|
||||
val intent = Intent(applicationContext, EditBillActivity::class.java).apply {
|
||||
@@ -777,8 +808,8 @@ class BillsListViewActivity :
|
||||
lifecycleScope.launch {
|
||||
val project = withContext(Dispatchers.IO) { db.getProject(projectId) } ?: return@launch
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.sync_error_dialog_title),
|
||||
message = getString(R.string.sync_error_dialog_full_content, project.name, errorMessage),
|
||||
title = getString(R.string.dialog_sync_error_title),
|
||||
message = getString(R.string.dialog_sync_error_msg, project.name, errorMessage),
|
||||
positiveText = getString(R.string.simple_close),
|
||||
icon = Icons.Default.Sync
|
||||
)
|
||||
@@ -798,7 +829,7 @@ class BillsListViewActivity :
|
||||
}
|
||||
MainConstants.BROADCAST_SSO_TOKEN_MISMATCH -> {
|
||||
viewModel.showDialog(
|
||||
title = getString(R.string.sync_error_dialog_title),
|
||||
title = getString(R.string.dialog_sync_error_title),
|
||||
message = getString(R.string.error_token_mismatch),
|
||||
positiveText = getString(R.string.simple_close),
|
||||
icon = Icons.Default.Sync
|
||||
|
||||
@@ -4,19 +4,21 @@ import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.Label
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import net.helcel.cowspent.R
|
||||
|
||||
import net.helcel.cowspent.model.DBProject
|
||||
import net.helcel.cowspent.model.ProjectType
|
||||
|
||||
@Composable
|
||||
fun ProjectOptionsDialogContent(
|
||||
@@ -24,12 +26,14 @@ fun ProjectOptionsDialogContent(
|
||||
onRemoveProject: () -> Unit,
|
||||
onManageMembers: () -> Unit,
|
||||
onManageCurrencies: () -> Unit,
|
||||
onManageLabels: () -> Unit,
|
||||
onStatistics: () -> Unit,
|
||||
onSettle: () -> Unit,
|
||||
onShareProject: () -> Unit,
|
||||
onExportProject: () -> Unit,
|
||||
onDismiss: () -> Unit,
|
||||
isArchived: Boolean = false,
|
||||
projectType: ProjectType = ProjectType.LOCAL,
|
||||
accessLevel: Int = DBProject.ACCESS_LEVEL_ADMIN,
|
||||
isShareable: Boolean = true
|
||||
) {
|
||||
@@ -44,46 +48,71 @@ fun ProjectOptionsDialogContent(
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.choose_project_management_action),
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = stringResource(R.string.choose_project_management_action).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
val options = mutableListOf<ProjectOption>()
|
||||
val isMaintainer = accessLevel >= DBProject.ACCESS_LEVEL_MAINTAINER || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN
|
||||
val isParticipant = accessLevel >= DBProject.ACCESS_LEVEL_PARTICIPANT || accessLevel == DBProject.ACCESS_LEVEL_UNKNOWN
|
||||
|
||||
val row1 = mutableListOf<ProjectOption>()
|
||||
val row2 = mutableListOf<ProjectOption>()
|
||||
val row3 = mutableListOf<ProjectOption>()
|
||||
|
||||
if (!isArchived && isMaintainer) {
|
||||
options.add(ProjectOption(stringResource(R.string.action_edit_project), Icons.Default.Edit, onEditProject))
|
||||
}
|
||||
options.add(ProjectOption(stringResource(R.string.fab_rm_project), Icons.Default.Delete, onRemoveProject))
|
||||
if (!isArchived && isMaintainer) {
|
||||
options.add(ProjectOption(stringResource(R.string.fab_manage_members), Icons.Default.Group, onManageMembers))
|
||||
options.add(ProjectOption(stringResource(R.string.fab_manage_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
|
||||
}
|
||||
options.add(ProjectOption(stringResource(R.string.fab_statistics), Icons.Default.BarChart, onStatistics))
|
||||
if (!isArchived && isParticipant) {
|
||||
options.add(ProjectOption(stringResource(R.string.fab_settle), Icons.Default.Handshake, onSettle))
|
||||
row1.add(ProjectOption(stringResource(R.string.action_edit), Icons.Default.Edit, onEditProject))
|
||||
}
|
||||
if (isShareable && isParticipant) {
|
||||
options.add(ProjectOption(stringResource(R.string.action_share_project), Icons.Default.Share, onShareProject))
|
||||
row1.add(ProjectOption(stringResource(R.string.action_share), Icons.Default.Share, onShareProject))
|
||||
}
|
||||
if (projectType == ProjectType.COSPEND) {
|
||||
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
|
||||
row1.add(ProjectOption(archiveLabel, archiveIcon, onRemoveProject))
|
||||
} else {
|
||||
row1.add(ProjectOption(stringResource(R.string.action_delete), Icons.Default.Delete, onRemoveProject))
|
||||
}
|
||||
options.add(ProjectOption(stringResource(R.string.fab_export_project), Icons.Default.Download, onExportProject))
|
||||
|
||||
// Simple 2-column grid using Rows
|
||||
for (i in options.indices step 2) {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
ProjectOptionItem(
|
||||
option = options[i],
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
if (i + 1 < options.size) {
|
||||
// Row 2: Manage Member, Manage Labels, Manage Currencies
|
||||
if (!isArchived && isMaintainer) {
|
||||
row2.add(ProjectOption(stringResource(R.string.action_members), Icons.Default.Group, onManageMembers))
|
||||
if (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_currencies), Icons.Default.MonetizationOn, onManageCurrencies))
|
||||
}
|
||||
|
||||
// Row 3: Statistics, Settle, Export
|
||||
row3.add(ProjectOption(stringResource(R.string.action_stats), Icons.Default.BarChart, onStatistics))
|
||||
if (!isArchived && isParticipant) {
|
||||
row3.add(ProjectOption(stringResource(R.string.action_settle), Icons.Default.Handshake, onSettle))
|
||||
}
|
||||
row3.add(ProjectOption(stringResource(R.string.action_export), Icons.Default.Download, onExportProject))
|
||||
|
||||
val allRows = listOf(row1, row2, row3).filter { it.isNotEmpty() }
|
||||
|
||||
allRows.forEach { rowOptions ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.Center
|
||||
) {
|
||||
val emptySpace = 3 - rowOptions.size
|
||||
if (emptySpace > 0) {
|
||||
Spacer(modifier = Modifier.weight(emptySpace / 2f))
|
||||
}
|
||||
|
||||
rowOptions.forEach { option ->
|
||||
ProjectOptionItem(
|
||||
option = options[i + 1],
|
||||
option = option,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
if (emptySpace > 0) {
|
||||
Spacer(modifier = Modifier.weight(emptySpace / 2f))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -145,10 +174,59 @@ fun ProjectOptionsDialogPreview() {
|
||||
onManageCurrencies = {},
|
||||
onStatistics = {},
|
||||
onSettle = {},
|
||||
onManageLabels = {},
|
||||
onShareProject = {},
|
||||
onExportProject = {},
|
||||
onDismiss = {},
|
||||
isArchived = false,
|
||||
projectType = ProjectType.COSPEND,
|
||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||
isShareable = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ProjectOptionsDialogPreview2() {
|
||||
MaterialTheme {
|
||||
ProjectOptionsDialogContent(
|
||||
onEditProject = {},
|
||||
onRemoveProject = {},
|
||||
onManageMembers = {},
|
||||
onManageCurrencies = {},
|
||||
onStatistics = {},
|
||||
onSettle = {},
|
||||
onManageLabels = {},
|
||||
onShareProject = {},
|
||||
onExportProject = {},
|
||||
onDismiss = {},
|
||||
isArchived = true,
|
||||
projectType = ProjectType.COSPEND,
|
||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||
isShareable = true
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun ProjectOptionsDialogPreview3() {
|
||||
MaterialTheme {
|
||||
ProjectOptionsDialogContent(
|
||||
onEditProject = {},
|
||||
onRemoveProject = {},
|
||||
onManageMembers = {},
|
||||
onManageCurrencies = {},
|
||||
onStatistics = {},
|
||||
onSettle = {},
|
||||
onManageLabels = {},
|
||||
onShareProject = {},
|
||||
onExportProject = {},
|
||||
onDismiss = {},
|
||||
isArchived = false,
|
||||
projectType = ProjectType.LOCAL,
|
||||
accessLevel = DBProject.ACCESS_LEVEL_ADMIN,
|
||||
isShareable = true
|
||||
)
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
@@ -31,8 +32,6 @@ import androidx.compose.material.Text
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.ContentCopy
|
||||
import androidx.compose.material.icons.filled.Link
|
||||
import androidx.compose.material.icons.filled.QrCode
|
||||
import androidx.compose.material.icons.filled.Share
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
@@ -42,10 +41,8 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.LocalClipboardManager
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.colorResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
@@ -69,7 +66,7 @@ fun ProjectShareDialogContent(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val clipboardManager = LocalClipboardManager.current
|
||||
val qrCodeLinkWarn = stringResource(R.string.qrcode_link_open_attempt_warning)
|
||||
val qrCodeLinkWarn = stringResource(R.string.msg_share_qr_warn)
|
||||
|
||||
val shareUrl = remember { proj.getShareUrl() }
|
||||
val publicWebUrl = remember { proj.getPublicWebUrl() }
|
||||
@@ -93,35 +90,24 @@ fun ProjectShareDialogContent(
|
||||
.heightIn(max = 650.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
) {
|
||||
Icon(
|
||||
Icons.Default.Share,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.primary,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.share_dialog_title),
|
||||
style = MaterialTheme.typography.h6,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.title_share).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp)
|
||||
) {
|
||||
ShareCard(
|
||||
title = stringResource(R.string.share_project_public_url_title),
|
||||
title = stringResource(R.string.title_share_web),
|
||||
url = publicWebUrl,
|
||||
description = stringResource(R.string.share_project_public_url_dialog_message),
|
||||
icon = Icons.Default.Link,
|
||||
description = stringResource(R.string.msg_share_web),
|
||||
onUrlClick = {
|
||||
val i = Intent(Intent.ACTION_VIEW).apply {
|
||||
data = publicWebUrl.toUri()
|
||||
@@ -130,22 +116,21 @@ fun ProjectShareDialogContent(
|
||||
},
|
||||
onCopyClick = {
|
||||
clipboardManager.setText(AnnotatedString(publicWebUrl))
|
||||
Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, R.string.msg_link_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
|
||||
ShareCard(
|
||||
title = stringResource(R.string.share_project_public_qrcode_title),
|
||||
title = stringResource(R.string.title_share_qr),
|
||||
url = shareUrl,
|
||||
description = stringResource(R.string.share_project_dialog_message),
|
||||
icon = Icons.Default.QrCode,
|
||||
description = stringResource(R.string.msg_share_qr),
|
||||
qrBitmap = qrBitmap,
|
||||
onUrlClick = {
|
||||
Toast.makeText(context, qrCodeLinkWarn, Toast.LENGTH_SHORT).show()
|
||||
},
|
||||
onCopyClick = {
|
||||
clipboardManager.setText(AnnotatedString(shareUrl))
|
||||
Toast.makeText(context, "Link copied to clipboard", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(context, R.string.msg_link_copied, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -165,43 +150,31 @@ private fun ShareCard(
|
||||
title: String,
|
||||
url: String,
|
||||
description: String,
|
||||
icon: ImageVector,
|
||||
onUrlClick: () -> Unit,
|
||||
onCopyClick: () -> Unit,
|
||||
qrBitmap: Bitmap? = null
|
||||
) {
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = colorResource(R.color.fg_default_low).copy(alpha = 0.04f),
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colors.primary.copy(alpha = 0.7f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.subtitle2,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.8f)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
if (qrBitmap != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (qrBitmap != null) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(bottom = 12.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.size(128.dp)
|
||||
modifier = Modifier
|
||||
.size(128.dp)
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(Color.White),
|
||||
contentAlignment = Alignment.Center
|
||||
@@ -209,55 +182,53 @@ private fun ShareCard(
|
||||
Image(
|
||||
bitmap = qrBitmap.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colors.surface,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { onUrlClick() }
|
||||
.padding(start = 12.dp, end = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.clickable { onUrlClick() }
|
||||
.padding(start = 12.dp, end = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = url,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colors.primary,
|
||||
style = MaterialTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Medium
|
||||
Text(
|
||||
text = url,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colors.primary,
|
||||
style = MaterialTheme.typography.body2,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontWeight = FontWeight.Medium
|
||||
)
|
||||
IconButton(onClick = onCopyClick) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.4f)
|
||||
)
|
||||
IconButton(onClick = onCopyClick) {
|
||||
Icon(
|
||||
Icons.Default.ContentCopy,
|
||||
contentDescription = "Copy",
|
||||
modifier = Modifier.size(18.dp),
|
||||
tint = colorResource(R.color.fg_default_low)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.caption,
|
||||
textAlign = TextAlign.Center,
|
||||
lineHeight = 16.sp,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
|
||||
Text(
|
||||
text = description,
|
||||
style = MaterialTheme.typography.caption,
|
||||
textAlign = TextAlign.Start,
|
||||
lineHeight = 16.sp,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -291,7 +262,7 @@ private fun DialogActions(
|
||||
Icon(Icons.Default.Share, contentDescription = null, modifier = Modifier.size(16.dp))
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.simple_share_share).uppercase(),
|
||||
text = stringResource(R.string.action_share).uppercase(),
|
||||
style = MaterialTheme.typography.button,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
|
||||
@@ -213,7 +213,7 @@ class NewProjectActivity : AppCompatActivity() {
|
||||
|
||||
val rid = viewModel.projectId
|
||||
if (!isCospendScheme && (rid == "" || rid.contains(",") || rid.contains("/"))) {
|
||||
showToast(getString(R.string.error_invalid_project_remote_id), Toast.LENGTH_LONG)
|
||||
showToast(getString(R.string.error_invalid_project_id), Toast.LENGTH_LONG)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
@@ -42,7 +43,7 @@ fun NewProjectScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.action_add_project)) },
|
||||
title = { Text(stringResource(R.string.title_add_project)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
@@ -55,7 +56,7 @@ fun NewProjectScreen(
|
||||
floatingActionButton = {
|
||||
if (viewModel.isFormValid()) {
|
||||
FloatingActionButton(onClick = onOkPressed) {
|
||||
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save_bill))
|
||||
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,17 +64,18 @@ fun NewProjectScreen(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.padding(16.dp)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.fillMaxSize()
|
||||
) {
|
||||
// What to do
|
||||
SectionRow(
|
||||
label = stringResource(R.string.new_project_what_todo)
|
||||
label = stringResource(R.string.new_project_action)
|
||||
) {
|
||||
Row {
|
||||
ToggleButton(
|
||||
text = stringResource(R.string.todo_join_label),
|
||||
text = stringResource(R.string.todo_join),
|
||||
selected = !viewModel.whatTodoIsCreate,
|
||||
onClick = {
|
||||
viewModel.whatTodoIsCreate = false
|
||||
@@ -83,7 +85,7 @@ fun NewProjectScreen(
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ToggleButton(
|
||||
text = stringResource(R.string.todo_create_label),
|
||||
text = stringResource(R.string.todo_create),
|
||||
selected = viewModel.whatTodoIsCreate,
|
||||
onClick = { viewModel.whatTodoIsCreate = true }
|
||||
)
|
||||
@@ -98,20 +100,20 @@ fun NewProjectScreen(
|
||||
Row {
|
||||
if (viewModel.whatTodoIsCreate) {
|
||||
ToggleButton(
|
||||
text = stringResource(R.string.where_local_short),
|
||||
text = stringResource(R.string.where_local),
|
||||
selected = viewModel.projectType == ProjectType.LOCAL,
|
||||
onClick = { viewModel.projectType = ProjectType.LOCAL }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
}
|
||||
ToggleButton(
|
||||
text = stringResource(R.string.where_cospend_short),
|
||||
text = stringResource(R.string.where_cospend),
|
||||
selected = viewModel.projectType == ProjectType.COSPEND,
|
||||
onClick = { viewModel.projectType = ProjectType.COSPEND }
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
ToggleButton(
|
||||
text = stringResource(R.string.where_ihatemoney_short),
|
||||
text = stringResource(R.string.where_ihatemoney),
|
||||
selected = viewModel.projectType == ProjectType.IHATEMONEY,
|
||||
onClick = { viewModel.projectType = ProjectType.IHATEMONEY }
|
||||
)
|
||||
@@ -120,13 +122,7 @@ fun NewProjectScreen(
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
if (viewModel.whatTodoIsCreate) {
|
||||
if (viewModel.projectType == ProjectType.COSPEND) {
|
||||
Button(
|
||||
onClick = onChooseFromNextcloud
|
||||
) {
|
||||
Text(stringResource(R.string.new_project_from_nextcloud_tooltip))
|
||||
}
|
||||
} else if (viewModel.projectType == ProjectType.LOCAL) {
|
||||
if (viewModel.projectType == ProjectType.LOCAL) {
|
||||
Button(
|
||||
onClick = onImportFile
|
||||
) {
|
||||
@@ -136,7 +132,7 @@ fun NewProjectScreen(
|
||||
} else {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Button(onClick = onScanQrCode) {
|
||||
Text(text = stringResource(R.string.scan_qrcode))
|
||||
Text(text = stringResource(R.string.action_scan_qrcode))
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Icon(
|
||||
Icons.Default.QrCode2,
|
||||
@@ -153,13 +149,8 @@ fun NewProjectScreen(
|
||||
OutlinedTextField(
|
||||
value = viewModel.projectUrl,
|
||||
onValueChange = { viewModel.projectUrl = it },
|
||||
label = {
|
||||
Text(
|
||||
stringResource(
|
||||
if (viewModel.projectType == ProjectType.COSPEND) R.string.setting_cospend_project_url
|
||||
else R.string.setting_ihatemoney_project_url
|
||||
)
|
||||
)
|
||||
placeholder = {
|
||||
Text(stringResource(R.string.label_url))
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Link, contentDescription = null) }
|
||||
@@ -171,7 +162,7 @@ fun NewProjectScreen(
|
||||
OutlinedTextField(
|
||||
value = viewModel.projectId,
|
||||
onValueChange = { viewModel.projectId = it },
|
||||
label = { Text(stringResource(R.string.setting_project_id)) },
|
||||
placeholder = { Text(stringResource(R.string.label_project_id)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.AutoMirrored.Filled.LibraryBooks, contentDescription = null) }
|
||||
)
|
||||
@@ -182,7 +173,7 @@ fun NewProjectScreen(
|
||||
OutlinedTextField(
|
||||
value = viewModel.projectPassword,
|
||||
onValueChange = { viewModel.projectPassword = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_password)) },
|
||||
placeholder = { Text(stringResource(R.string.label_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
|
||||
)
|
||||
@@ -193,7 +184,7 @@ fun NewProjectScreen(
|
||||
OutlinedTextField(
|
||||
value = viewModel.projectName,
|
||||
onValueChange = { viewModel.projectName = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_name)) },
|
||||
placeholder = { Text(stringResource(R.string.label_project_title)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
|
||||
)
|
||||
@@ -203,7 +194,7 @@ fun NewProjectScreen(
|
||||
OutlinedTextField(
|
||||
value = viewModel.projectEmail,
|
||||
onValueChange = { viewModel.projectEmail = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_email)) },
|
||||
placeholder = { Text(stringResource(R.string.label_email)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
|
||||
)
|
||||
@@ -215,8 +206,8 @@ fun NewProjectScreen(
|
||||
if (viewModel.showAuthWarningDialog) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.showAuthWarningDialog = false },
|
||||
title = { Text(stringResource(R.string.auth_project_creation_title)) },
|
||||
text = { Text(stringResource(R.string.warning_auth_project_creation)) },
|
||||
title = { Text(stringResource(R.string.app_name)) },
|
||||
text = { Text(stringResource(R.string.msg_auth_warning)) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.showAuthWarningDialog = false
|
||||
@@ -270,12 +261,12 @@ fun NewProjectScreen(
|
||||
if (viewModel.isCreatingRemoteProject) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { },
|
||||
title = { Text(stringResource(R.string.simple_loading)) },
|
||||
title = { Text(stringResource(R.string.error_loading)) },
|
||||
text = {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
CircularProgressIndicator()
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(stringResource(R.string.creating_remote_project))
|
||||
Text(stringResource(R.string.action_add_project))
|
||||
}
|
||||
},
|
||||
confirmButton = {}
|
||||
@@ -285,7 +276,7 @@ fun NewProjectScreen(
|
||||
if (viewModel.errorDialogMessage != null) {
|
||||
AlertDialog(
|
||||
onDismissRequest = { viewModel.errorDialogMessage = null },
|
||||
title = { Text(stringResource(R.string.simple_error)) },
|
||||
title = { Text(stringResource(R.string.error_generic)) },
|
||||
text = { Text(viewModel.errorDialogMessage!!) },
|
||||
confirmButton = {
|
||||
TextButton(onClick = { viewModel.errorDialogMessage = null }) {
|
||||
@@ -301,11 +292,15 @@ fun SectionRow(
|
||||
label: String,
|
||||
content: @Composable () -> Unit
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.Top) {
|
||||
Column {
|
||||
Text(label, fontSize = 12.sp, color = MaterialTheme.colors.onSurface)
|
||||
content()
|
||||
}
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp)) {
|
||||
Text(
|
||||
text = label.uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ class EditProjectActivity : AppCompatActivity() {
|
||||
|
||||
private fun onDeleteRemote() {
|
||||
viewModel.showDialog(
|
||||
message = getString(R.string.confirm_delete_project_dialog_title),
|
||||
message = getString(R.string.title_confirm),
|
||||
positiveText = getString(R.string.simple_yes),
|
||||
onConfirm = {
|
||||
if (!db.cowspentServerSyncHelper.deleteRemoteProject(project.id, deleteCallBack)) {
|
||||
|
||||
@@ -2,6 +2,8 @@ package net.helcel.cowspent.android.project.edit
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
@@ -14,6 +16,7 @@ import androidx.compose.material.icons.filled.Title
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.helcel.cowspent.R
|
||||
@@ -59,70 +62,91 @@ fun EditProjectScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.simple_edit_project)) },
|
||||
title = { Text(stringResource(R.string.title_edit_project)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(onClick = onDeleteRemote) {
|
||||
Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.menu_delete_project_remote))
|
||||
if (!viewModel.isLocal) {
|
||||
IconButton(onClick = onDeleteRemote) {
|
||||
Icon(Icons.Default.DeleteForever, contentDescription = stringResource(R.string.action_delete))
|
||||
}
|
||||
}
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.surface,
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
elevation = 0.dp
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onSave) {
|
||||
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.menu_save_project))
|
||||
Icon(Icons.Default.Done, contentDescription = stringResource(R.string.action_save))
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
val scrollState = rememberScrollState()
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(padding)
|
||||
.imePadding()
|
||||
.padding(16.dp)
|
||||
.fillMaxSize()
|
||||
.verticalScroll(scrollState)
|
||||
) {
|
||||
Text(
|
||||
text = "GENERAL",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.name,
|
||||
onValueChange = { viewModel.name = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_name)) },
|
||||
placeholder = { Text(stringResource(R.string.label_project_title)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Title, contentDescription = null) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
if (!viewModel.isLocal) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.password,
|
||||
onValueChange = { viewModel.password = it },
|
||||
label = { Text(stringResource(R.string.setting_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.email,
|
||||
onValueChange = { viewModel.email = it },
|
||||
placeholder = { Text(stringResource(R.string.label_email)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = "SECURITY",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
fontWeight = FontWeight.Bold,
|
||||
modifier = Modifier.padding(bottom = 8.dp, top = 24.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.newPassword,
|
||||
onValueChange = { viewModel.newPassword = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.LockOpen, contentDescription = null) }
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.password,
|
||||
onValueChange = { viewModel.password = it },
|
||||
placeholder = { Text("Old " + stringResource(R.string.label_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Lock, contentDescription = null) }
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
OutlinedTextField(
|
||||
value = viewModel.email,
|
||||
onValueChange = { viewModel.email = it },
|
||||
label = { Text(stringResource(R.string.setting_new_project_email)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.Email, contentDescription = null) }
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = viewModel.newPassword,
|
||||
onValueChange = { viewModel.newPassword = it },
|
||||
placeholder = { Text("New " + stringResource(R.string.label_password)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = { Icon(Icons.Default.LockOpen, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ class EditProjectViewModel : ViewModel() {
|
||||
var password by mutableStateOf("")
|
||||
var newPassword by mutableStateOf("")
|
||||
var email by mutableStateOf("")
|
||||
var isLocal by mutableStateOf(false)
|
||||
|
||||
var dialogState by mutableStateOf<DialogState?>(null)
|
||||
|
||||
@@ -53,5 +54,6 @@ class EditProjectViewModel : ViewModel() {
|
||||
password = project.password
|
||||
newPassword = project.password
|
||||
email = project.email?.let { if (it == "null") "" else it } ?: ""
|
||||
isLocal = project.isLocal
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
@@ -29,15 +30,17 @@ fun MemberAddDialogContent(
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.add_member_dialog_title),
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = stringResource(R.string.add_member_dialog_title).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(R.string.member_edit_name)) },
|
||||
placeholder = { Text(stringResource(R.string.label_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences),
|
||||
singleLine = true
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.KeyboardCapitalization
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -57,8 +58,10 @@ fun MemberEditDialogContent(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.edit_member_dialog_title),
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = stringResource(R.string.edit_member_dialog_title).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
@@ -66,7 +69,7 @@ fun MemberEditDialogContent(
|
||||
OutlinedTextField(
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = { Text(stringResource(R.string.member_edit_name)) },
|
||||
placeholder = { Text(stringResource(R.string.label_name)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Person, contentDescription = null)
|
||||
@@ -80,7 +83,7 @@ fun MemberEditDialogContent(
|
||||
OutlinedTextField(
|
||||
value = weight,
|
||||
onValueChange = { weight = it },
|
||||
label = { Text(stringResource(R.string.member_edit_weight)) },
|
||||
placeholder = { Text(stringResource(R.string.label_weight)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.LineWeight, contentDescription = null)
|
||||
@@ -98,9 +101,17 @@ fun MemberEditDialogContent(
|
||||
.clickable { isActivated = !isActivated }
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Block, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(stringResource(R.string.member_edit_toggle), modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Block,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_activated),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
Checkbox(checked = isActivated, onCheckedChange = { isActivated = it })
|
||||
}
|
||||
|
||||
@@ -113,9 +124,17 @@ fun MemberEditDialogContent(
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
) {
|
||||
Icon(Icons.Default.Palette, contentDescription = null)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(stringResource(R.string.member_edit_color), modifier = Modifier.weight(1f))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Palette,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.label_color),
|
||||
modifier = Modifier.weight(1f),
|
||||
style = MaterialTheme.typography.subtitle1
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package net.helcel.cowspent.android.project.member
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import net.helcel.cowspent.theme.ThemeUtils
|
||||
|
||||
class MemberManagementActivity : AppCompatActivity() {
|
||||
|
||||
private val viewModel: MemberManagementViewModel by viewModels()
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
enableEdgeToEdge()
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val projectId = intent.getLongExtra(EXTRA_PROJECT_ID, 0)
|
||||
if (projectId == 0L) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
val db = CowspentSQLiteOpenHelper.getInstance(this)
|
||||
viewModel.loadMembers(projectId)
|
||||
|
||||
setContent {
|
||||
ThemeUtils.CowspentTheme {
|
||||
var showAddMemberDialog by remember { mutableStateOf(false) }
|
||||
var editingMember by remember { mutableStateOf<DBMember?>(null) }
|
||||
|
||||
MemberManagementScreen(
|
||||
members = viewModel.members,
|
||||
onAddMember = { showAddMemberDialog = true },
|
||||
onEditMember = { editingMember = it },
|
||||
onToggleMember = { member, isActivated ->
|
||||
db.updateMemberAndSync(member, member.name, member.weight, isActivated, member.r, member.g, member.b, "", "")
|
||||
viewModel.loadMembers(projectId)
|
||||
},
|
||||
onBack = { finish() }
|
||||
)
|
||||
|
||||
if (showAddMemberDialog) {
|
||||
Dialog(
|
||||
onDismissRequest = { showAddMemberDialog = false },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
MemberAddDialogContent(
|
||||
onAdd = { memberName ->
|
||||
val memberNames = db.getMembersOfProject(projectId, null).map { it.name }
|
||||
if (memberNames.contains(memberName)) {
|
||||
Toast.makeText(this, R.string.member_already_exists, Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
val color = net.helcel.cowspent.android.helper.TextDrawable.getColorFromName(memberName)
|
||||
db.addMemberAndSync(
|
||||
DBMember(
|
||||
0, 0, projectId, memberName, true, 1.0,
|
||||
DBBill.STATE_ADDED,
|
||||
android.graphics.Color.red(color),
|
||||
android.graphics.Color.green(color),
|
||||
android.graphics.Color.blue(color),
|
||||
null, null
|
||||
)
|
||||
)
|
||||
viewModel.loadMembers(projectId)
|
||||
showAddMemberDialog = false
|
||||
}
|
||||
},
|
||||
onDismiss = { showAddMemberDialog = false }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (editingMember != null) {
|
||||
val member = editingMember!!
|
||||
Dialog(
|
||||
onDismissRequest = { editingMember = null },
|
||||
properties = DialogProperties(usePlatformDefaultWidth = false)
|
||||
) {
|
||||
MemberEditDialogContent(
|
||||
member = member,
|
||||
onSave = { name, weight, isActivated, r, g, b ->
|
||||
db.updateMemberAndSync(member, name, weight, isActivated, r, g, b, "", "")
|
||||
viewModel.loadMembers(projectId)
|
||||
editingMember = null
|
||||
},
|
||||
onDelete = {
|
||||
db.deleteMember(member.id)
|
||||
viewModel.loadMembers(projectId)
|
||||
editingMember = null
|
||||
},
|
||||
onDismiss = { editingMember = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val EXTRA_PROJECT_ID = "project_id"
|
||||
|
||||
fun createIntent(context: Context, projectId: Long): Intent {
|
||||
return Intent(context, MemberManagementActivity::class.java).apply {
|
||||
putExtra(EXTRA_PROJECT_ID, projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
-127
@@ -1,127 +0,0 @@
|
||||
package net.helcel.cowspent.android.project.member
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.IconButton
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.TextButton
|
||||
import androidx.compose.material.contentColorFor
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.MemberAvatar
|
||||
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
|
||||
@Composable
|
||||
fun MemberManagementDialogContent(
|
||||
members: List<DBMember>,
|
||||
onAddMember: () -> Unit,
|
||||
onEditMember: (DBMember) -> Unit,
|
||||
onDismiss: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.large,
|
||||
color = MaterialTheme.colors.surface,
|
||||
contentColor = contentColorFor(MaterialTheme.colors.surface)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.heightIn(max = 500.dp)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.fab_manage_members),
|
||||
style = MaterialTheme.typography.h6
|
||||
)
|
||||
IconButton(onClick = onAddMember) {
|
||||
Icon(Icons.Default.Add,modifier=Modifier.size(32.dp),
|
||||
contentDescription = stringResource(R.string.fab_add_member))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.weight(1f, fill = false)
|
||||
.lazyVerticalScrollbar(listState)
|
||||
) {
|
||||
items(members) { member ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onEditMember(member) }
|
||||
.padding(vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MemberAvatar(
|
||||
member = member,
|
||||
size = 40.dp
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
Text(
|
||||
text = member.name,
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
|
||||
Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.simple_close).uppercase())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MemberManagementDialogContentPreview() {
|
||||
MaterialTheme {
|
||||
MemberManagementDialogContent(
|
||||
members = listOf(
|
||||
DBMember(1, 0, 1, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
|
||||
DBMember(2, 0, 1, "Bob", true, 1.0, 0, 100, 255, 100, null, null),
|
||||
DBMember(3, 0, 1, "Charlie", false, 1.0, 0, 100, 100, 255, null, null)
|
||||
),
|
||||
onAddMember = {},
|
||||
onEditMember = {},
|
||||
onDismiss = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
package net.helcel.cowspent.android.project.member
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.helper.MemberAvatar
|
||||
import net.helcel.cowspent.android.helper.lazyVerticalScrollbar
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
|
||||
@Composable
|
||||
fun MemberManagementScreen(
|
||||
members: List<DBMember>,
|
||||
onAddMember: () -> Unit,
|
||||
onEditMember: (DBMember) -> Unit,
|
||||
onToggleMember: (DBMember, Boolean) -> Unit,
|
||||
onBack: () -> Unit
|
||||
) {
|
||||
val listState = rememberLazyListState()
|
||||
val activatedMembers = members.filter { it.isActivated }
|
||||
val deactivatedMembers = members.filter { !it.isActivated }
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.action_members)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
}
|
||||
},
|
||||
backgroundColor = MaterialTheme.colors.primary,
|
||||
elevation = 0.dp
|
||||
)
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(onClick = onAddMember) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.action_save))
|
||||
}
|
||||
}
|
||||
) { padding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(padding)
|
||||
) {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.lazyVerticalScrollbar(listState)
|
||||
) {
|
||||
item {
|
||||
Text(
|
||||
text = "ACTIVATED MEMBERS",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
items(activatedMembers) { member ->
|
||||
MemberRow(
|
||||
member = member,
|
||||
onEditMember = onEditMember,
|
||||
onToggleMember = { onToggleMember(member, it) }
|
||||
)
|
||||
}
|
||||
|
||||
if (deactivatedMembers.isNotEmpty()) {
|
||||
item {
|
||||
Text(
|
||||
text = "DEACTIVATED MEMBERS",
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
|
||||
modifier = Modifier.padding(start = 16.dp, top = 24.dp, end = 16.dp, bottom = 16.dp)
|
||||
)
|
||||
}
|
||||
|
||||
items(deactivatedMembers) { member ->
|
||||
MemberRow(
|
||||
member = member,
|
||||
onEditMember = onEditMember,
|
||||
onToggleMember = { onToggleMember(member, it) },
|
||||
alpha = 0.6f
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun MemberRow(
|
||||
member: DBMember,
|
||||
onEditMember: (DBMember) -> Unit,
|
||||
onToggleMember: (Boolean) -> Unit,
|
||||
alpha: Float = 1f
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onEditMember(member) }
|
||||
.padding(horizontal = 16.dp, vertical = 0.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
MemberAvatar(
|
||||
member = member,
|
||||
size = 24.dp,
|
||||
alpha = alpha
|
||||
)
|
||||
Spacer(modifier = Modifier.width(32.dp))
|
||||
Text(
|
||||
text = member.name,
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
modifier = Modifier.weight(1f),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = alpha)
|
||||
)
|
||||
IconButton(onClick = { onToggleMember(!member.isActivated) }) {
|
||||
Icon(
|
||||
imageVector = if (member.isActivated) Icons.Default.Delete else Icons.Default.Add,
|
||||
contentDescription = if (member.isActivated) "Deactivate" else "Activate",
|
||||
tint = if (member.isActivated) MaterialTheme.colors.error.copy(alpha = 0.6f) else MaterialTheme.colors.primary.copy(alpha = 0.6f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Composable
|
||||
fun MemberManagementScreenPreview() {
|
||||
MaterialTheme {
|
||||
MemberManagementScreen(
|
||||
members = listOf(
|
||||
DBMember(1, 0, 1, "Alice", true, 1.0, 0, 255, 100, 100, null, null),
|
||||
DBMember(2, 0, 1, "Bob", true, 1.0, 0, 100, 255, 100, null, null),
|
||||
DBMember(3, 0, 1, "Charlie", false, 1.0, 0, 100, 100, 255, null, null)
|
||||
),
|
||||
onAddMember = {},
|
||||
onEditMember = {},
|
||||
onToggleMember = { _, _ -> },
|
||||
onBack = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package net.helcel.cowspent.android.project.member
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableLongStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.helcel.cowspent.android.helper.DialogState
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
|
||||
class MemberManagementViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val db = CowspentSQLiteOpenHelper.getInstance(application)
|
||||
|
||||
var projectId by mutableLongStateOf(0L)
|
||||
var members by mutableStateOf<List<DBMember>>(emptyList())
|
||||
|
||||
var dialogState by mutableStateOf<DialogState?>(null)
|
||||
|
||||
fun loadMembers(projId: Long) {
|
||||
projectId = projId
|
||||
viewModelScope.launch {
|
||||
members = withContext(Dispatchers.IO) { db.getMembersOfProject(projId, null) }
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteMember(id: Long) {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
db.deleteMember(id)
|
||||
}
|
||||
loadMembers(projectId)
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-5
@@ -129,8 +129,10 @@ fun ProjectSettlementUI(
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settle_dialog_title),
|
||||
style = MaterialTheme.typography.h6,
|
||||
text = stringResource(R.string.title_settle).uppercase(),
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
)
|
||||
|
||||
@@ -174,7 +176,7 @@ private fun BalancedStateMessage() {
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.settle_dialog_balanced),
|
||||
text = stringResource(R.string.dialog_balanced_msg),
|
||||
style = MaterialTheme.typography.body1
|
||||
)
|
||||
}
|
||||
@@ -279,9 +281,9 @@ private fun TableHeaderText(
|
||||
Text(
|
||||
text = text.uppercase(),
|
||||
modifier = modifier,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontWeight = FontWeight.Bold,
|
||||
style = MaterialTheme.typography.overline,
|
||||
color = colorResource(R.color.fg_default_low),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f),
|
||||
textAlign = textAlign,
|
||||
letterSpacing = 0.8.sp
|
||||
)
|
||||
|
||||
@@ -135,10 +135,10 @@ fun SettingsScreen(
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
// Appearance
|
||||
SettingsCategory(stringResource(R.string.settings_appearance_category))
|
||||
SettingsCategory(stringResource(R.string.settings_appearance))
|
||||
|
||||
SettingsSwitchPreference(
|
||||
title = stringResource(R.string.setting_show_archived),
|
||||
title = stringResource(R.string.settings_show_archived),
|
||||
icon = Icons.Default.Archive,
|
||||
checked = showArchived,
|
||||
onCheckedChange = {
|
||||
@@ -160,7 +160,7 @@ fun SettingsScreen(
|
||||
)
|
||||
|
||||
SettingsListPreference(
|
||||
title = stringResource(R.string.settings_night_mode_title),
|
||||
title = stringResource(R.string.settings_night_mode),
|
||||
icon = Icons.Default.Brightness2,
|
||||
value = nightMode,
|
||||
entries = mapOf(
|
||||
@@ -178,7 +178,7 @@ fun SettingsScreen(
|
||||
)
|
||||
|
||||
SettingsListPreference(
|
||||
title = stringResource(R.string.settings_color_mode_title),
|
||||
title = stringResource(R.string.settings_color_mode),
|
||||
icon = Icons.Default.Palette,
|
||||
value = colorMode,
|
||||
entries = mapOf(
|
||||
@@ -203,8 +203,8 @@ fun SettingsScreen(
|
||||
|
||||
if (colorMode == "manual") {
|
||||
SettingsColorPreference(
|
||||
title = stringResource(R.string.settings_color_title),
|
||||
summary = stringResource(R.string.settings_color_summary),
|
||||
title = stringResource(R.string.settings_color_custom),
|
||||
summary = stringResource(R.string.settings_color_custom),
|
||||
icon = Icons.Default.Palette,
|
||||
initialColor = appColor,
|
||||
onColorSelected = {
|
||||
@@ -218,10 +218,10 @@ fun SettingsScreen(
|
||||
}
|
||||
|
||||
// Network
|
||||
SettingsCategory(stringResource(R.string.settings_network_category))
|
||||
SettingsCategory(stringResource(R.string.settings_network))
|
||||
|
||||
SettingsSwitchPreference(
|
||||
title = stringResource(R.string.settings_offline_mode_title),
|
||||
title = stringResource(R.string.settings_offline_mode),
|
||||
summary = stringResource(R.string.settings_offline_mode_summary),
|
||||
icon = Icons.Default.Sync,
|
||||
checked = offlineMode,
|
||||
@@ -234,16 +234,16 @@ fun SettingsScreen(
|
||||
)
|
||||
|
||||
SettingsPreference(
|
||||
title = stringResource(R.string.settings_server_settings),
|
||||
title = stringResource(R.string.title_account),
|
||||
icon = Icons.Default.AccountCircle,
|
||||
onClick = onAccountSettingsClick
|
||||
)
|
||||
|
||||
// Other
|
||||
SettingsCategory(stringResource(R.string.settings_other_category))
|
||||
SettingsCategory(stringResource(R.string.settings_other))
|
||||
|
||||
SettingsPreference(
|
||||
title = stringResource(R.string.settings_about),
|
||||
title = stringResource(R.string.title_about),
|
||||
icon = Icons.Default.Info,
|
||||
onClick = onAboutClick
|
||||
)
|
||||
@@ -255,9 +255,9 @@ fun SettingsScreen(
|
||||
fun SettingsCategory(title: String) {
|
||||
Text(
|
||||
text = title.uppercase(),
|
||||
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp),
|
||||
color = MaterialTheme.colors.primary,
|
||||
style = MaterialTheme.typography.overline,
|
||||
modifier = Modifier.padding(start = 16.dp, top = 24.dp, bottom = 8.dp),
|
||||
color = MaterialTheme.colors.onSurface,
|
||||
style = MaterialTheme.typography.subtitle1,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
|
||||
@@ -92,7 +92,7 @@ fun ProjectSankeyDiagram(
|
||||
customCategories: List<DBCategory>,
|
||||
onShareReady: (String) -> Unit
|
||||
) {
|
||||
val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName)
|
||||
val shareStatsIntro = stringResource(R.string.msg_stats_intro, projectName)
|
||||
var selectedMemberId by remember { mutableLongStateOf(-1L) }
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -105,7 +105,7 @@ fun ProjectSankeyDiagram(
|
||||
|
||||
if (activeBills.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("No data to display", style = MaterialTheme.typography.h6)
|
||||
Text("No data to display", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -43,10 +43,10 @@ fun ProjectSpendingGraph(
|
||||
allBills: List<DBBill>,
|
||||
onShareReady: (String) -> Unit
|
||||
) {
|
||||
val shareStatsIntro = stringResource(R.string.share_stats_intro, projectName)
|
||||
val shareStatsIntro = stringResource(R.string.msg_stats_intro, projectName)
|
||||
if (allBills.isEmpty()) {
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("No data to display", style = MaterialTheme.typography.h6)
|
||||
Text("No data to display", style = MaterialTheme.typography.subtitle1, fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ fun ProjectStatisticsScreen(
|
||||
}
|
||||
var currentShareText by remember { mutableStateOf("") }
|
||||
val tabs = listOf(
|
||||
stringResource(R.string.statistic_title),
|
||||
stringResource(R.string.title_stats),
|
||||
"Trend",
|
||||
"Sankey"
|
||||
)
|
||||
@@ -43,7 +43,7 @@ fun ProjectStatisticsScreen(
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text(stringResource(R.string.statistic_title)) },
|
||||
title = { Text(stringResource(R.string.title_stats)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onBack) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null)
|
||||
|
||||
+12
-14
@@ -57,8 +57,8 @@ fun ProjectStatisticsTable(
|
||||
val paymentModeAll = stringResource(R.string.payment_mode_all)
|
||||
val paymentModeNone = stringResource(R.string.payment_mode_none)
|
||||
|
||||
val shareStatsHeader = stringResource(R.string.share_stats_header)
|
||||
val shareStatsIntro = stringResource(R.string.share_stats_intro, proj.name.ifEmpty { proj.remoteId })
|
||||
val shareStatsHeader = stringResource(R.string.msg_stats_header)
|
||||
val shareStatsIntro = stringResource(R.string.msg_stats_intro, proj.name.ifEmpty { proj.remoteId })
|
||||
|
||||
val categories = remember(proj.id, customCategories, categoryAll, categoryNone, categoryReimbursement, categoryAllExceptReimbursement) {
|
||||
val list = mutableListOf<Triple<Int, String, String>>()
|
||||
@@ -164,12 +164,12 @@ fun ProjectStatisticsTable(
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedCategory?.third ?: "",
|
||||
placeholder = stringResource(R.string.setting_category),
|
||||
placeholder = stringResource(R.string.label_category),
|
||||
expanded = categoryExpanded,
|
||||
onExpandedChange = { categoryExpanded = it },
|
||||
onDismissRequest = { categoryExpanded = false },
|
||||
leadingIcon = {
|
||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (selectedCategory != null) {
|
||||
Text(text = selectedCategory.second, fontSize = 20.sp)
|
||||
} else {
|
||||
@@ -198,12 +198,12 @@ fun ProjectStatisticsTable(
|
||||
|
||||
EditableExposedDropdownMenu(
|
||||
value = selectedPm?.third ?: "",
|
||||
placeholder = stringResource(R.string.setting_payment_mode),
|
||||
placeholder = stringResource(R.string.label_mode),
|
||||
expanded = pmExpanded,
|
||||
onExpandedChange = { pmExpanded = it },
|
||||
onDismissRequest = { pmExpanded = false },
|
||||
leadingIcon = {
|
||||
Box(modifier = Modifier.padding(start = 12.dp)) {
|
||||
Box(modifier = Modifier) {
|
||||
if (selectedPm != null) {
|
||||
Text(text = selectedPm.second, fontSize = 20.sp)
|
||||
} else {
|
||||
@@ -269,17 +269,15 @@ fun ProjectStatisticsTable(
|
||||
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
|
||||
// Table Header
|
||||
Surface(
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.05f),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
color = MaterialTheme.colors.background,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp)) {
|
||||
Text(stringResource(R.string.stats_who), modifier = Modifier.weight(2f), fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
Text(stringResource(R.string.stats_paid), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
Text(stringResource(R.string.stats_spent), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
Text(stringResource(R.string.stats_balance), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f), fontSize = 12.sp)
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 12.dp)) {
|
||||
Text(stringResource(R.string.stats_who).uppercase(), modifier = Modifier.weight(2f), fontWeight = FontWeight.Bold, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
|
||||
Text(stringResource(R.string.stats_paid).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
|
||||
Text(stringResource(R.string.stats_spent).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
|
||||
Text(stringResource(R.string.stats_balance).uppercase(), modifier = Modifier.weight(1.5f), fontWeight = FontWeight.Bold, textAlign = TextAlign.End, color = MaterialTheme.colors.onSurface, style = MaterialTheme.typography.overline)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,6 @@ class DBAccountProject(
|
||||
) : Serializable {
|
||||
|
||||
override fun toString(): String {
|
||||
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, $password, archivedTs=$archivedTs"
|
||||
return "#DBAccountProject$id/$remoteId,$name, $ncUrl, [SECURE], archivedTs=$archivedTs"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.preference.PreferenceManager
|
||||
import net.helcel.cowspent.R
|
||||
import net.helcel.cowspent.android.main.BillsListViewActivity
|
||||
import net.helcel.cowspent.model.*
|
||||
import net.helcel.cowspent.util.SecureStorage
|
||||
import net.helcel.cowspent.util.SupportUtil
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
@@ -316,11 +317,12 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val db = writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(key_remoteId, accountProject.remoteId)
|
||||
values.put(key_password, accountProject.password)
|
||||
values.put(key_ncUrl, accountProject.ncUrl)
|
||||
values.put(key_name, accountProject.name)
|
||||
values.put(key_archived, accountProject.archivedTs ?: 0L)
|
||||
return db.insert(table_account_projects, null, values)
|
||||
val id = db.insert(table_account_projects, null, values)
|
||||
SecureStorage.savePasswordSync(context, "AccountProjectPassword_$id", accountProject.password)
|
||||
return id
|
||||
}
|
||||
|
||||
val accountProjects: List<DBAccountProject>
|
||||
@@ -344,11 +346,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
|
||||
@SuppressLint("Range")
|
||||
private fun getAccountProjectFromCursor(cursor: Cursor): DBAccountProject {
|
||||
val id = cursor.getLong(cursor.getColumnIndex(key_id))
|
||||
val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived))
|
||||
val password = SecureStorage.getPasswordSync(context, "AccountProjectPassword_$id")
|
||||
return DBAccountProject(
|
||||
cursor.getLong(cursor.getColumnIndex(key_id)),
|
||||
id,
|
||||
cursor.getString(cursor.getColumnIndex(key_remoteId)),
|
||||
cursor.getString(cursor.getColumnIndex(key_password)),
|
||||
password,
|
||||
cursor.getString(cursor.getColumnIndex(key_name)),
|
||||
cursor.getString(cursor.getColumnIndex(key_ncUrl)),
|
||||
if (archivedTs > 0) archivedTs else null
|
||||
@@ -608,14 +612,15 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val db = writableDatabase
|
||||
val values = ContentValues()
|
||||
values.put(key_remoteId, project.remoteId)
|
||||
values.put(key_password, project.password)
|
||||
values.put(key_bearer_token, project.bearerToken)
|
||||
values.put(key_email, project.email)
|
||||
values.put(key_name, project.name)
|
||||
values.put(key_ihmUrl, project.serverUrl)
|
||||
values.put(key_type, project.type.id)
|
||||
values.put(key_archived, project.archivedTs ?: 0L)
|
||||
return db.insert(table_projects, null, values)
|
||||
val id = db.insert(table_projects, null, values)
|
||||
SecureStorage.savePasswordSync(context, "ProjectPassword_$id", project.password)
|
||||
return id
|
||||
}
|
||||
|
||||
fun getProject(id: Long): DBProject? {
|
||||
@@ -644,11 +649,13 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
|
||||
@SuppressLint("Range")
|
||||
private fun getProjectFromCursor(cursor: Cursor): DBProject {
|
||||
val id = cursor.getLong(cursor.getColumnIndex(key_id))
|
||||
val archivedTs = cursor.getLong(cursor.getColumnIndex(key_archived))
|
||||
val password = SecureStorage.getPasswordSync(context, "ProjectPassword_$id") ?: ""
|
||||
return DBProject(
|
||||
cursor.getLong(cursor.getColumnIndex(key_id)),
|
||||
id,
|
||||
cursor.getString(cursor.getColumnIndex(key_remoteId)),
|
||||
cursor.getString(cursor.getColumnIndex(key_password)),
|
||||
password,
|
||||
cursor.getString(cursor.getColumnIndex(key_name)),
|
||||
cursor.getString(cursor.getColumnIndex(key_ihmUrl)),
|
||||
cursor.getString(cursor.getColumnIndex(key_email)),
|
||||
@@ -671,6 +678,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
}
|
||||
db.delete(table_members, "$key_projectid = ?", arrayOf(id.toString()))
|
||||
db.delete(table_projects, "$key_id = ?", arrayOf(id.toString()))
|
||||
SecureStorage.removePasswordSync(context, "ProjectPassword_$id")
|
||||
}
|
||||
|
||||
fun updateProject(
|
||||
@@ -685,7 +693,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val values = ContentValues()
|
||||
if (newName != null) values.put(key_name, newName)
|
||||
if (newEmail != null) values.put(key_email, newEmail)
|
||||
if (newPassword != null) values.put(key_password, newPassword)
|
||||
if (newPassword != null) SecureStorage.savePasswordSync(context, "ProjectPassword_$projId", newPassword)
|
||||
if (newBearerToken != null) values.put(key_bearer_token, newBearerToken)
|
||||
if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId)
|
||||
if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp)
|
||||
@@ -725,7 +733,7 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
||||
val values = ContentValues()
|
||||
if (newName != null) values.put(key_name, newName)
|
||||
if (newEmail != null) values.put(key_email, newEmail)
|
||||
if (newPassword != null) values.put(key_password, newPassword)
|
||||
if (newPassword != null) SecureStorage.savePasswordSync(context, "ProjectPassword_$projId", newPassword)
|
||||
if (newBearerToken != null) values.put(key_bearer_token, newBearerToken)
|
||||
if (newLastPayerId != null) values.put(key_lastPayerId, newLastPayerId)
|
||||
if (newLastSyncedTimestamp != null) values.put(key_lastSyncTimestamp, newLastSyncedTimestamp)
|
||||
|
||||
@@ -111,6 +111,7 @@ object ThemeUtils {
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.navigationBarsPadding()
|
||||
.imePadding()
|
||||
) {
|
||||
content()
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ object CospendClientUtil {
|
||||
|
||||
enum class LoginStatus(@param:StringRes val str: Int) {
|
||||
OK(0),
|
||||
AUTH_FAILED(R.string.error_username_password_invalid),
|
||||
AUTH_FAILED(R.string.error_auth),
|
||||
CONNECTION_FAILED(R.string.error_io),
|
||||
NO_NETWORK(R.string.error_no_network),
|
||||
JSON_FAILED(R.string.error_json),
|
||||
|
||||
@@ -35,13 +35,13 @@ class NextcloudClient(
|
||||
val method = if (useOcsApi) METHOD_GET else METHOD_POST
|
||||
return if (nextcloudAPI != null) {
|
||||
Log.d(javaClass.simpleName, "using SSO to get/sync account projects")
|
||||
Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
// Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
ServerResponse.AccountProjectsResponse(
|
||||
requestServerWithSSO(nextcloudAPI, target, method, null, useOcsApi),
|
||||
useOcsApi
|
||||
)
|
||||
} else {
|
||||
Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
// Log.d(javaClass.simpleName, "Sync projects target $target")
|
||||
ServerResponse.AccountProjectsResponse(
|
||||
requestServer(target, method, null, "", true, useOcsApi),
|
||||
useOcsApi
|
||||
@@ -184,7 +184,7 @@ class NextcloudClient(
|
||||
): VersatileProjectSyncClient.ResponseData {
|
||||
val result = StringBuilder()
|
||||
val targetURL = url + target.replace("^/".toRegex(), "")
|
||||
Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
|
||||
// Log.d(javaClass.simpleName, "method and target URL: $method $targetURL")
|
||||
val httpCon = SupportUtil.getHttpURLConnection(targetURL)
|
||||
httpCon.requestMethod = method
|
||||
if (needLogin) {
|
||||
@@ -206,7 +206,7 @@ class NextcloudClient(
|
||||
var paramData: ByteArray? = null
|
||||
if (params != null) {
|
||||
paramData = params.toString().toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $params")
|
||||
// Log.d(javaClass.simpleName, "Params: $params")
|
||||
httpCon.setFixedLengthStreamingMode(paramData.size)
|
||||
httpCon.setRequestProperty("Content-Type", application_json)
|
||||
httpCon.doOutput = true
|
||||
@@ -263,11 +263,11 @@ class NextcloudClient(
|
||||
httpCon.setRequestProperty("OCS-APIRequest", "true")
|
||||
}
|
||||
httpCon.connectTimeout = 10 * 1000 // 10 seconds
|
||||
Log.d(javaClass.simpleName, "$method $targetURL")
|
||||
// Log.d(javaClass.simpleName, "$method $targetURL")
|
||||
var paramData: ByteArray?
|
||||
if (params != null) {
|
||||
paramData = params.toString().toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $params")
|
||||
// Log.d(javaClass.simpleName, "Params: $params")
|
||||
httpCon.setFixedLengthStreamingMode(paramData.size)
|
||||
httpCon.setRequestProperty("Content-Type", application_json)
|
||||
httpCon.doOutput = true
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package net.helcel.cowspent.util
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Base64
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.core.edit
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.google.crypto.tink.Aead
|
||||
import com.google.crypto.tink.KeyTemplates
|
||||
import com.google.crypto.tink.RegistryConfiguration
|
||||
import com.google.crypto.tink.aead.AeadConfig
|
||||
import com.google.crypto.tink.integration.android.AndroidKeysetManager
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "secure_prefs")
|
||||
|
||||
object SecureStorage {
|
||||
private var aead: Aead? = null
|
||||
|
||||
@Synchronized
|
||||
private fun getAead(context: Context): Aead {
|
||||
aead?.let { return it }
|
||||
AeadConfig.register()
|
||||
val masterKeyUri = "android-keystore://secure_storage_master_key"
|
||||
val keysetManager = AndroidKeysetManager.Builder()
|
||||
.withSharedPref(context, "tink_keyset", "secure_storage_keyset_prefs")
|
||||
.withKeyTemplate(KeyTemplates.get("AES256_GCM"))
|
||||
.withMasterKeyUri(masterKeyUri)
|
||||
.build()
|
||||
val newAead = keysetManager.keysetHandle.getPrimitive(RegistryConfiguration.get(), Aead::class.java)
|
||||
aead = newAead
|
||||
return newAead
|
||||
}
|
||||
|
||||
private fun encrypt(context: Context, value: String): String {
|
||||
val encrypted = getAead(context).encrypt(value.toByteArray(), null)
|
||||
return Base64.encodeToString(encrypted, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
private fun decrypt(context: Context, encryptedValue: String): String? {
|
||||
return try {
|
||||
val decoded = Base64.decode(encryptedValue, Base64.NO_WRAP)
|
||||
val decrypted = getAead(context).decrypt(decoded, null)
|
||||
String(decrypted)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun savePassword(context: Context, key: String, password: String?) {
|
||||
if (password == null) {
|
||||
removePassword(context, key)
|
||||
} else {
|
||||
val encrypted = encrypt(context, password)
|
||||
context.dataStore.edit { it[stringPreferencesKey(key)] = encrypted }
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getPassword(context: Context, key: String): String? {
|
||||
val encrypted = context.dataStore.data.map { it[stringPreferencesKey(key)] }.first()
|
||||
return encrypted?.let { decrypt(context, it) }
|
||||
}
|
||||
|
||||
suspend fun removePassword(context: Context, key: String) {
|
||||
context.dataStore.edit { it.remove(stringPreferencesKey(key)) }
|
||||
}
|
||||
|
||||
// Synchronous alternatives for legacy code
|
||||
fun savePasswordSync(context: Context, key: String, password: String?) = runBlocking {
|
||||
savePassword(context, key, password)
|
||||
}
|
||||
|
||||
fun getPasswordSync(context: Context, key: String): String? = runBlocking {
|
||||
getPassword(context, key)
|
||||
}
|
||||
|
||||
fun removePasswordSync(context: Context, key: String) = runBlocking {
|
||||
removePassword(context, key)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package net.helcel.cowspent.util
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import net.helcel.cowspent.model.*
|
||||
import net.helcel.cowspent.model.CreditDebt
|
||||
import net.helcel.cowspent.model.DBBill
|
||||
import net.helcel.cowspent.model.DBMember
|
||||
import net.helcel.cowspent.model.Transaction
|
||||
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
@@ -81,13 +81,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getProjectInfo")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
Log.i(TAG, "using new API for getProjectInfo")
|
||||
ServerResponse.ProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, null, null, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
|
||||
@@ -99,7 +95,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for getProjectInfo")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -172,13 +167,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteProject")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
Log.i(TAG, "using new API for editRemoteProject")
|
||||
ServerResponse.EditRemoteProjectResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId
|
||||
@@ -190,7 +181,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -239,13 +229,9 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteMember")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
Log.i(TAG, "using new API for editRemoteMember")
|
||||
ServerResponse.EditRemoteMemberResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_PUT, paramKeys, paramValues, true), true)
|
||||
} else {
|
||||
target = "/index.php/apps/cospend/api-priv/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
@@ -257,7 +243,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members/" + member.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteMember")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members/" + member.remoteId
|
||||
@@ -333,9 +318,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
@@ -351,7 +333,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + bill.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for editRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + bill.remoteId
|
||||
@@ -389,9 +370,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteProject")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId
|
||||
@@ -407,7 +385,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password)
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId
|
||||
@@ -439,9 +416,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
@@ -457,7 +431,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills/" + billRemoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteProject")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills/" + billRemoteId
|
||||
@@ -528,9 +501,7 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createAuthenticatedRemoteProject")
|
||||
}
|
||||
|
||||
return ServerResponse.CreateRemoteProjectResponse(
|
||||
requestServer(
|
||||
target, METHOD_POST, paramKeys, paramValues,
|
||||
@@ -594,9 +565,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteBill")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
|
||||
@@ -612,7 +580,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/bills"
|
||||
@@ -664,9 +631,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteMember")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
|
||||
@@ -682,7 +646,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteBill")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
|
||||
@@ -717,9 +680,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getBills")
|
||||
}
|
||||
return ServerResponse.BillsResponse(
|
||||
requestServer(
|
||||
target, METHOD_GET, null, null,
|
||||
@@ -750,7 +710,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync
|
||||
Log.i(TAG, "using public API, target is: ${target}for getBills")
|
||||
return ServerResponse.BillsResponse(
|
||||
requestServer(
|
||||
target, METHOD_GET, null, null,
|
||||
@@ -790,9 +749,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/members"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for getMembers, projectId: " + project.remoteId)
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/members"
|
||||
@@ -808,7 +764,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/members"
|
||||
Log.i(TAG, "using public API, target is: ${target}for getMembers")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/members"
|
||||
@@ -847,9 +802,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency"
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for createRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency"
|
||||
@@ -865,7 +817,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency"
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency"
|
||||
@@ -904,9 +855,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for editRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
@@ -922,7 +870,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currency.remoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for createRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currency.remoteId
|
||||
@@ -954,9 +901,6 @@ class VersatileProjectSyncClient(
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
useOcsApiRequest = cospendVersionGT161
|
||||
if (cospendVersionGT161) {
|
||||
Log.i(TAG, "using new API (weblogin, $username:$password) for deleteRemoteCurrency")
|
||||
}
|
||||
} else if (canAccessProjectWithSSO(project)) {
|
||||
return if (cospendVersionGT161) {
|
||||
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
@@ -972,7 +916,6 @@ class VersatileProjectSyncClient(
|
||||
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
|
||||
else
|
||||
project.getRequestBaseUrl(false) + "/api/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/currency/" + currencyRemoteId
|
||||
Log.i(TAG, "using public API, target is: ${target}for deleteRemoteCurrency")
|
||||
}
|
||||
} else {
|
||||
target = project.serverUrl!!.replace("/+$".toRegex(), "") + "/api/projects/" + project.remoteId + "/currency/" + currencyRemoteId
|
||||
@@ -1071,7 +1014,6 @@ class VersatileProjectSyncClient(
|
||||
httpCon.setRequestProperty("Accept", "application/json")
|
||||
}
|
||||
httpCon.connectTimeout = 10 * 1000 // 10 seconds
|
||||
Log.d(javaClass.simpleName, "$method $target")
|
||||
if (paramKeys != null && paramValues != null) {
|
||||
var dataString = ""
|
||||
for (i in paramKeys.indices) {
|
||||
@@ -1084,7 +1026,6 @@ class VersatileProjectSyncClient(
|
||||
dataString += URLEncoder.encode(value, "UTF-8")
|
||||
}
|
||||
val data = dataString.toByteArray()
|
||||
Log.d(javaClass.simpleName, "Params: $dataString")
|
||||
httpCon.setFixedLengthStreamingMode(data.size)
|
||||
httpCon.setRequestProperty("Content-Type", "application/x-www-form-urlencoded")
|
||||
httpCon.setRequestProperty("Content-Length", data.size.toString())
|
||||
|
||||
@@ -2,10 +2,39 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="108dp"
|
||||
android:height="108dp"
|
||||
android:viewportWidth="100"
|
||||
android:viewportHeight="100">
|
||||
<!-- Matches #0F172A Base -->
|
||||
<path
|
||||
android:pathData="M0,0h100v100h-100z"
|
||||
android:fillColor="#0F172A"/>
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<group android:translateY="-0.33333334">
|
||||
<group android:scaleX="0.63461536"
|
||||
android:scaleY="0.63461536"
|
||||
android:translateX="13.153846"
|
||||
android:translateY="13.153846">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<!-- Inner Face (Expanded path) -->
|
||||
<path
|
||||
android:fillColor="#DAA520"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Center Detail (Pure path replacement for the center circle) -->
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
|
||||
|
||||
|
||||
<!-- Massive Background Coin Base -->
|
||||
<path
|
||||
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
|
||||
android:fillColor="#FBBF24"/>
|
||||
|
||||
<!-- Coin Decorative Dotted Inner Rim -->
|
||||
<path
|
||||
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
|
||||
android:strokeColor="#D97706"
|
||||
android:strokeWidth="4"/>
|
||||
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -9,20 +9,6 @@
|
||||
android:scaleY="0.63461536"
|
||||
android:translateX="13.153846"
|
||||
android:translateY="13.153846">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<!-- Inner Face (Expanded path) -->
|
||||
<path
|
||||
android:fillColor="#DAA520"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Center Detail (Pure path replacement for the center circle) -->
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
|
||||
|
||||
|
||||
<group android:name="color">
|
||||
<path android:fillColor="#fff" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
|
||||
@@ -43,17 +29,6 @@
|
||||
</group>
|
||||
|
||||
|
||||
<!-- Massive Background Coin Base -->
|
||||
<path
|
||||
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
|
||||
android:fillColor="#FBBF24"/>
|
||||
|
||||
<!-- Coin Decorative Dotted Inner Rim -->
|
||||
<path
|
||||
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
|
||||
android:strokeColor="#D97706"
|
||||
android:strokeWidth="4"/>
|
||||
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
|
||||
@@ -9,17 +9,6 @@
|
||||
android:translateX="13.153846"
|
||||
android:translateY="13.153846">
|
||||
|
||||
<!-- Circle Outline -->
|
||||
<path
|
||||
android:strokeColor="#000"
|
||||
android:strokeWidth="2.5"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<path
|
||||
android:strokeColor="#000"
|
||||
android:strokeWidth="2.5"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Cow Outlines -->
|
||||
<group android:name="color">
|
||||
<path android:fillColor="#000" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
|
||||
@@ -27,5 +16,16 @@
|
||||
<path android:fillColor="#000" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#000" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
|
||||
</group>
|
||||
<group android:name="line">
|
||||
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
|
||||
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
|
||||
</group>
|
||||
</group>
|
||||
</vector>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="128dp"
|
||||
android:height="128dp"
|
||||
android:viewportWidth="72"
|
||||
android:viewportHeight="72">
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,2 C17.22,2 2,17.22 2,36 C2,54.78 17.22,70 36,70 C54.78,70 70,54.78 70,36 C70,17.22 54.78,2 36,2 Z" />
|
||||
|
||||
<!-- Inner Face (Expanded path) -->
|
||||
<path
|
||||
android:fillColor="#DAA520"
|
||||
android:pathData="M36,8 C20.54,8 8,20.54 8,36 C8,51.46 20.54,64 36,64 C51.46,64 64,51.46 64,36 C64,20.54 51.46,8 36,8 Z" />
|
||||
|
||||
<!-- Center Detail (Pure path replacement for the center circle) -->
|
||||
<path
|
||||
android:fillColor="#FFD700"
|
||||
android:pathData="M36,22 C28.27,22 22,28.27 22,36 C22,43.73 28.27,50 36,50 C43.73,50 50,43.73 50,36 C50,28.27 43.73,22 36,22 Z" />
|
||||
|
||||
|
||||
<group android:name="color">
|
||||
<path android:fillColor="#fff" android:pathData="m15,12.0211h0c0,.6116.3056,1.1827.8145,1.5219l4.2449,2.83c.5764.3843,1.774.8721,1.688,1.5596l-1.7051,6.7284c-.028.2241-.0143.4514.0405.6705l1.8343,7.3371c.0548.2191.0685.4464.0405.6705l-.9152,7.322c-.028.2241-.0143.4514.0405.6705l1.7456,6.9825c.1104.4414.3813.8259.7599,1.0783l1.7312,1.1541c.433.2887.7225.7481.7961,1.2632l-.2598,3.7979s-.2916,3.1088,2.3012,6.051c1.6332,1.8533,5.7148,3.8846,8.2656,3.8847,3.3409,0,5.9547-1.9378,7.3949-3.6465.8914-1.0576,2.5398-2.4978,3.2821-5.8244.2014-.9023-.0996-3.6967-.0996-3.6967,0-.2477.4009-1.311.4985-1.5386l1.3063-2.5077c.0158-.0369.0264-.0595.0389-.0795.5394-.8642.7885-1.8786.8851-2.8927,0,0,.9043-4.9747,1.1585-6.6406.005-.0325-.4996-5.5158-.4953-5.5485l1.5496-6.8172c.0386-.2315.0323-.4683-.0187-.6975l-1.5364-6.9139c-.2151-.9678.3801-1.9308,1.3419-2.1712l1.6416-.4104c.4075-.1019.7676-.341,1.0196-.677l2.2445-2.9927c.2375-.3166.3658-.7017.3658-1.0974v-.1102c0-1.1701-1.1152-1.1434-2.2575-.8896l-6.191.4868c-.3593.0798-.7343.0494-1.0761-.0873l-4.3602-1.7441c-.0767-.0307-.1555-.0562-.2357-.0762l-7.3628-1.8407c-.3375-.0844-.692-.0708-1.022.0393l-5.3714,1.7905c-.0821.0274-.1622.0605-.2396.0992l-3.4104,1.7052c-.3092.1546-.656.2184-1,.184l-7.4623-.7462c-1.0768-.1077-2.0111.7379-2.0111,1.82Z"/>
|
||||
<path android:fillColor="#a57939" android:pathData="m65.1818,16.5746l-3.5658.2422c-.1304.0089-.2591.0347-.3828.077l-7.081,2.42c-.043.0147-.0854.0314-.1269.0499l-2.6409,1.1807c-.1171.0524-.2341.0883-.3607.1091-1.3737.2264-10.1731,1.8874-10.0194,7.2658,0,0-.4646,3.6003,1.951,6.1473,1.8803,1.9826,2.9889,4.6478,2.5192,7.3395-.3101,1.7768-1.2515,3.5306-3.4702,4.594h0l2.9104,1.0217c.4295.1508.767.4887.9174.9183l1.4763,4.2192,1.6616-1.7712c1.1136-1.1945,1.9689-5.8742,1.9689-5.8742.4855-3.6065.5032-7.264.0031-10.8685-.0148-.1064-.0287-.2031-.0417-.2889h0s1.2606-.965,1.2606-.965c.1608-.1231.3449-.2123.5411-.2622l5.5633-1.4149c.1716-.0436.3341-.1174.4799-.2177l4.3017-2.9608c.1811-.1247.3326-.2877.4436-.4775l2.6005-4.4465c.1016-.1737.1671-.3661.1927-.5658l.4849-3.7874c.1198-.9359-.6453-1.748-1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#a57939" android:pathData="m6.867,16.5746l3.5658.2422c.1304.0089.2591.0347.3828.077l7.081,2.42c.043.0147.0854.0314.1269.0499l2.6409,1.1807c.1171.0524.2341.0883.3607.1091,1.3737.2264,10.1731,1.8874,10.0194,7.2658,0,0,.4645,3.6003-1.951,6.1473-1.8803,1.9826-2.9889,4.6478-2.5192,7.3395.3101,1.7768,1.2515,3.5306,3.4702,4.594h0l-2.9104,1.0217c-.4295.1508-.767.4887-.9174.9183l-1.4763,4.2192-1.6616-1.7712c-1.1136-1.1945-2.0412-6.9083-2.0412-6.9083-.3413-3.1673-.1698-6.3689.5077-9.4817h0c.1234-.5672-.092-1.1546-.553-1.5075l-.4665-.3571c-.1858-.1422-.4024-.2389-.6323-.2824l-6.0587-1.1441c-.2049-.0387-.3994-.1197-.5711-.2379l-4.2616-2.9332c-.1811-.1247-.3326-.2877-.4436-.4775l-2.6005-4.4465c-.1016-.1737-.1671-.3661-.1927-.5657l-.485-3.7875c-.1198-.9359.6453-1.7481,1.5867-1.6841Z"/>
|
||||
<path android:fillColor="#f4aa41" android:pathData="m27.3197,47.9945l-.6598,1.1318c-.7217,2.1691-.9617,4.4695-.7033,6.7408h0c.1194,1.0495.9592,1.871,2.0111,1.9674l.0811.0074,6.4963-.5146c1.249-.0989,2.5045-.0796,3.7499.0577l5.9262.6536c.7598.0838,1.5171-.1772,2.064-.7112h0c.4669-.4559.7436-1.0718.7746-1.7237l.0484-1.0203c.1133-2.3893-.4008-4.7667-1.4912-6.8958h0c-.4317-.843-1.2874-1.3848-2.234-1.4148l-8.6286-.2728h-3.9632c-1.4296,0-2.7515.7595-3.4715,1.9945Z"/>
|
||||
|
||||
</group>
|
||||
<group android:name="line">
|
||||
<path android:fillColor="#000" android:pathData="m26.1616,31.0595s-3.0474-.219-3.8284-1-.781-2.0474,0-2.8284,2.0474-.781,2.8284,0c.7811.781,1,3.8284,1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m19.6191,26.2353l2.0377-9.248s-7.6806-.627-6.505-7.4454c0,0,9.9534,3.2917,12.7748.4702,0,0,6.2375-6.6642,16.1956-.0429,3.3226,2.2093,12.7748-.4702,12.7748-.4702,1.1756,6.8184-6.505,7.4454-6.505,7.4454l2.0377,9.248"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m28.4825,46h15.7265c.272,0,.5324.1138.7082.3213.8467.9992,3.1442,4.3471,2.0943,10.2966-.1836,1.0404-1.1647,1.7418-2.2037,1.5503-3.4759-.6406-11.324-1.7893-16.7592-.3263-1.1058.2976-2.2065-.454-2.3432-1.5909-.302-2.5115-.3157-6.6284,2.0103-9.8682.1751-.2438.4667-.3828.7668-.3828Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.8671,50.277s-.6865,2.855,3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m29.1276,61.4663s6.3482,7.9157,13.7936.3135"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m17.5177,20.1544c-3.0642-1.9576-7.3791-4.5873-12.4353-3.3426,0,0-1.4891,14.0288,15.3611,15.5179l1.2133.881s-2.2906,8.9177,1.2618,16.1018"/>
|
||||
<path android:fillColor="#000" android:pathData="m45.8871,31.0139s3.0474-.219,3.8284-1c.7811-.781.7811-2.0474,0-2.8284-.781-.781-2.0474-.781-2.8284,0-.781.781-1,3.8284-1,3.8284Z"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m43.1817,50.2341s.6865,2.855-3.1538,2.926"/>
|
||||
<path android:strokeColor="#000" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2" android:pathData="m54.7743,19.9585c3.0474-1.888,7.2683-4.4017,12.1921-3.1896,0,0,1.4891,14.0288-15.3611,15.5179l-1.2133.881s2.5382,7.9772-1.0141,15.1613"/>
|
||||
</group>
|
||||
|
||||
|
||||
<!-- Massive Background Coin Base -->
|
||||
<path
|
||||
android:pathData="M 256,76 A 180,180 0 1,1 255.9,76 Z"
|
||||
android:fillColor="#FBBF24"/>
|
||||
|
||||
<!-- Coin Decorative Dotted Inner Rim -->
|
||||
<path
|
||||
android:pathData="M 256,101 A 155,155 0 1,1 255.9,101 Z"
|
||||
android:strokeColor="#D97706"
|
||||
android:strokeWidth="4"/>
|
||||
|
||||
</vector>
|
||||
+214
-223
@@ -2,231 +2,148 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<string name="app_name">Cowspent</string>
|
||||
<string name="label_all_bills">All bills</string>
|
||||
<string name="action_create_bill">New bill</string>
|
||||
<string name="action_settings">App settings</string>
|
||||
<string name="action_scan_qrcode">Scan QRCode from Cospend/Cowspent</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_new_bill">New bill</string>
|
||||
<string name="action_add_project">Add project</string>
|
||||
<string name="action_save_bill">Save bill</string>
|
||||
<string name="action_edit_project">Edit project</string>
|
||||
<string name="action_share_project">Share project</string>
|
||||
<string name="setting_what">What?</string>
|
||||
<string name="setting_payer">Who paid?</string>
|
||||
<string name="setting_all">All</string>
|
||||
<string name="setting_none">None</string>
|
||||
|
||||
<string name="save_or_discard_bill_dialog_title">Bill edition</string>
|
||||
<string name="save_or_discard_bill_dialog_message">Bill values have been changed. What do you want to do?</string>
|
||||
<string name="save_or_discard_bill_dialog_save">Save</string>
|
||||
<string name="save_or_discard_bill_dialog_discard">Discard</string>
|
||||
<string name="currency_manager">Currency manager</string>
|
||||
<string name="add_currency_title">Add currency</string>
|
||||
<string name="currency_management_unavailable">Currency management is only available for Cospend projects</string>
|
||||
<string name="setting_comment">Comment</string>
|
||||
<string name="currency_dialog_title">Convert current amount from another currency into %s</string>
|
||||
<string name="no_currency_error">There is no additional currency</string>
|
||||
<string name="main_currency">Main currency</string>
|
||||
<string name="currency_edit_name">Name</string>
|
||||
<string name="currency_rate">Exchange rate</string>
|
||||
<string name="setting_owers">For whom?</string>
|
||||
<string name="setting_project_id">Project ID/name</string>
|
||||
|
||||
<string name="setting_project_repetition">Repeat this bill every</string>
|
||||
<string name="setting_payment_mode">Mode</string>
|
||||
<string name="setting_category">Category</string>
|
||||
|
||||
<string name="action_label_bills">Label missing categories</string>
|
||||
<string name="label_bills_title">Label Bills</string>
|
||||
<string name="label_bills_no_more">No more bills to label</string>
|
||||
<string name="label_bills_skip">Skip</string>
|
||||
<string name="label_bills_suggested">Suggested</string>
|
||||
<string name="label_bills_no_suggestions">No suggestions for this bill</string>
|
||||
<string name="new_project_what_todo">What do you want to do?</string>
|
||||
<string name="new_project_where">Where?</string>
|
||||
<string name="where_local_short">Local</string>
|
||||
<string name="where_cospend_short">Cospend</string>
|
||||
<string name="where_ihatemoney_short">ihatemoney</string>
|
||||
<string name="setting_ihatemoney_project_url">ihatemoney address</string>
|
||||
<string name="todo_join_label">Join</string>
|
||||
<string name="todo_create_label">Create</string>
|
||||
<string name="setting_cospend_project_url">Nextcloud address</string>
|
||||
<string name="setting_password">Password (local)</string>
|
||||
<string name="setting_new_project_password">Password</string>
|
||||
<string name="setting_new_project_name">Project title</string>
|
||||
<string name="setting_new_project_email">E-mail</string>
|
||||
<string name="warning_auth_project_creation">Warning: creating a Cospend project using Nextcloud account works with Cospend v0.3.4 and higher.</string>
|
||||
<string name="auth_project_creation_title">Authenticated project creation</string>
|
||||
<string name="project_edition_no_change">Nothing was changed</string>
|
||||
<string name="project_added_success">Project successfully added to Cowspent</string>
|
||||
<string name="action_save">Save</string>
|
||||
<string name="action_edit">Edit</string>
|
||||
<string name="action_share">Share</string>
|
||||
<string name="action_search">Search</string>
|
||||
<string name="action_delete">Delete</string>
|
||||
<string name="simple_error">Error</string>
|
||||
<string name="simple_close">Close</string>
|
||||
<string name="simple_loading">Loading</string>
|
||||
<string name="sync_error_dialog_title">Project sync failed</string>
|
||||
<string name="sync_error_dialog_full_content">Synchronization with project %1$s failed. Error message:\n\n%2$s\n\nThe associated remote project might not exist anymore. If so, you could remove this project from Cowspent.</string>
|
||||
<string name="error_maintenance_mode">The Nextcloud instance is probably in maintenance mode</string>
|
||||
<string name="action_archive">Archive</string>
|
||||
<string name="action_unarchive">Unarchive</string>
|
||||
<string name="action_export">Export</string>
|
||||
<string name="action_stats">Stats</string>
|
||||
<string name="action_settle">Settle</string>
|
||||
<string name="action_scan_qrcode">Scan QR Code</string>
|
||||
<string name="action_settings">Settings</string>
|
||||
<string name="action_label_bills">Label missing categories</string>
|
||||
<string name="action_logout">Logout</string>
|
||||
<string name="action_connect">Connect</string>
|
||||
<string name="action_discard">Discard</string>
|
||||
<string name="action_members">Members</string>
|
||||
<string name="action_labels">Labels</string>
|
||||
<string name="action_currencies">Currencies</string>
|
||||
|
||||
<!-- Titles -->
|
||||
<string name="title_stats">Statistics</string>
|
||||
<string name="title_edit_project">Edit project</string>
|
||||
<string name="title_label_bills">Label Bills</string>
|
||||
<string name="title_labels">Manage Labels</string>
|
||||
<string name="title_about">About</string>
|
||||
<string name="title_settle">Settle Project</string>
|
||||
<string name="title_share">Share Project</string>
|
||||
<string name="title_add_project">Add Project</string>
|
||||
<string name="title_account">Nextcloud Account</string>
|
||||
<string name="title_share_web">Web link</string>
|
||||
<string name="title_share_qr">Cowspent link</string>
|
||||
<string name="title_confirm">Are you sure?</string>
|
||||
|
||||
<!-- Labels and Fields -->
|
||||
<string name="label_all_bills">All bills</string>
|
||||
<string name="label_categories">Categories</string>
|
||||
<string name="label_payment_modes">Payment Modes</string>
|
||||
<string name="label_name">Name</string>
|
||||
<string name="label_icon">Icon / Emoji</string>
|
||||
<string name="label_color">Color</string>
|
||||
<string name="label_weight">Weight</string>
|
||||
<string name="label_activated">Activated</string>
|
||||
<string name="label_password">Password</string>
|
||||
<string name="label_email">E-mail</string>
|
||||
<string name="label_url">Server address</string>
|
||||
<string name="label_username">Username</string>
|
||||
<string name="label_comment">Comment</string>
|
||||
<string name="label_what">What?</string>
|
||||
<string name="label_payer">Who paid?</string>
|
||||
<string name="label_owers">For whom?</string>
|
||||
<string name="label_repeat">Repeat every</string>
|
||||
<string name="label_mode">Mode</string>
|
||||
<string name="label_category">Category</string>
|
||||
<string name="label_project_id">Project ID/name</string>
|
||||
<string name="label_project_title">Project title</string>
|
||||
<string name="label_use_sso">Use Nextcloud App Account</string>
|
||||
|
||||
<!-- Dialogs and Messages -->
|
||||
<string name="dialog_unsaved_changes_title">Unsaved changes</string>
|
||||
<string name="dialog_unsaved_changes_msg">Save changes before leaving?</string>
|
||||
<string name="dialog_confirm_remove_project_msg">The remote project will not be deleted.</string>
|
||||
<string name="dialog_sync_error_title">Sync error</string>
|
||||
<string name="dialog_sync_error_msg">Sync failed for %1$s.\n\n%2$s</string>
|
||||
<string name="dialog_balanced_msg">Expenses are already balanced.</string>
|
||||
<string name="msg_project_added">Project %1$s added</string>
|
||||
<string name="msg_bill_labeled_done">All bills labeled</string>
|
||||
<string name="msg_no_suggestions">No suggestions</string>
|
||||
<string name="msg_auth_warning">Requires Cospend v0.3.4+.</string>
|
||||
<string name="msg_link_copied">Link copied to clipboard</string>
|
||||
<string name="msg_share_qr">Scan QR code or share the link to join.</string>
|
||||
<string name="msg_share_web">Link for web browser access.</string>
|
||||
<string name="msg_share_qr_warn">Share this link with a Cowspent user.</string>
|
||||
<string name="msg_settle_intro">Settlement for %1$s:</string>
|
||||
<string name="msg_settle_sentence">%1$s owes %3$.2f to %2$s</string>
|
||||
<string name="msg_stats_intro">Stats for %1$s:</string>
|
||||
<string name="msg_stats_header">Member (Paid | Spent | Balance)</string>
|
||||
<string name="msg_logged_in_as">Logged in as %1$s</string>
|
||||
|
||||
<!-- Errors -->
|
||||
<string name="error_generic">Error</string>
|
||||
<string name="error_loading">Loading</string>
|
||||
<string name="error_no_projects">No projects found</string>
|
||||
<string name="error_no_members">No members found</string>
|
||||
<string name="error_no_bills">No bills found</string>
|
||||
<string name="error_no_member">At least one member required</string>
|
||||
<string name="error_maintenance_mode">Server is in maintenance mode</string>
|
||||
<string name="error_400">400 Bad request</string>
|
||||
<string name="error_401">401 Unauthorized</string>
|
||||
<string name="error_403">403 Forbidden</string>
|
||||
<string name="error_404">404 Not Found</string>
|
||||
<string name="creating_remote_project">Creating remote project</string>
|
||||
<string name="error_sync">Sync failed: %1$s</string>
|
||||
<string name="error_invalid_login">Invalid login: %1$s</string>
|
||||
<string name="error_auth">Wrong username or password</string>
|
||||
<string name="error_json">Invalid server response</string>
|
||||
<string name="error_req_failed">Request failed</string>
|
||||
<string name="error_invalid_email">Invalid e-mail</string>
|
||||
<string name="error_invalid_project_id">Invalid project ID</string>
|
||||
<string name="error_invalid_project_name">Invalid project title</string>
|
||||
<string name="error_invalid_bill_name">Invalid bill name</string>
|
||||
<string name="error_invalid_bill_date">Invalid bill date</string>
|
||||
<string name="error_invalid_bill_payer">Payer required</string>
|
||||
<string name="error_invalid_bill_owers">Owers required</string>
|
||||
<string name="error_no_network">No network connection</string>
|
||||
<string name="error_server">Server error</string>
|
||||
<string name="error_io">Server connection broken</string>
|
||||
<string name="error_share_impossible">Cannot share this project</string>
|
||||
|
||||
<!-- Drawer / Common UI -->
|
||||
<string name="drawer_no_account">Connect to Nextcloud account</string>
|
||||
<string name="drawer_last_sync">Last sync: %1$02d:%2$02d</string>
|
||||
<string name="simple_cancel">Cancel</string>
|
||||
<string name="simple_ok" tools:ignore="ButtonCase,Typos">Ok</string>
|
||||
<string name="simple_yes">Yes</string>
|
||||
<string name="simple_no">No</string>
|
||||
<string name="simple_add">Add</string>
|
||||
<string name="simple_edit_bill">Edit bill</string>
|
||||
<string name="simple_new_bill">New bill</string>
|
||||
<string name="simple_add_project">Add project in Cowspent</string>
|
||||
<string name="simple_edit_project">Edit project</string>
|
||||
<string name="simple_about">About</string>
|
||||
<string name="share_impossible">Impossible to share this project</string>
|
||||
<string name="simple_settle_share">Share</string>
|
||||
<string name="simple_create_bills">Create bills</string>
|
||||
<string name="settle_bill_what">Settlement bill</string>
|
||||
<string name="simple_share_share">Share Cowspent link</string>
|
||||
<string name="menu_delete_project_remote">Delete project on server</string>
|
||||
<string name="menu_save_project">Save project</string>
|
||||
<string name="fab_add_member">Add a member</string>
|
||||
<string name="fab_rm_project">Remove Project</string>
|
||||
<string name="remove_project_confirmation">Project %1$s has been removed</string>
|
||||
<string name="choose_project_management_action">Manage project</string>
|
||||
<string name="fab_manage_members">Manage members</string>
|
||||
<string name="fab_export_project">Export project</string>
|
||||
<string name="drawer_last_sync_text">Last sync: %1$02d:%2$02d</string>
|
||||
<string name="scan_qrcode">Scan QR code</string>
|
||||
<string name="new_project_from_nextcloud_tooltip">Choose from Nextcloud account</string>
|
||||
<string name="import_tooltip">Import from CSV file</string>
|
||||
<string name="import_error_owers">Malformed CSV, bad owers on line %1$d</string>
|
||||
<string name="import_error_date">Malformed CSV, bad date on line %1$d</string>
|
||||
<string name="import_error_header">Malformed CSV, bad column names on line %1$d</string>
|
||||
<string name="member_already_exists">This member already exists in this project</string>
|
||||
<string name="confirm_remove_project_dialog_title">Are you sure?</string>
|
||||
<string name="confirm_delete_project_dialog_title">Are you sure you want to delete remote project?</string>
|
||||
<string name="confirm_remove_project_dialog_message">The remote project will NOT be deleted</string>
|
||||
<string name="member_edit_toggle">Activated</string>
|
||||
<string name="member_edit_name">Name</string>
|
||||
<string name="member_edit_color">Color</string>
|
||||
<string name="member_edit_weight">Weight</string>
|
||||
<string name="member_edit_delete">Delete member</string>
|
||||
<string name="remote_project_operation_no_network">Remote project operations require network connectivity</string>
|
||||
<string name="insufficient_access_level">You are not allowed to perform this action</string>
|
||||
<string name="simple_close">Close</string>
|
||||
|
||||
<!-- Settings -->
|
||||
<string name="settings_appearance_category">Appearance</string>
|
||||
<string name="settings_network_category">Network</string>
|
||||
<string name="settings_other_category">Other</string>
|
||||
<string name="settings_about">About Cowspent</string>
|
||||
<string name="settings_server_settings">Nextcloud account</string>
|
||||
<string name="account_logged_in_as">Logged in as %1$s</string>
|
||||
<string name="account_logout">Logout</string>
|
||||
<string name="settings_url">Server address</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Please use "https".</string>
|
||||
<string name="settings_username">Username</string>
|
||||
<string name="settings_password">Password</string>
|
||||
<string name="settings_submit">Connect</string>
|
||||
<string name="settings_night_mode_title">Theme</string>
|
||||
<string name="settings_offline_mode_title">Offline mode</string>
|
||||
<string name="settings_appearance">Appearance</string>
|
||||
<string name="settings_network">Network</string>
|
||||
<string name="settings_other">Other</string>
|
||||
<string name="settings_night_mode">Theme</string>
|
||||
<string name="settings_offline_mode">Offline mode</string>
|
||||
<string name="settings_offline_mode_summary">Only sync manually.</string>
|
||||
<string name="settings_color_title">Custom color</string>
|
||||
<string name="settings_color_summary">UI Accent color</string>
|
||||
<string name="settings_colorpicker_title">Choose color</string>
|
||||
<string name="settings_color_mode_title">Color Selection</string>
|
||||
<string name="settings_color_custom">Custom color</string>
|
||||
<string name="settings_color_mode">Color Selection</string>
|
||||
<string name="settings_show_archived">Show archived projects</string>
|
||||
<string name="settings_url_warn_http">WARNING: "http" is unsafe. Use "https".</string>
|
||||
<string name="settings_colorpicker_title">Choose Color</string>
|
||||
|
||||
<string name="pref_value_color_system">System</string>
|
||||
<string name="pref_value_color_server">Nextcloud</string>
|
||||
<string name="pref_value_color_manual">Manual</string>
|
||||
<string name="pref_value_theme_light">Light</string>
|
||||
<string name="pref_value_theme_dark">Dark</string>
|
||||
<string name="pref_value_theme_system">Follow system</string>
|
||||
|
||||
|
||||
<string name="currency_saved_success">Main currency saved</string>
|
||||
<string name="file_saved_success">Project saved in %s</string>
|
||||
<string name="add_member_dialog_title">New member name</string>
|
||||
<string name="edit_member_dialog_title">Edit member</string>
|
||||
<string name="center_none">Optimal</string>
|
||||
<string name="settle_who">Who pays?</string>
|
||||
<string name="settle_to_whom">To whom?</string>
|
||||
<string name="settle_how_much">How much?</string>
|
||||
<string name="total">Total Spent: %s</string>
|
||||
<string name="stats_date_min">From</string>
|
||||
<string name="stats_date_max">To</string>
|
||||
<string name="stats_who">Who</string>
|
||||
<string name="stats_paid">Paid</string>
|
||||
<string name="stats_spent">Spent</string>
|
||||
<string name="stats_balance">Balance</string>
|
||||
<string name="fab_statistics">Statistics</string>
|
||||
<string name="fab_settle">Settle</string>
|
||||
<string name="project_add_success_message">Project %1$s was successfully added</string>
|
||||
<string name="project_create_success_message">Project %1$s was successfully created</string>
|
||||
<string name="project_add_success_title">Project added</string>
|
||||
<string name="project_create_success_title">Project created</string>
|
||||
<string name="statistic_title">Statistics</string>
|
||||
<string name="settle_dialog_title">Settle the project</string>
|
||||
<string name="settle_dialog_balanced">The expenses in this project are already balanced. There is nothing to settle!</string>
|
||||
<string name="share_dialog_title">Share the project</string>
|
||||
<string name="share_project_dialog_message">Scan this QRCode with Cowspent or a scanner app on an Android device where Cowspent is installed. You can also simply send/share the Cowspent link above.</string>
|
||||
<string name="share_project_public_url_dialog_message">Share this web link to let others access the project with a web browser.</string>
|
||||
<string name="share_project_public_url_title">Web link</string>
|
||||
<string name="share_project_public_qrcode_title">Cowspent link</string>
|
||||
<string name="qrcode_link_open_attempt_warning">Share this link to a Cowspent user.</string>
|
||||
<string name="share_share_intent_title">Cowspent link of %1$s</string>
|
||||
<string name="share_share_chooser_title">Share Cowspent link of %1$s</string>
|
||||
<string name="share_settle_intro">Here is how to settle project %1$s :</string>
|
||||
<string name="share_settle_sentence">%1$s owes %3$.2f to %2$s</string>
|
||||
<string name="share_settle_title">Settle the project %1$s</string>
|
||||
<string name="share_stats_intro">Statistics of project %1$s :</string>
|
||||
<string name="share_stats_header">MEMBER_NAME (PAID | SPENT | BALANCE)</string>
|
||||
<string name="drawer_no_account">Click here to connect to a Nextcloud account</string>
|
||||
<string name="add_bill_impossible_no_member">At least one activated member is required to add a bill</string>
|
||||
<string name="edit_project_local_impossible">Impossible for local projects</string>
|
||||
<string name="choose_account_project_dialog_title">Choose a project from your Nextcloud account</string>
|
||||
<string name="choose_account_project_dialog_impossible">There is no Cospend project in your Nextcloud account</string>
|
||||
<string name="activity_dialog_title">Activity in project %1$s</string>
|
||||
|
||||
<!-- Bills list corner cases -->
|
||||
<string name="no_projects_title">No Projects</string>
|
||||
<string name="no_projects_text">You don\'t have any projects yet. If you have Cospend installed on a Nextcloud server, you can configure your Nextcloud account. Alternatively, you can manually add projects.</string>
|
||||
<string name="configure_account_choice">Configure Nextcloud account</string>
|
||||
<string name="add_project_choice">Manually add a project (Cospend or local)</string>
|
||||
<string name="no_members_title">No Members</string>
|
||||
<string name="no_members_text">This project doesn\'t have any members. In order to create bills, you need to add at least one member (better two or more).</string>
|
||||
<string name="no_bills_title">No Bills</string>
|
||||
<string name="no_bills_text">This project doesn\'t have any bills. Click the button below to add your first bill.</string>
|
||||
|
||||
<!-- Certificates -->
|
||||
|
||||
<!-- Network -->
|
||||
|
||||
<!-- Error -->
|
||||
<string name="error_project_connect_check">Impossible to reach the remote project:\n\n%1$s</string>
|
||||
<string name="error_sync">Project sync failed: %1$s</string>
|
||||
<string name="error_invalid_login">Invalid login: %1$s</string>
|
||||
<string name="error_json">is the Cospend app activated on the server?</string>
|
||||
<string name="error_io">server connection is broken</string>
|
||||
<string name="error_no_network">no network connection</string>
|
||||
<string name="error_server">server error</string>
|
||||
<string name="error_username_password_invalid">wrong username or password</string>
|
||||
<string name="error_invalid_email">Invalid e-mail address</string>
|
||||
<string name="error_invalid_project_remote_id">Invalid project ID/name</string>
|
||||
<string name="error_invalid_project_name">Invalid project title</string>
|
||||
<string name="error_invalid_bill_what">Invalid bill name</string>
|
||||
<string name="error_invalid_bill_date">Invalid bill date</string>
|
||||
<string name="error_invalid_bill_payerid">Bill should have a payer</string>
|
||||
<string name="error_invalid_bill_owers">Bill should have at least one ower</string>
|
||||
<string name="error_create_remote_project_helper">Error at remote project creation : %s</string>
|
||||
<string name="error_edit_remote_project_helper">Error at remote project edition : %s</string>
|
||||
<string name="error_token_mismatch">Please reauthenticate your SSO account in settings</string>
|
||||
<string name="error_req_failed">Request failed</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="about_version">Version %1$s</string>
|
||||
<string name="about_maintainer_title">Maintainer</string>
|
||||
<string name="about_maintainer" translatable="false">Helcel.net</string>
|
||||
<string name="about_license_title">License</string>
|
||||
<string name="about_license" translatable="false">GNU GPL3x / CC BY-SA 4.0</string>
|
||||
<string name="about_source_title">Source code</string>
|
||||
<string name="about_source" translatable="false">https://github.com/helcel-net/cowspent</string>
|
||||
|
||||
<!-- Key / Values for preferences -->
|
||||
<!-- Constants (Do not translate) -->
|
||||
<string name="pref_key_night_mode" translatable="false">nightMode</string>
|
||||
<string name="pref_key_color" translatable="false">appColor</string>
|
||||
<string name="pref_key_server_color" translatable="false">serverColor</string>
|
||||
@@ -236,36 +153,28 @@
|
||||
<string name="pref_key_color_mode" translatable="false">colorMode</string>
|
||||
<string name="pref_key_offline_mode" translatable="false">offlineMode</string>
|
||||
<string name="pref_key_show_archived" translatable="false">showArchived</string>
|
||||
<string name="pref_value_theme_light">Light</string>
|
||||
<string name="pref_value_theme_dark">Dark</string>
|
||||
<string name="pref_value_theme_system">Follow system</string>
|
||||
<string name="pref_value_night_mode_no" translatable="false">1</string>
|
||||
<string name="pref_value_night_mode_yes" translatable="false">2</string>
|
||||
<string name="pref_value_night_mode_system" translatable="false">-1</string>
|
||||
<string name="use_sso_toggle">Use Nextcloud app account</string>
|
||||
|
||||
<!-- QR Code Scanner -->
|
||||
<string name="error_scanning_bill_qr_code">QR code could not be scanned.</string>
|
||||
<string name="fab_manage_currencies">Manage currencies</string>
|
||||
<string name="setting_show_archived">Show archived projects</string>
|
||||
|
||||
|
||||
|
||||
<!-- Enums and Lists -->
|
||||
<string name="repeat_no">No repeat</string>
|
||||
<string name="repeat_day">Daily</string>
|
||||
<string name="repeat_week">Weekly</string>
|
||||
<string name="repeat_fortnight">Fortnightly</string>
|
||||
<string name="repeat_month">Monthly</string>
|
||||
<string name="repeat_year">Yearly</string>
|
||||
<string name="payment_mode_none">No payment mode</string>
|
||||
<string name="payment_mode_all">All payment modes</string>
|
||||
|
||||
<string name="payment_mode_none">None</string>
|
||||
<string name="payment_mode_all">All</string>
|
||||
<string name="payment_mode_credit_card">Credit card</string>
|
||||
<string name="payment_mode_cash">Cash</string>
|
||||
<string name="payment_mode_check">Check</string>
|
||||
<string name="payment_mode_online">Online service</string>
|
||||
<string name="payment_mode_online">Online</string>
|
||||
<string name="payment_mode_transfer">Transfer</string>
|
||||
<string name="category_none">No category</string>
|
||||
<string name="category_all">All categories</string>
|
||||
|
||||
<string name="category_none">None</string>
|
||||
<string name="category_all">All</string>
|
||||
<string name="category_all_except_reimbursement">All except reimbursement</string>
|
||||
<string name="category_groceries">Grocery</string>
|
||||
<string name="category_leisure">Bar/Party</string>
|
||||
@@ -279,4 +188,86 @@
|
||||
<string name="category_accomodation">Accommodation</string>
|
||||
<string name="category_transport">Transport</string>
|
||||
<string name="category_sport">Sport</string>
|
||||
|
||||
<!-- Project specific -->
|
||||
<string name="new_project_action">What</string>
|
||||
<string name="new_project_where">Where</string>
|
||||
<string name="where_local">Local only</string>
|
||||
<string name="where_cospend">Cospend</string>
|
||||
<string name="where_ihatemoney">IHateMoney</string>
|
||||
<string name="todo_join">Join existing project</string>
|
||||
<string name="todo_create">Create new project</string>
|
||||
<string name="import_tooltip">Import from file</string>
|
||||
<string name="choose_account_project_dialog_title">Choose project</string>
|
||||
<string name="choose_account_project_dialog_impossible">No projects found on this account.</string>
|
||||
<string name="choose_project_management_action">Project</string>
|
||||
<string name="project_added_success">Project added successfully.</string>
|
||||
<string name="no_projects_text">You have no projects yet.</string>
|
||||
<string name="configure_account_choice">Configure Nextcloud account</string>
|
||||
<string name="add_project_choice">Add project manually</string>
|
||||
<string name="no_members_text">No members in this project.</string>
|
||||
<string name="no_bills_text">No bills found.</string>
|
||||
<string name="member_already_exists">Member already exists.</string>
|
||||
<string name="activity_dialog_title">Project: %1$s</string>
|
||||
<string name="remove_project_confirmation">Project %1$s removed.</string>
|
||||
<string name="file_saved_success">File saved: %1$s</string>
|
||||
<string name="import_error_header">Import failed at row %d</string>
|
||||
<string name="import_error_date">Invalid date format at row %d</string>
|
||||
<string name="import_error_owers">Invalid owers at row %d</string>
|
||||
<string name="add_member_dialog_title">Add Member</string>
|
||||
<string name="edit_member_dialog_title">Edit Member</string>
|
||||
<string name="member_edit_delete">Delete</string>
|
||||
<string name="project_edition_no_change">No changes to save.</string>
|
||||
|
||||
<!-- Settlement -->
|
||||
<string name="center_none">None (Optimal)</string>
|
||||
<string name="settle_who">Who pays</string>
|
||||
<string name="settle_to_whom">To whom</string>
|
||||
<string name="settle_how_much">Amount</string>
|
||||
<string name="simple_settle_share">Share</string>
|
||||
<string name="simple_create_bills">Create bills</string>
|
||||
<string name="settle_bill_what">Settlement</string>
|
||||
|
||||
<!-- Currencies -->
|
||||
<string name="currency_dialog_title">Choose Currency</string>
|
||||
<string name="setting_none">None</string>
|
||||
<string name="setting_all">All</string>
|
||||
<string name="currency_saved_success">Currency settings saved.</string>
|
||||
<string name="main_currency">Main Currency</string>
|
||||
|
||||
<!-- Statistics -->
|
||||
<string name="label_bills_suggested">Suggested Categories</string>
|
||||
<string name="label_bills_skip">Skip</string>
|
||||
<string name="stats_date_min">From</string>
|
||||
<string name="stats_date_max">To</string>
|
||||
<string name="stats_who">Member</string>
|
||||
<string name="stats_paid">Paid</string>
|
||||
<string name="stats_spent">Spent</string>
|
||||
<string name="stats_balance">Balance</string>
|
||||
<string name="total">Total: %1$s</string>
|
||||
|
||||
<!-- Errors Extra -->
|
||||
<string name="error_project_connect_check">Connection failed: %1$s</string>
|
||||
<string name="error_create_remote_project_helper">Creation failed: %1$s</string>
|
||||
<string name="error_edit_remote_project_helper">Error updating remote project: %1$s</string>
|
||||
<string name="remote_project_operation_no_network">Network unavailable for remote operation.</string>
|
||||
<string name="error_scanning_bill_qr_code">Failed to parse QR code.</string>
|
||||
<string name="error_token_mismatch">Authentication token mismatch. Please log in again.</string>
|
||||
<string name="insufficient_access_level">You don\'t have permission to perform this action.</string>
|
||||
<string name="delete_label_confirmation_title">Delete Label</string>
|
||||
<string name="delete_label_confirmation_message">Are you sure you want to delete this label?</string>
|
||||
|
||||
<!-- About -->
|
||||
<string name="about_version">Version %1$s</string>
|
||||
<string name="about_maintainer_title">Maintainer</string>
|
||||
<string name="about_maintainer" translatable="false">Helcel.net</string>
|
||||
<string name="about_license_title">License</string>
|
||||
<string name="about_license" translatable="false">GNU GPL3x / CC BY-SA 4.0</string>
|
||||
<string name="about_source_title">Source code</string>
|
||||
<string name="about_source" translatable="false">https://github.com/helcel-net/cowspent</string>
|
||||
|
||||
<!-- New constants for backward compatibility or shared use -->
|
||||
<string name="share_intent_title">Project %1$s</string>
|
||||
<string name="share_chooser_title">Share %1$s</string>
|
||||
|
||||
</resources>
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-9.6.1-bin.zip
|
||||
networkTimeout=10000
|
||||
retries=0
|
||||
retryBackOffMs=500
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
Reference in New Issue
Block a user