4 Commits

Author SHA1 Message Date
soraefir 7581985abd Stripping fix 2026-07-01 22:12:42 +02:00
soraefir 810ca20201 Custom Labels, User archive, Versioning, String and UI cleanup 2026-07-01 22:10:08 +02:00
soraefir 678a90ead9 Archiving and Label options 2026-06-30 20:40:32 +02:00
soraefir 88469fa96c Fix monochrome logo 2026-06-30 19:31:39 +02:00
45 changed files with 1821 additions and 893 deletions
+2 -1
View File
@@ -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
+8 -4
View File
@@ -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
}
@@ -83,6 +83,10 @@ android {
generateLocaleConfig = true
}
packaging {
jniLibs.keepDebugSymbols.add("**/*.so")
}
dependenciesInfo {
// Disables dependency metadata when building APKs (for IzzyOnDroid/F-Droid)
includeInApk = false
@@ -92,8 +96,8 @@ android {
}
dependencies {
implementation 'androidx.compose.foundation:foundation:1.11.3'
implementation 'androidx.compose.runtime:runtime:1.11.3'
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'
@@ -106,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.06.00')
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'
+19 -7
View File
@@ -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)
@@ -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))
}
}
}
@@ -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)
)
@@ -255,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) {
@@ -288,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)
)
}
}
@@ -164,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)
@@ -172,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) }
)
@@ -262,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,
@@ -340,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))
@@ -447,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 {
@@ -497,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 {
@@ -548,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 },
@@ -572,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 = {
@@ -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))
}
}
}
@@ -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))
}
}
@@ -96,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)
@@ -125,11 +125,11 @@ fun ManageCurrenciesScreen(
// Section 1: Main Currency
Text(
text = stringResource(R.string.main_currency).uppercase(),
style = MaterialTheme.typography.overline,
color = MaterialTheme.colors.primary,
fontWeight = FontWeight.Bold
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
Card(
shape = RoundedCornerShape(12.dp),
elevation = 2.dp,
@@ -153,11 +153,11 @@ fun ManageCurrenciesScreen(
val isEditing = viewModel.editingCurrencyId != null
Text(
text = (if (isEditing) "Edit exchange rate" else "Add exchange rate").uppercase(),
style = MaterialTheme.typography.overline,
color = if (isEditing) MaterialTheme.colors.secondary else MaterialTheme.colors.primary,
fontWeight = FontWeight.Bold
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
Spacer(modifier = Modifier.height(8.dp))
// Integrated Add/Edit Ribbon
Surface(
@@ -236,9 +236,10 @@ fun ManageCurrenciesScreen(
Text(
text = "SAVED RATES",
style = MaterialTheme.typography.overline,
color = MaterialTheme.colors.onSurface.copy(alpha = 0.4f),
fontWeight = FontWeight.Bold
style = MaterialTheme.typography.subtitle1,
color = MaterialTheme.colors.onSurface,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(bottom = 8.dp)
)
}
@@ -160,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))
@@ -200,6 +207,7 @@ private fun DrawerHeader(
}
if (lastSyncText.isNotEmpty()) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.width(16.dp))
Icon(
Icons.Default.Sync,
contentDescription = null,
@@ -291,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",
@@ -134,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)
}
}
}
@@ -29,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
@@ -44,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))
}
}
@@ -65,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)
)
}
}
@@ -78,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)
)
}
}
@@ -122,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
)
@@ -151,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
)
}
}
}
@@ -168,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)
}
@@ -45,6 +45,7 @@ import net.helcel.cowspent.model.Category
import net.helcel.cowspent.model.DBBill
import net.helcel.cowspent.model.DBProject
import net.helcel.cowspent.model.GroupedBill
import net.helcel.cowspent.model.ProjectType
import net.helcel.cowspent.persistence.CowspentSQLiteOpenHelper
import net.helcel.cowspent.persistence.CowspentServerSyncHelper
import net.helcel.cowspent.theme.ThemeUtils
@@ -118,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)
}
@@ -220,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 {
@@ -254,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 = {
@@ -382,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) {
@@ -401,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 {
@@ -423,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 {
@@ -432,7 +468,7 @@ class BillsListViewActivity :
return@launch
}
viewModel.showMemberManagementDialogByProjectId = projectId
startActivity(net.helcel.cowspent.android.project.member.MemberManagementActivity.createIntent(this@BillsListViewActivity, projectId))
}
}
@@ -459,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)
}
}
@@ -560,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
}
}
@@ -658,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 {
@@ -772,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
)
@@ -793,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))
}
}
}
@@ -70,11 +71,11 @@ fun NewProjectScreen(
) {
// 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
@@ -84,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 }
)
@@ -99,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 }
)
@@ -131,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,
@@ -148,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) }
@@ -166,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) }
)
@@ -177,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) }
)
@@ -188,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) }
)
@@ -198,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) }
)
@@ -210,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
@@ -265,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 = {}
@@ -280,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 }) {
@@ -296,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)) {
@@ -16,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
@@ -61,24 +62,26 @@ 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 ->
@@ -91,43 +94,59 @@ fun EditProjectScreen(
.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)
@@ -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)
}
}
}
}
@@ -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 = {}
)
}
}
@@ -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 = {}
)
}
}
@@ -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)
}
}
}
@@ -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)
@@ -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)
}
}
@@ -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),
@@ -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>
+214 -217
View File
@@ -2,225 +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="setting_comment">Comment</string>
<string name="currency_dialog_title">Convert current amount from another currency into %s</string>
<string name="main_currency">Main currency</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_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>
@@ -230,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>
@@ -273,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>