feat(#29,#32,#33): home polish — Manage Categories to Settings, View All, empty-state illustration #36
@@ -0,0 +1,49 @@
|
|||||||
|
package dev.achmad.ledgerr.ui.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ReceiptLong
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EmptyStateIllustration(
|
||||||
|
message: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(32.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = rememberVectorPainter(image = Icons.AutoMirrored.Outlined.ReceiptLong),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(120.dp),
|
||||||
|
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurfaceVariant),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
text = message,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
|
|||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringExpenseWithCategory
|
||||||
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
import dev.achmad.ledgerr.domain.recurring.model.RecurringInterval
|
||||||
import dev.achmad.ledgerr.ui.components.AppBar
|
import dev.achmad.ledgerr.ui.components.AppBar
|
||||||
|
import dev.achmad.ledgerr.ui.components.EmptyStateIllustration
|
||||||
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
||||||
import dev.achmad.ledgerr.ui.components.ExportAction
|
import dev.achmad.ledgerr.ui.components.ExportAction
|
||||||
import dev.achmad.ledgerr.ui.components.MiniFab
|
import dev.achmad.ledgerr.ui.components.MiniFab
|
||||||
@@ -240,16 +241,9 @@ private fun ExpensesTabContent(
|
|||||||
onExpenseLongClick: (Long) -> Unit,
|
onExpenseLongClick: (Long) -> Unit,
|
||||||
) {
|
) {
|
||||||
if (expenses.isEmpty() && searchQuery.isBlank() && dateRangeFilter == DateRangeFilter.ALL) {
|
if (expenses.isEmpty() && searchQuery.isBlank() && dateRangeFilter == DateRangeFilter.ALL) {
|
||||||
Box(
|
EmptyStateIllustration(
|
||||||
modifier = Modifier.fillMaxSize(),
|
message = stringResource(R.string.expense_list_empty),
|
||||||
contentAlignment = Alignment.Center,
|
)
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.expense_list_empty),
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -21,17 +22,16 @@ import androidx.compose.material.icons.outlined.Close
|
|||||||
import androidx.compose.material.icons.outlined.Edit
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
import androidx.compose.material.icons.outlined.Settings
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
import androidx.compose.material.icons.outlined.UploadFile
|
import androidx.compose.material.icons.outlined.UploadFile
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedButton
|
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
@@ -68,12 +68,12 @@ import dev.achmad.ledgerr.domain.expense.model.ExpenseSummary
|
|||||||
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
|
import dev.achmad.ledgerr.domain.expense.model.ExpenseWithCategory
|
||||||
import dev.achmad.ledgerr.domain.preference.DateRangeOption
|
import dev.achmad.ledgerr.domain.preference.DateRangeOption
|
||||||
import dev.achmad.ledgerr.ui.components.AppBar
|
import dev.achmad.ledgerr.ui.components.AppBar
|
||||||
|
import dev.achmad.ledgerr.ui.components.EmptyStateIllustration
|
||||||
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
import dev.achmad.ledgerr.ui.components.ExpandedFab
|
||||||
import dev.achmad.ledgerr.ui.components.ExportAction
|
import dev.achmad.ledgerr.ui.components.ExportAction
|
||||||
import dev.achmad.ledgerr.ui.components.MiniFab
|
import dev.achmad.ledgerr.ui.components.MiniFab
|
||||||
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
|
import dev.achmad.ledgerr.ui.components.SingleSelectFilterChipGroup
|
||||||
import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen
|
import dev.achmad.ledgerr.ui.screens.add_edit_expense.AddEditExpenseScreen
|
||||||
import dev.achmad.ledgerr.ui.screens.category.CategoryScreen
|
|
||||||
import dev.achmad.ledgerr.ui.screens.expenses.ExpenseListScreen
|
import dev.achmad.ledgerr.ui.screens.expenses.ExpenseListScreen
|
||||||
import dev.achmad.ledgerr.ui.screens.import_bank_statement.ImportBankStatementScreen
|
import dev.achmad.ledgerr.ui.screens.import_bank_statement.ImportBankStatementScreen
|
||||||
import dev.achmad.ledgerr.ui.screens.settings.SettingsScreen
|
import dev.achmad.ledgerr.ui.screens.settings.SettingsScreen
|
||||||
@@ -158,7 +158,6 @@ object HomeScreen : Screen {
|
|||||||
onSelectedDateRangeChange = screenModel::setSelectedDateRange,
|
onSelectedDateRangeChange = screenModel::setSelectedDateRange,
|
||||||
recurringBannerCount = recurringBanner?.size,
|
recurringBannerCount = recurringBanner?.size,
|
||||||
onDismissBanner = screenModel::dismissRecurringBanner,
|
onDismissBanner = screenModel::dismissRecurringBanner,
|
||||||
onManageCategories = { navigator.push(CategoryScreen) },
|
|
||||||
onSeeAll = { navigator.push(ExpenseListScreen) },
|
onSeeAll = { navigator.push(ExpenseListScreen) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,7 +173,6 @@ private fun DashboardContent(
|
|||||||
onSelectedDateRangeChange: (DateRangeOption) -> Unit,
|
onSelectedDateRangeChange: (DateRangeOption) -> Unit,
|
||||||
recurringBannerCount: Int?,
|
recurringBannerCount: Int?,
|
||||||
onDismissBanner: () -> Unit,
|
onDismissBanner: () -> Unit,
|
||||||
onManageCategories: () -> Unit,
|
|
||||||
onSeeAll: () -> Unit,
|
onSeeAll: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val hasData = summary?.takeIf { it.byCategory.isNotEmpty() }
|
val hasData = summary?.takeIf { it.byCategory.isNotEmpty() }
|
||||||
@@ -215,26 +213,22 @@ private fun DashboardContent(
|
|||||||
CategoryChartCard(summary = data)
|
CategoryChartCard(summary = data)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
item(key = "actions") {
|
|
||||||
ActionsRow(
|
|
||||||
onManageCategories = onManageCategories,
|
|
||||||
onSeeAll = onSeeAll,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (recent.isNotEmpty()) {
|
if (recent.isNotEmpty()) {
|
||||||
item(key = "recent-header") {
|
item(key = "recent-header") {
|
||||||
SectionHeader(text = stringResource(R.string.home_recent))
|
SectionHeader(text = stringResource(R.string.home_recent)) {
|
||||||
|
TextButton(onClick = onSeeAll) {
|
||||||
|
Text(stringResource(R.string.home_view_all))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
items(items = recent, key = { it.expense.id }) { item ->
|
items(items = recent, key = { it.expense.id }) { item ->
|
||||||
ExpenseRow(item = item)
|
ExpenseRow(item = item)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
item(key = "empty") {
|
item(key = "empty") {
|
||||||
Text(
|
EmptyStateIllustration(
|
||||||
text = stringResource(R.string.home_dashboard_empty),
|
message = stringResource(R.string.home_dashboard_empty),
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
modifier = Modifier.fillParentMaxSize(),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,40 +388,26 @@ private fun CategoryLegend(items: List<Pair<Category, Double>>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ActionsRow(
|
private fun SectionHeader(
|
||||||
onManageCategories: () -> Unit,
|
text: String,
|
||||||
onSeeAll: () -> Unit,
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier
|
||||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
.fillMaxWidth()
|
||||||
|
.padding(top = 8.dp, bottom = 4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
OutlinedButton(
|
Text(
|
||||||
onClick = onManageCategories,
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
colors = ButtonDefaults.outlinedButtonColors(),
|
)
|
||||||
) {
|
actions()
|
||||||
Text(stringResource(R.string.home_manage_categories))
|
|
||||||
}
|
|
||||||
OutlinedButton(
|
|
||||||
onClick = onSeeAll,
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.home_see_all))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SectionHeader(text: String) {
|
|
||||||
Text(
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.titleSmall,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
modifier = Modifier.padding(top = 8.dp, bottom = 4.dp),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExpenseRow(item: ExpenseWithCategory) {
|
private fun ExpenseRow(item: ExpenseWithCategory) {
|
||||||
Surface(
|
Surface(
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.pluralStringResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
@@ -24,6 +27,7 @@ import dev.achmad.ledgerr.ui.components.preference.Preference
|
|||||||
import dev.achmad.ledgerr.ui.components.preference.PreferenceScreen
|
import dev.achmad.ledgerr.ui.components.preference.PreferenceScreen
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import dev.achmad.ledgerr.di.util.inject
|
import dev.achmad.ledgerr.di.util.inject
|
||||||
|
import dev.achmad.ledgerr.ui.screens.category.CategoryScreen
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
object SettingsScreen : Screen {
|
object SettingsScreen : Screen {
|
||||||
@@ -38,6 +42,7 @@ object SettingsScreen : Screen {
|
|||||||
val appPreference = inject<AppPreference>()
|
val appPreference = inject<AppPreference>()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
val categoriesCount by screenModel.categoriesCount.collectAsState()
|
||||||
|
|
||||||
val exportSuccess = stringResource(R.string.settings_export_success)
|
val exportSuccess = stringResource(R.string.settings_export_success)
|
||||||
val exportFailure = stringResource(R.string.settings_export_failure)
|
val exportFailure = stringResource(R.string.settings_export_failure)
|
||||||
@@ -77,6 +82,20 @@ object SettingsScreen : Screen {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Preference.PreferenceGroup(
|
||||||
|
title = stringResource(R.string.settings_categories),
|
||||||
|
preferenceItems = listOf(
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(R.string.settings_edit_categories),
|
||||||
|
subtitle = pluralStringResource(
|
||||||
|
R.plurals.settings_edit_categories_subtitle,
|
||||||
|
categoriesCount,
|
||||||
|
categoriesCount,
|
||||||
|
),
|
||||||
|
onClick = { navigator.push(CategoryScreen) },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Preference.PreferenceGroup(
|
Preference.PreferenceGroup(
|
||||||
title = stringResource(R.string.settings_data),
|
title = stringResource(R.string.settings_data),
|
||||||
preferenceItems = buildList {
|
preferenceItems = buildList {
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import android.net.Uri
|
|||||||
import cafe.adriel.voyager.core.model.ScreenModel
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import dev.achmad.ledgerr.di.util.inject
|
import dev.achmad.ledgerr.di.util.inject
|
||||||
|
import dev.achmad.ledgerr.domain.category.interactor.GetCategories
|
||||||
import dev.achmad.ledgerr.domain.category.interactor.SeedDefaultCategories
|
import dev.achmad.ledgerr.domain.category.interactor.SeedDefaultCategories
|
||||||
import dev.achmad.ledgerr.domain.data.interactor.ClearAllData
|
import dev.achmad.ledgerr.domain.data.interactor.ClearAllData
|
||||||
import dev.achmad.ledgerr.domain.expense.model.DateRange
|
import dev.achmad.ledgerr.domain.expense.model.DateRange
|
||||||
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
import dev.achmad.ledgerr.domain.export.interactor.ExportExpensesToCsv
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
@@ -16,8 +21,17 @@ class SettingsScreenModel(
|
|||||||
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
private val exportExpensesToCsv: ExportExpensesToCsv = inject(),
|
||||||
private val clearAllData: ClearAllData = inject(),
|
private val clearAllData: ClearAllData = inject(),
|
||||||
private val seedDefaultCategories: SeedDefaultCategories = inject(),
|
private val seedDefaultCategories: SeedDefaultCategories = inject(),
|
||||||
|
private val getCategories: GetCategories = inject(),
|
||||||
) : ScreenModel {
|
) : ScreenModel {
|
||||||
|
|
||||||
|
val categoriesCount: StateFlow<Int> = getCategories.subscribeAll()
|
||||||
|
.map { it.size }
|
||||||
|
.stateIn(
|
||||||
|
scope = screenModelScope,
|
||||||
|
started = SharingStarted.Eagerly,
|
||||||
|
initialValue = 0,
|
||||||
|
)
|
||||||
|
|
||||||
fun exportToCsv(range: DateRange, uri: Uri, onResult: (Result<Unit>) -> Unit) {
|
fun exportToCsv(range: DateRange, uri: Uri, onResult: (Result<Unit>) -> Unit) {
|
||||||
screenModelScope.launch {
|
screenModelScope.launch {
|
||||||
val result = withContext(Dispatchers.IO) {
|
val result = withContext(Dispatchers.IO) {
|
||||||
|
|||||||
@@ -52,6 +52,12 @@
|
|||||||
<string name="settings_theme_light">Light</string>
|
<string name="settings_theme_light">Light</string>
|
||||||
<string name="settings_theme_dark">Dark</string>
|
<string name="settings_theme_dark">Dark</string>
|
||||||
<string name="settings_theme_system">System</string>
|
<string name="settings_theme_system">System</string>
|
||||||
|
<string name="settings_categories">Categories</string>
|
||||||
|
<string name="settings_edit_categories">Edit Categories</string>
|
||||||
|
<plurals name="settings_edit_categories_subtitle">
|
||||||
|
<item quantity="one">%d category</item>
|
||||||
|
<item quantity="other">%d categories</item>
|
||||||
|
</plurals>
|
||||||
<string name="settings_data">Data</string>
|
<string name="settings_data">Data</string>
|
||||||
<string name="settings_export_csv">Export CSV</string>
|
<string name="settings_export_csv">Export CSV</string>
|
||||||
<string name="settings_export_csv_subtitle">Export expenses in a date range to a CSV file</string>
|
<string name="settings_export_csv_subtitle">Export expenses in a date range to a CSV file</string>
|
||||||
@@ -110,8 +116,7 @@
|
|||||||
<string name="home_total">Total %1$s</string>
|
<string name="home_total">Total %1$s</string>
|
||||||
<string name="home_period_this_week">this week</string>
|
<string name="home_period_this_week">this week</string>
|
||||||
<string name="home_period_this_month">this month</string>
|
<string name="home_period_this_month">this month</string>
|
||||||
<string name="home_manage_categories">Manage Categories</string>
|
<string name="home_view_all">View All</string>
|
||||||
<string name="home_see_all">See all</string>
|
|
||||||
<string name="home_recent">Recent</string>
|
<string name="home_recent">Recent</string>
|
||||||
<plurals name="home_recurring_banner">
|
<plurals name="home_recurring_banner">
|
||||||
<item quantity="one">%d new recurring expense added</item>
|
<item quantity="one">%d new recurring expense added</item>
|
||||||
|
|||||||
Reference in New Issue
Block a user