diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 82c5eaa..50903f1 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index e27c227..9b8ea02 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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 } @@ -92,8 +92,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 +106,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' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 778d94d..2e082dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,7 +46,7 @@ @@ -102,7 +102,7 @@ @@ -110,20 +110,32 @@ + + + + diff --git a/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt b/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt index e9ae939..aa34814 100644 --- a/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/about/AboutScreen.kt @@ -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) diff --git a/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt b/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt index 9c7c213..c6e25d0 100644 --- a/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/account/AccountScreen.kt @@ -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)) } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt b/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt index c630148..1b45f1c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/account/LoginDialog.kt @@ -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) ) diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt index fa6af65..7906472 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillActivity.kt @@ -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 = { diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt index 1aeeb30..89412fc 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_edit/EditBillScreen.kt @@ -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, 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 = { diff --git a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt index cf1263a..f1e06bb 100644 --- a/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/bill_label/LabelBillsScreen.kt @@ -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)) } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt index 41dda19..c47fd0d 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesActivity.kt @@ -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)) } } diff --git a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt index 5e3c8ee..030477a 100644 --- a/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/currencies/ManageCurrenciesScreen.kt @@ -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) ) } diff --git a/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt b/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt index 397a020..24e5bb0 100644 --- a/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt +++ b/app/src/main/java/net/helcel/cowspent/android/drawer/Drawer.kt @@ -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( diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt b/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt index 421a38c..99fb016 100644 --- a/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/helper/AlertDialog.kt @@ -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 + ) } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt b/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt index 12855ad..0c9ec90 100644 --- a/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt +++ b/app/src/main/java/net/helcel/cowspent/android/helper/ColorPicker.kt @@ -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", diff --git a/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt index 2204e41..3837685 100644 --- a/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/helper/QrCodeScannerActivity.kt @@ -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) diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt new file mode 100644 index 0000000..d6bfad0 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementActivity.kt @@ -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) + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt new file mode 100644 index 0000000..5568011 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementScreen.kt @@ -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, + paymentModes: List, + 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(null) } + var editingPaymentMode by remember { mutableStateOf(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, + 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, + 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 + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt new file mode 100644 index 0000000..1421622 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/label/LabelManagementViewModel.kt @@ -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>(emptyList()) + var paymentModes by mutableStateOf>(emptyList()) + + var dialogState by mutableStateOf(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) + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt index 5468ee8..5935ecc 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListComponents.kt @@ -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) + ) } } diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt index 0c0f1a0..3057b91 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListScreen.kt @@ -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,9 +57,6 @@ 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 @@ -104,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) @@ -214,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 @@ -330,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) @@ -364,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), @@ -387,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 + ) + } } } }, @@ -431,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)) } } }, diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt index b0eee62..7412547 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListUtils.kt @@ -65,11 +65,11 @@ object BillsListUtils { memberIdToName: Map ) { 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) } diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt index 148786e..2aeed56 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -119,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) } @@ -221,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 { @@ -269,7 +269,9 @@ class BillsListViewActivity : 5 -> onSettleProjectClick(pid) 6 -> onShareProjectClick(pid) 7 -> onExportProjectClick(pid) - 8 -> { /* Manage Labels - Placeholder for now */ } + 8 -> { + startActivity(net.helcel.cowspent.android.label.LabelManagementActivity.createIntent(this@BillsListViewActivity, pid)) + } } }, onAccountSwitcherClick = { @@ -391,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) { @@ -410,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 { @@ -473,7 +468,7 @@ class BillsListViewActivity : return@launch } - viewModel.showMemberManagementDialogByProjectId = projectId + startActivity(net.helcel.cowspent.android.project.member.MemberManagementActivity.createIntent(this@BillsListViewActivity, projectId)) } } @@ -500,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) } } @@ -601,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 } } @@ -699,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 { @@ -813,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 ) @@ -834,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 diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt index 178b13f..5f96043 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectOptionsDialog.kt @@ -11,6 +11,7 @@ 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 @@ -47,8 +48,10 @@ 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) ) @@ -59,34 +62,35 @@ fun ProjectOptionsDialogContent( val row2 = mutableListOf() val row3 = mutableListOf() - // Row 1: Edit, Share, Remove/Archive if (!isArchived && isMaintainer) { - row1.add(ProjectOption(stringResource(R.string.action_edit_project), Icons.Default.Edit, onEditProject)) + row1.add(ProjectOption(stringResource(R.string.action_edit), Icons.Default.Edit, onEditProject)) } if (isShareable && isParticipant) { - row1.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.fab_rm_project), Icons.Default.Delete, onRemoveProject)) + row1.add(ProjectOption(stringResource(R.string.action_delete), Icons.Default.Delete, onRemoveProject)) } // Row 2: Manage Member, Manage Labels, Manage Currencies if (!isArchived && isMaintainer) { - row2.add(ProjectOption(stringResource(R.string.fab_manage_members), Icons.Default.Group, onManageMembers)) - row2.add(ProjectOption(stringResource(R.string.fab_manage_labels), Icons.AutoMirrored.Filled.Label, onManageLabels)) - row2.add(ProjectOption(stringResource(R.string.fab_manage_currencies), Icons.Default.MonetizationOn, onManageCurrencies)) + 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.fab_statistics), Icons.Default.BarChart, onStatistics)) + row3.add(ProjectOption(stringResource(R.string.action_stats), Icons.Default.BarChart, onStatistics)) if (!isArchived && isParticipant) { - row3.add(ProjectOption(stringResource(R.string.fab_settle), Icons.Default.Handshake, onSettle)) + row3.add(ProjectOption(stringResource(R.string.action_settle), Icons.Default.Handshake, onSettle)) } - row3.add(ProjectOption(stringResource(R.string.fab_export_project), Icons.Default.Download, onExportProject)) + row3.add(ProjectOption(stringResource(R.string.action_export), Icons.Default.Download, onExportProject)) val allRows = listOf(row1, row2, row3).filter { it.isNotEmpty() } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt index ade833d..299bff8 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/ProjectShareDialog.kt @@ -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 ) diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt index d348563..0fc0bb2 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectActivity.kt @@ -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 } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt index f29dff2..cfe97d7 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/create/NewProjectScreen.kt @@ -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() } } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt index 4e39ac1..4afcf0c 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectActivity.kt @@ -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)) { diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt index 2b9ad31..f9e97f6 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectScreen.kt @@ -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) } + ) + } } } } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt index fac868d..8b7d26e 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/edit/EditProjectViewModel.kt @@ -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(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 } } diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt index 0cfd11d..b1b4a0b 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberAddDialog.kt @@ -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 diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt index 656bf80..642a841 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberEditDialog.kt @@ -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) diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt new file mode 100644 index 0000000..5dba687 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementActivity.kt @@ -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(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) + } + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt deleted file mode 100644 index aa476f4..0000000 --- a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementDialog.kt +++ /dev/null @@ -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, - 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 = {} - ) - } -} - diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementScreen.kt new file mode 100644 index 0000000..51aaf1c --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementScreen.kt @@ -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, + 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 = {} + ) + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementViewModel.kt b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementViewModel.kt new file mode 100644 index 0000000..e061930 --- /dev/null +++ b/app/src/main/java/net/helcel/cowspent/android/project/member/MemberManagementViewModel.kt @@ -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>(emptyList()) + + var dialogState by mutableStateOf(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) + } + } +} diff --git a/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt b/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt index bcb6f04..364df84 100644 --- a/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/project/settle/ProjectSettlementScreen.kt @@ -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 ) diff --git a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt index b84290b..71f0e33 100644 --- a/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/settings/SettingsScreen.kt @@ -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 ) } diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt index 351234f..976fccd 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSankeyDiagram.kt @@ -92,7 +92,7 @@ fun ProjectSankeyDiagram( customCategories: List, 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 } diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt index 1db969f..8c69aaf 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectSpendingGraph.kt @@ -43,10 +43,10 @@ fun ProjectSpendingGraph( allBills: List, 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 } diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt index 7e19785..4e90aed 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsScreen.kt @@ -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) diff --git a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt index b157c1e..ebb086f 100644 --- a/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt +++ b/app/src/main/java/net/helcel/cowspent/android/statistics/ProjectStatisticsTable.kt @@ -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>() @@ -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) } } diff --git a/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt b/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt index dabbdd0..e5485d7 100644 --- a/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt +++ b/app/src/main/java/net/helcel/cowspent/util/CospendClientUtil.kt @@ -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), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3f61dc1..81e9cf1 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,228 +2,148 @@ Cowspent - All bills - New bill - App settings - Scan QRCode from Cospend/Cowspent + + + New bill Add project - Save bill - Edit - Share - What? - Who paid? - All - None - - Bill edition - Bill values have been changed. What do you want to do? - Save - Discard - Currency manager - Comment - Convert current amount from another currency into %s - Main currency - For whom? - Project ID/name - - Repeat this bill every - Mode - Category - - Label missing categories - Label Bills - No more bills to label - Skip - Suggested - No suggestions for this bill - What do you want to do? - Where? - Local - Cospend - ihatemoney - ihatemoney address - Join - Create - Nextcloud address - Password (local) - Password - Project title - E-mail - Warning: creating a Cospend project using Nextcloud account works with Cospend v0.3.4 and higher. - Authenticated project creation - Nothing was changed - Project successfully added to Cowspent + Save + Edit + Share Search Delete - Error - Close - Loading - Project sync failed - 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. - The Nextcloud instance is probably in maintenance mode + Archive + Unarchive + Export + Stats + Settle + Scan QR Code + Settings + Label missing categories + Logout + Connect + Discard + Members + Labels + Currencies + + + Statistics + Edit project + Label Bills + Manage Labels + About + Settle Project + Share Project + Add Project + Nextcloud Account + Web link + Cowspent link + Are you sure? + + + All bills + Categories + Payment Modes + Name + Icon / Emoji + Color + Weight + Activated + Password + E-mail + Server address + Username + Comment + What? + Who paid? + For whom? + Repeat every + Mode + Category + Project ID/name + Project title + Use Nextcloud App Account + + + Unsaved changes + Save changes before leaving? + The remote project will not be deleted. + Sync error + Sync failed for %1$s.\n\n%2$s + Expenses are already balanced. + Project %1$s added + All bills labeled + No suggestions + Requires Cospend v0.3.4+. + Link copied to clipboard + Scan QR code or share the link to join. + Link for web browser access. + Share this link with a Cowspent user. + Settlement for %1$s: + %1$s owes %3$.2f to %2$s + Stats for %1$s: + Member (Paid | Spent | Balance) + Logged in as %1$s + + + Error + Loading + No projects found + No members found + No bills found + At least one member required + Server is in maintenance mode 400 Bad request 401 Unauthorized 403 Forbidden 404 Not Found - Creating remote project + Sync failed: %1$s + Invalid login: %1$s + Wrong username or password + Invalid server response + Request failed + Invalid e-mail + Invalid project ID + Invalid project title + Invalid bill name + Invalid bill date + Payer required + Owers required + No network connection + Server error + Server connection broken + Cannot share this project + + + Connect to Nextcloud account + Last sync: %1$02d:%2$02d Cancel Ok Yes No - Edit bill - New bill - Add project in Cowspent - Edit project - About - Impossible to share this project - Share - Create bills - Settlement bill - Share Cowspent link - Delete project on server - Save project - Add member - Remove - Project %1$s has been removed - Project - Members - Labels - Archive - Unarchive - Export - Last sync: %1$02d:%2$02d - Scan QR code - Choose from Nextcloud account - Import from CSV file - Malformed CSV, bad owers on line %1$d - Malformed CSV, bad date on line %1$d - Malformed CSV, bad column names on line %1$d - This member already exists in this project - Are you sure? - Are you sure you want to delete remote project? - The remote project will NOT be deleted - Activated - Name - Color - Weight - Delete member - Remote project operations require network connectivity - You are not allowed to perform this action + Close - Appearance - Network - Other - About Cowspent - Nextcloud account - Logged in as %1$s - Logout - Server address - WARNING: "http" is unsafe. Please use "https". - Username - Password - Connect - Theme - Offline mode + Appearance + Network + Other + Theme + Offline mode Only sync manually. - Custom color - UI Accent color - Choose color - Color Selection + Custom color + Color Selection + Show archived projects + WARNING: "http" is unsafe. Use "https". + Choose Color + System Nextcloud Manual + Light + Dark + Follow system - - Main currency saved - Project saved in %s - New member name - Edit member - Optimal - Who pays? - To whom? - How much? - Total Spent: %s - From - To - Who - Paid - Spent - Balance - Stats - Settle - Project %1$s was successfully added - Project %1$s was successfully created - Project added - Project created - Statistics - Settle the project - The expenses in this project are already balanced. There is nothing to settle! - Share the project - 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. - Share this web link to let others access the project with a web browser. - Web link - Cowspent link - Share this link to a Cowspent user. - Cowspent link of %1$s - Share Cowspent link of %1$s - Here is how to settle project %1$s : - %1$s owes %3$.2f to %2$s - Settle the project %1$s - Statistics of project %1$s : - MEMBER_NAME (PAID | SPENT | BALANCE) - Click here to connect to a Nextcloud account - At least one activated member is required to add a bill - Impossible for local projects - Choose a project from your Nextcloud account - There is no Cospend project in your Nextcloud account - Activity in project %1$s - - - No Projects - 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. - Configure Nextcloud account - Manually add a project (Cospend or local) - No Members - This project doesn\'t have any members. In order to create bills, you need to add at least one member (better two or more). - No Bills - This project doesn\'t have any bills. Click the button below to add your first bill. - - - - - - - Impossible to reach the remote project:\n\n%1$s - Project sync failed: %1$s - Invalid login: %1$s - is the Cospend app activated on the server? - server connection is broken - no network connection - server error - wrong username or password - Invalid e-mail address - Invalid project ID/name - Invalid project title - Invalid bill name - Invalid bill date - Bill should have a payer - Bill should have at least one ower - Error at remote project creation : %s - Error at remote project edition : %s - Please reauthenticate your SSO account in settings - Request failed - - - Version %1$s - Maintainer - Helcel.net - License - GNU GPL3x / CC BY-SA 4.0 - Source code - https://github.com/helcel-net/cowspent - - + nightMode appColor serverColor @@ -233,36 +153,28 @@ colorMode offlineMode showArchived - Light - Dark - Follow system 1 2 -1 - Use Nextcloud app account - - - QR code could not be scanned. - Currencies - Show archived projects - - + No repeat Daily Weekly Fortnightly Monthly Yearly - No payment mode - All payment modes + + None + All Credit card Cash Check - Online service + Online Transfer - No category - All categories + + None + All All except reimbursement Grocery Bar/Party @@ -276,4 +188,86 @@ Accommodation Transport Sport + + + What + Where + Local only + Cospend + IHateMoney + Join existing project + Create new project + Import from file + Choose project + No projects found on this account. + Project + Project added successfully. + You have no projects yet. + Configure Nextcloud account + Add project manually + No members in this project. + No bills found. + Member already exists. + Project: %1$s + Project %1$s removed. + File saved: %1$s + Import failed at row %d + Invalid date format at row %d + Invalid owers at row %d + Add Member + Edit Member + Delete + No changes to save. + + + None (Optimal) + Who pays + To whom + Amount + Share + Create bills + Settlement + + + Choose Currency + None + All + Currency settings saved. + Main Currency + + + Suggested Categories + Skip + From + To + Member + Paid + Spent + Balance + Total: %1$s + + + Connection failed: %1$s + Creation failed: %1$s + Error updating remote project: %1$s + Network unavailable for remote operation. + Failed to parse QR code. + Authentication token mismatch. Please log in again. + You don\'t have permission to perform this action. + Delete Label + Are you sure you want to delete this label? + + + Version %1$s + Maintainer + Helcel.net + License + GNU GPL3x / CC BY-SA 4.0 + Source code + https://github.com/helcel-net/cowspent + + + Project %1$s + Share %1$s +