diff --git a/Android/EasyBudget/app/build.gradle.kts b/Android/EasyBudget/app/build.gradle.kts index 3297d05f..6ef9a1b1 100644 --- a/Android/EasyBudget/app/build.gradle.kts +++ b/Android/EasyBudget/app/build.gradle.kts @@ -38,8 +38,8 @@ android { compileSdk = 34 minSdk = 23 targetSdk = 34 - versionCode = 131 - versionName = "3.1.11" + versionCode = 133 + versionName = "3.2.0" vectorDrawables.useSupportLibrary = true } @@ -187,4 +187,6 @@ dependencies { implementation("net.sf.biweekly:biweekly:0.6.8") implementation("net.lingala.zip4j:zip4j:2.11.5") + + implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.9.3") } diff --git a/Android/EasyBudget/app/src/main/AndroidManifest.xml b/Android/EasyBudget/app/src/main/AndroidManifest.xml index 6c49d5b9..82de9950 100644 --- a/Android/EasyBudget/app/src/main/AndroidManifest.xml +++ b/Android/EasyBudget/app/src/main/AndroidManifest.xml @@ -71,7 +71,7 @@ android:configChanges="locale|keyboardHidden|orientation|screenSize" android:launchMode="singleTask" android:screenOrientation="portrait" - tools:ignore="LockedOrientationActivity" + tools:ignore="DiscouragedApi,LockedOrientationActivity" android:exported="true"> @@ -96,34 +96,34 @@ android:configChanges="locale|keyboardHidden|orientation|screenSize" android:label="@string/title_activity_monthly_report" android:screenOrientation="portrait" - tools:ignore="LockedOrientationActivity" + tools:ignore="DiscouragedApi,LockedOrientationActivity" android:exported="false"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity"/> + tools:ignore="DiscouragedApi,LockedOrientationActivity" /> + + + + + + = 33 && ActivityCompat.checkSelfPermission(this@EasyBudget, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { - return - } + if (previousVersion < 132) { + GlobalScope.launch { + try { + if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this@EasyBudget, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { + return@launch + } - val intent = Intent(this, MainActivity::class.java).apply { - flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - putExtra(MainActivity.INTENT_OPEN_ACCOUNTS_TRAY_EXTRA, true) - } - val pendingIntent: PendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE) - - val builder = NotificationCompat.Builder(this, CHANNEL_NEW_FEATURES) - .setSmallIcon(R.drawable.ic_push) - .setContentTitle(getString(R.string.update_three_dot_zero_notification_title)) - .setContentText(getString(R.string.update_three_dot_zero_notification_message)) - .setStyle(NotificationCompat.BigTextStyle().bigText(getString(R.string.update_three_dot_zero_notification_message))) - .setPriority(NotificationCompat.PRIORITY_DEFAULT) - .setAutoCancel(true) - .setContentIntent(pendingIntent) - .addAction(R.drawable.ic_baseline_new_releases, getString(R.string.update_three_dot_zero_notification_cta), pendingIntent) - - with(NotificationManagerCompat.from(this)) { - // notificationId is a unique int for each notification that you must define - notify(100001, builder.build()) + if (!iab.isUserPro()) { + return@launch + } + + val intent = Intent(this@EasyBudget, MainActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + } + val pendingIntent: PendingIntent = PendingIntent.getActivity(this@EasyBudget, 0, intent, PendingIntent.FLAG_IMMUTABLE) + + val builder = NotificationCompat.Builder(this@EasyBudget, CHANNEL_NEW_FEATURES) + .setSmallIcon(R.drawable.ic_push) + .setContentTitle(getString(R.string.update_three_dot_two_notification_title)) + .setContentText(getString(R.string.update_three_dot_two_notification_message)) + .setStyle(NotificationCompat.BigTextStyle().bigText(getString(R.string.update_three_dot_two_notification_message))) + .setPriority(NotificationCompat.PRIORITY_DEFAULT) + .setAutoCancel(true) + .setContentIntent(pendingIntent) + .addAction(R.drawable.ic_baseline_new_releases, getString(R.string.update_three_dot_two_notification_cta), pendingIntent) + + with(NotificationManagerCompat.from(this@EasyBudget)) { + // notificationId is a unique int for each notification that you must define + notify(100001, builder.build()) + } + } catch (e: Exception) { + Logger.error("Error while showing update notification", e) } - } catch (e: Exception) { - Logger.error("Error while showing update notification", e) } } } diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/accounts/FirebaseAccounts.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/accounts/FirebaseAccounts.kt index da436974..d662548e 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/accounts/FirebaseAccounts.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/accounts/FirebaseAccounts.kt @@ -28,7 +28,7 @@ import com.google.firebase.firestore.FieldValue import com.google.firebase.firestore.FirebaseFirestore import com.google.firebase.firestore.Query import com.google.firebase.firestore.QuerySnapshot -import com.google.firebase.firestore.ktx.firestoreSettings +import com.google.firebase.firestore.firestoreSettings import com.google.firebase.firestore.ktx.memoryCacheSettings import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.channels.trySendBlocking diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/db/onlineimpl/OnlineDBImpl.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/db/onlineimpl/OnlineDBImpl.kt index 47535dcb..e8544738 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/db/onlineimpl/OnlineDBImpl.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/db/onlineimpl/OnlineDBImpl.kt @@ -34,6 +34,7 @@ import io.realm.kotlin.ext.query import io.realm.kotlin.log.LogLevel import io.realm.kotlin.log.RealmLogger import io.realm.kotlin.mongodb.App +import io.realm.kotlin.mongodb.AppConfiguration import io.realm.kotlin.mongodb.Credentials import io.realm.kotlin.mongodb.subscriptions import io.realm.kotlin.mongodb.sync.SyncConfiguration @@ -81,7 +82,6 @@ class OnlineDBImpl( } private val onChangeMutableFlow = MutableSharedFlow() - override val onChangeFlow: Flow = onChangeMutableFlow override fun ensureDBCreated() { /* No-op */ } @@ -526,6 +526,7 @@ class OnlineDBImpl( realm.close() app.close() cancel() + recurringExpensesLoadingStateMutableFlow.value = RecurringExpenseLoadingState.NotLoaded } private fun generateQueryForDateRange(from: LocalDate, to: LocalDate): String @@ -557,7 +558,11 @@ class OnlineDBImpl( accountId: String, accountSecret: String, ): OnlineDBImpl { - val app = App.create(atlasAppId) + val app = App.create( + AppConfiguration.Builder(atlasAppId) + .enableSessionMultiplexing(true) + .build() + ) val user = withContext(Dispatchers.IO) { try { diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/DateHelper.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/DateHelper.kt index b89f6024..24f268ef 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/DateHelper.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/DateHelper.kt @@ -18,25 +18,32 @@ package com.benoitletondor.easybudgetapp.helper import com.benoitletondor.easybudgetapp.parameters.Parameters import com.benoitletondor.easybudgetapp.parameters.getInitDate +import com.kizitonwose.calendar.core.yearMonth import java.time.LocalDate +import java.time.YearMonth import java.util.ArrayList /** - * Get the list of months available for the user for the monthly report view. - * - * @return a list of Date object set at the 1st day of the month 00:00:00:000 + * Get the list of months available in the monthly report view. */ -fun Parameters.getListOfMonthsAvailableForUser(): List { - val initDate = getInitDate() ?: return emptyList() - val today = LocalDate.now() +fun Parameters.getListOfMonthsAvailableForUser(): List { + val initMonth = getInitDate()?.yearMonth ?: YearMonth.now() + + // End 12 months in the future (13 because we are comparing with "isBefore") + val endRange = LocalDate.now().yearMonth.plusMonths(13) - val months = ArrayList() - var currentDate = LocalDate.of(initDate.year, initDate.month, 1) + // Start at least 12 months ago + var currentMonth = if (initMonth.isAfter(YearMonth.now().minusMonths(12))) { + YearMonth.now().minusMonths(12) + } else { + initMonth + } - while (currentDate.isBefore(today) || currentDate == today) { - months.add(currentDate) - currentDate = currentDate.plusMonths(1) + val months = ArrayList() + while (currentMonth.isBefore(endRange)) { + months.add(currentMonth) + currentMonth = currentMonth.plusMonths(1) } return months diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/RecurrenceHelper.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/RecurrenceHelper.kt index 22d20da5..1fa72cc0 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/RecurrenceHelper.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/RecurrenceHelper.kt @@ -16,8 +16,10 @@ package com.benoitletondor.easybudgetapp.helper +import android.content.Context import biweekly.util.Frequency import biweekly.util.Recurrence +import com.benoitletondor.easybudgetapp.R import com.benoitletondor.easybudgetapp.model.RecurringExpenseType fun Recurrence.toRecurringExpenseType(): RecurringExpenseType { @@ -62,4 +64,17 @@ fun RecurringExpenseType.toRecurrence(): Recurrence { } return Recurrence.Builder(frequency).interval(interval).build() +} + +fun RecurringExpenseType.toFormattedString(context: Context): String = when(this) { + RecurringExpenseType.DAILY -> context.getString(R.string.daily) + RecurringExpenseType.WEEKLY -> context.getString(R.string.weekly) + RecurringExpenseType.BI_WEEKLY -> context.getString(R.string.bi_weekly) + RecurringExpenseType.TER_WEEKLY -> context.getString(R.string.ter_weekly) + RecurringExpenseType.FOUR_WEEKLY -> context.getString(R.string.four_weekly) + RecurringExpenseType.MONTHLY -> context.getString(R.string.monthly) + RecurringExpenseType.BI_MONTHLY -> context.getString(R.string.bi_monthly) + RecurringExpenseType.TER_MONTHLY -> context.getString(R.string.ter_monthly) + RecurringExpenseType.SIX_MONTHLY ->context.getString(R.string.six_monthly) + RecurringExpenseType.YEARLY -> context.getString(R.string.yearly) } \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/UIHelper.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/UIHelper.kt index a994589c..8b518e83 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/UIHelper.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/helper/UIHelper.kt @@ -36,6 +36,7 @@ import androidx.core.view.ViewCompat import androidx.core.view.updateLayoutParams import com.benoitletondor.easybudgetapp.R import java.time.LocalDate +import java.time.YearMonth import java.time.format.DateTimeFormatter import java.util.Locale @@ -178,7 +179,7 @@ fun AlertDialog.centerButtons() { * @param context non null context * @return a formatted string like "January 2016" */ -fun LocalDate.getMonthTitle(context: Context): String { +fun YearMonth.getMonthTitle(context: Context): String { val format = DateTimeFormatter.ofPattern(context.resources.getString(R.string.monthly_report_month_title_format), Locale.getDefault()) return format.format(this) } diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/main/account/ExpensesRecyclerViewAdapter.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/main/account/ExpensesRecyclerViewAdapter.kt index 14e6b1d7..b011a9ef 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/main/account/ExpensesRecyclerViewAdapter.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/main/account/ExpensesRecyclerViewAdapter.kt @@ -29,9 +29,9 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager import androidx.recyclerview.widget.RecyclerView import com.benoitletondor.easybudgetapp.R import com.benoitletondor.easybudgetapp.helper.CurrencyHelper +import com.benoitletondor.easybudgetapp.helper.toFormattedString import com.benoitletondor.easybudgetapp.model.Expense import com.benoitletondor.easybudgetapp.model.RecurringExpenseDeleteType -import com.benoitletondor.easybudgetapp.model.RecurringExpenseType import com.benoitletondor.easybudgetapp.parameters.Parameters import com.benoitletondor.easybudgetapp.view.expenseedit.ExpenseEditActivity import com.benoitletondor.easybudgetapp.view.main.MainActivity @@ -97,18 +97,7 @@ class ExpensesRecyclerViewAdapter( viewHolder.checkedCheckBox.isChecked = expense.checked if (expense.isRecurring()) { - when (expense.associatedRecurringExpense!!.recurringExpense.type) { - RecurringExpenseType.DAILY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.daily) - RecurringExpenseType.WEEKLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.weekly) - RecurringExpenseType.BI_WEEKLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.bi_weekly) - RecurringExpenseType.TER_WEEKLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.ter_weekly) - RecurringExpenseType.FOUR_WEEKLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.four_weekly) - RecurringExpenseType.MONTHLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.monthly) - RecurringExpenseType.BI_MONTHLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.bi_monthly) - RecurringExpenseType.TER_MONTHLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.ter_monthly) - RecurringExpenseType.SIX_MONTHLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.six_monthly) - RecurringExpenseType.YEARLY -> viewHolder.recurringIndicatorTextview.text = viewHolder.view.context.getString(R.string.yearly) - } + viewHolder.recurringIndicatorTextview.text = expense.associatedRecurringExpense!!.recurringExpense.type.toFormattedString(viewHolder.view.context) } val onClickListener = View.OnClickListener { diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/premium/view/SubscribeView.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/premium/view/SubscribeView.kt index 4a1f9d8d..ff014c39 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/premium/view/SubscribeView.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/premium/view/SubscribeView.kt @@ -455,31 +455,19 @@ private fun ColumnScope.ProSubscriptionView(proSubscribed: Boolean) { modifier = Modifier.align(Alignment.CenterHorizontally), ) - Spacer(modifier = Modifier.height(30.dp)) - - Text( - modifier = Modifier - .padding(bottom = 10.dp) - .fillMaxWidth(), - text = stringResource(R.string.premium_settings_coming_soon), - color = Color.White, - fontSize = 20.sp, - textAlign = TextAlign.Center, - ) + Spacer(modifier = Modifier.height(20.dp)) Text( modifier = Modifier.padding(bottom = 10.dp), text = stringResource(R.string.premium_popup_not_pro_feature3_title), - color = Color.White.copy(alpha = 0.9f), + color = Color.White, fontSize = 20.sp, - fontStyle = FontStyle.Italic, ) Text( text = stringResource(R.string.premium_popup_not_pro_feature3_message), - color = Color.White.copy(alpha = 0.9f), + color = Color.White, fontSize = 16.sp, - fontStyle = FontStyle.Italic, ) } diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportFragment.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportFragment.kt index 350fe63b..3836ed95 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportFragment.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportFragment.kt @@ -35,10 +35,10 @@ import com.benoitletondor.easybudgetapp.helper.viewLifecycleScope import com.benoitletondor.easybudgetapp.parameters.Parameters import com.google.android.material.dialog.MaterialAlertDialogBuilder import dagger.hilt.android.AndroidEntryPoint -import java.time.LocalDate +import java.time.YearMonth import javax.inject.Inject -private const val ARG_FIRST_DAY_OF_MONTH_DATE = "arg_date" +private const val ARG_MONTH = "arg_month" /** * Fragment that displays monthly report for a given month @@ -50,7 +50,7 @@ class MonthlyReportFragment : Fragment() { /** * The first day of the month */ - private lateinit var firstDayOfMonth: LocalDate + private lateinit var month: YearMonth private val viewModel: MonthlyReportViewModel by viewModels() @Inject lateinit var parameters: Parameters @@ -73,7 +73,7 @@ class MonthlyReportFragment : Fragment() { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - firstDayOfMonth = requireArguments().getSerializable(ARG_FIRST_DAY_OF_MONTH_DATE) as LocalDate + month = requireArguments().getSerializable(ARG_MONTH) as YearMonth // Inflate the layout for this fragment val v = inflater.inflate(R.layout.fragment_monthly_report, container, false) @@ -120,7 +120,7 @@ class MonthlyReportFragment : Fragment() { } } - viewModel.loadDataForMonth(firstDayOfMonth) + viewModel.loadDataForMonth(month) return v } @@ -134,9 +134,9 @@ class MonthlyReportFragment : Fragment() { } companion object { - fun newInstance(firstDayOfMonth: LocalDate): MonthlyReportFragment = MonthlyReportFragment().apply { + fun newInstance(month: YearMonth): MonthlyReportFragment = MonthlyReportFragment().apply { arguments = Bundle().apply { - putSerializable(ARG_FIRST_DAY_OF_MONTH_DATE, firstDayOfMonth) + putSerializable(ARG_MONTH, month) } } } diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportViewModel.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportViewModel.kt index 52b0b728..0ed9c236 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportViewModel.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/MonthlyReportViewModel.kt @@ -22,14 +22,13 @@ import com.benoitletondor.easybudgetapp.model.Expense import com.benoitletondor.easybudgetapp.db.DB import com.benoitletondor.easybudgetapp.helper.MutableLiveFlow import com.benoitletondor.easybudgetapp.view.main.account.AccountViewModel -import com.kizitonwose.calendar.core.yearMonth import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.time.LocalDate +import java.time.YearMonth import javax.inject.Inject @HiltViewModel @@ -53,14 +52,14 @@ class MonthlyReportViewModel @Inject constructor() : ViewModel() { } } - fun loadDataForMonth(month: LocalDate) { + fun loadDataForMonth(month: YearMonth) { if (!::db.isInitialized) { return } viewModelScope.launch { val expensesForMonth = withContext(Dispatchers.Default) { - db.getExpensesForMonth(month.yearMonth) + db.getExpensesForMonth(month) } if( expensesForMonth.isEmpty() ) { stateMutableFlow.emit(MonthlyReportState.Empty) diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseActivity.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseActivity.kt index 8aa968a9..196c1ee8 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseActivity.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseActivity.kt @@ -17,6 +17,8 @@ package com.benoitletondor.easybudgetapp.view.report.base import android.os.Bundle +import android.view.Menu +import android.view.MenuInflater import android.view.MenuItem import android.view.View import androidx.activity.viewModels @@ -24,14 +26,16 @@ import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentPagerAdapter import androidx.lifecycle.lifecycleScope import androidx.viewpager.widget.ViewPager +import com.benoitletondor.easybudgetapp.R import com.benoitletondor.easybudgetapp.databinding.ActivityMonthlyReportBinding import com.benoitletondor.easybudgetapp.helper.BaseActivity import com.benoitletondor.easybudgetapp.helper.getMonthTitle import com.benoitletondor.easybudgetapp.helper.launchCollect import com.benoitletondor.easybudgetapp.helper.removeButtonBorder import com.benoitletondor.easybudgetapp.view.report.MonthlyReportFragment +import com.benoitletondor.easybudgetapp.view.report.export.ExportReportActivity import dagger.hilt.android.AndroidEntryPoint -import java.time.LocalDate +import java.time.YearMonth /** * Activity that displays monthly report @@ -40,6 +44,7 @@ import java.time.LocalDate */ @AndroidEntryPoint class MonthlyReportBaseActivity : BaseActivity(), ViewPager.OnPageChangeListener { + private val viewModel: MonthlyReportBaseViewModel by viewModels() private var ignoreNextPageSelectedEvent: Boolean = false @@ -67,16 +72,16 @@ class MonthlyReportBaseActivity : BaseActivity(), binding.monthlyReportPreviousMonthButton.removeButtonBorder() binding.monthlyReportNextMonthButton.removeButtonBorder() - var loadedDates: List = emptyList() + var loadedMonths: List = emptyList() lifecycleScope.launchCollect(viewModel.stateFlow) { state -> when(state) { is MonthlyReportBaseViewModel.State.Loaded -> { binding.monthlyReportProgressBar.visibility = View.GONE binding.monthlyReportContent.visibility = View.VISIBLE - if (state.dates != loadedDates) { - loadedDates = state.dates - configureViewPager(state.dates) + if (state.months != loadedMonths) { + loadedMonths = state.months + configureViewPager(state.months) } if( !ignoreNextPageSelectedEvent ) { @@ -85,7 +90,7 @@ class MonthlyReportBaseActivity : BaseActivity(), ignoreNextPageSelectedEvent = false - binding.monthlyReportMonthTitleTv.text = state.selectedPosition.date.getMonthTitle(this) + binding.monthlyReportMonthTitleTv.text = state.selectedPosition.month.getMonthTitle(this) // Last and first available month val isFirstMonth = state.selectedPosition.position == 0 @@ -99,6 +104,31 @@ class MonthlyReportBaseActivity : BaseActivity(), } } } + + lifecycleScope.launchCollect(viewModel.eventFlow) { event -> + when(event) { + is MonthlyReportBaseViewModel.Event.OpenExport -> startActivity(ExportReportActivity.createIntent(this, event.month)) + MonthlyReportBaseViewModel.Event.RefreshMenu -> invalidateOptionsMenu() + } + } + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + val inflater: MenuInflater = menuInflater + inflater.inflate(R.menu.menu_monthly_report, menu) + return true + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + if (menu == null) { + return false + } + + if (!viewModel.shouldShowExportButton()) { + menu.removeItem(R.id.action_export) + } + + return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -107,6 +137,9 @@ class MonthlyReportBaseActivity : BaseActivity(), if (id == android.R.id.home) { finish() return true + } else if (id == R.id.action_export) { + viewModel.onExportButtonClicked() + return true } return super.onOptionsItemSelected(item) @@ -115,7 +148,7 @@ class MonthlyReportBaseActivity : BaseActivity(), /** * Configure the [.pager] adapter and listener. */ - private fun configureViewPager(dates: List) { + private fun configureViewPager(dates: List) { binding.monthlyReportViewPager.removeOnPageChangeListener(this) binding.monthlyReportViewPager.offscreenPageLimit = 0 diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseViewModel.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseViewModel.kt index 87d9039d..a2082c11 100644 --- a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseViewModel.kt +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/base/MonthlyReportBaseViewModel.kt @@ -19,25 +19,39 @@ package com.benoitletondor.easybudgetapp.view.report.base import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.benoitletondor.easybudgetapp.helper.Logger +import com.benoitletondor.easybudgetapp.helper.MutableLiveFlow import com.benoitletondor.easybudgetapp.helper.getListOfMonthsAvailableForUser +import com.benoitletondor.easybudgetapp.helper.launchCollect +import com.benoitletondor.easybudgetapp.iab.Iab +import com.benoitletondor.easybudgetapp.iab.PremiumCheckStatus import com.benoitletondor.easybudgetapp.parameters.Parameters import com.benoitletondor.easybudgetapp.view.report.base.MonthlyReportBaseActivity.Companion.FROM_NOTIFICATION_EXTRA import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.time.LocalDate +import java.time.YearMonth import javax.inject.Inject @HiltViewModel class MonthlyReportBaseViewModel @Inject constructor( private val parameters: Parameters, + private val iab: Iab, savedStateHandle: SavedStateHandle, ) : ViewModel() { private val fromNotification = savedStateHandle.get(FROM_NOTIFICATION_EXTRA) ?: false + private var isUserPro = false + + private val eventMutableFlow = MutableLiveFlow() + val eventFlow: Flow = eventMutableFlow + private val stateMutableFlow = MutableStateFlow(State.Loading) val stateFlow: Flow = stateMutableFlow @@ -45,27 +59,45 @@ class MonthlyReportBaseViewModel @Inject constructor( viewModelScope.launch { stateMutableFlow.value = State.Loading - val dates = withContext(Dispatchers.IO) { + val months = withContext(Dispatchers.IO) { return@withContext parameters.getListOfMonthsAvailableForUser() } - val selectedPosition = if( !fromNotification || dates.size == 1) { - MonthlyReportSelectedPosition(dates.size - 1, dates[dates.size - 1], true) + var currentMonthPosition = months.indexOf(YearMonth.now()) + if (currentMonthPosition == -1) { + Logger.error("Error while getting current month position, returned -1", IllegalStateException("Current month not found in list of available months")) + currentMonthPosition = months.size - 1 + } + + val selectedPosition = if( !fromNotification || months.size == 1) { + MonthlyReportSelectedPosition(currentMonthPosition, months[currentMonthPosition], currentMonthPosition == months.size - 1) } else { - MonthlyReportSelectedPosition(dates.size - 2, dates[dates.size - 2], false) + MonthlyReportSelectedPosition(currentMonthPosition - 1, months[currentMonthPosition - 1], false) } - stateMutableFlow.value = State.Loaded(dates, selectedPosition) + stateMutableFlow.value = State.Loaded(months, selectedPosition) + } + + viewModelScope.launch { + iab.iabStatusFlow + .map { it == PremiumCheckStatus.PRO_SUBSCRIBED } + .distinctUntilChanged() + .collect { isPro -> + isUserPro = isPro + eventMutableFlow.emit(Event.RefreshMenu) + } } } + fun shouldShowExportButton(): Boolean = isUserPro + fun onPreviousMonthButtonClicked() { val loadedState = stateMutableFlow.value as? State.Loaded ?: return val position = loadedState.selectedPosition.position if (position > 0) { stateMutableFlow.value = loadedState.copy( - selectedPosition = MonthlyReportSelectedPosition(position - 1, loadedState.dates[position - 1], false) + selectedPosition = MonthlyReportSelectedPosition(position - 1, loadedState.months[position - 1], false) ) } } @@ -74,9 +106,9 @@ class MonthlyReportBaseViewModel @Inject constructor( val loadedState = stateMutableFlow.value as? State.Loaded ?: return val position = loadedState.selectedPosition.position - if ( position < loadedState.dates.size - 1 ) { + if ( position < loadedState.months.size - 1 ) { stateMutableFlow.value = loadedState.copy( - selectedPosition = MonthlyReportSelectedPosition(position + 1, loadedState.dates[position + 1], loadedState.dates.size == position + 2) + selectedPosition = MonthlyReportSelectedPosition(position + 1, loadedState.months[position + 1], loadedState.months.size == position + 2) ) } } @@ -85,14 +117,28 @@ class MonthlyReportBaseViewModel @Inject constructor( val loadedState = stateMutableFlow.value as? State.Loaded ?: return stateMutableFlow.value = loadedState.copy( - selectedPosition = MonthlyReportSelectedPosition(position, loadedState.dates[position], loadedState.dates.size == position + 1) + selectedPosition = MonthlyReportSelectedPosition(position, loadedState.months[position], loadedState.months.size == position + 1) ) } + fun onExportButtonClicked() { + val loadedState = stateMutableFlow.value as? State.Loaded ?: return + + val selectedMonth = loadedState.selectedPosition.month + viewModelScope.launch { + eventMutableFlow.emit(Event.OpenExport(selectedMonth)) + } + } + sealed class State { data object Loading : State() - data class Loaded(val dates: List, val selectedPosition: MonthlyReportSelectedPosition) : State() + data class Loaded(val months: List, val selectedPosition: MonthlyReportSelectedPosition) : State() + } + + sealed class Event { + data object RefreshMenu : Event() + data class OpenExport(val month: YearMonth) : Event() } } -data class MonthlyReportSelectedPosition(val position: Int, val date: LocalDate, val latest: Boolean) \ No newline at end of file +data class MonthlyReportSelectedPosition(val position: Int, val month: YearMonth, val latest: Boolean) \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/export/ExportReportActivity.kt b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/export/ExportReportActivity.kt new file mode 100644 index 00000000..04818afe --- /dev/null +++ b/Android/EasyBudget/app/src/main/java/com/benoitletondor/easybudgetapp/view/report/export/ExportReportActivity.kt @@ -0,0 +1,347 @@ +/* + * Copyright 2024 Benoit LETONDOR + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.benoitletondor.easybudgetapp.view.report.export + +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.MenuItem +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +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.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.content.FileProvider +import com.benoitletondor.easybudgetapp.BuildConfig +import com.benoitletondor.easybudgetapp.R +import com.benoitletondor.easybudgetapp.databinding.ActivityMonthlyReportExportBinding +import com.benoitletondor.easybudgetapp.db.DB +import com.benoitletondor.easybudgetapp.helper.BaseActivity +import com.benoitletondor.easybudgetapp.helper.CurrencyHelper +import com.benoitletondor.easybudgetapp.helper.Logger +import com.benoitletondor.easybudgetapp.helper.toFormattedString +import com.benoitletondor.easybudgetapp.parameters.Parameters +import com.benoitletondor.easybudgetapp.theme.AppTheme +import com.benoitletondor.easybudgetapp.view.main.account.AccountViewModel +import com.benoitletondor.easybudgetapp.view.report.export.ExportReportActivity.Companion.REQUEST_CODE_SHARE_CSV +import com.benoitletondor.easybudgetapp.view.report.export.ExportReportActivity.Companion.tempFileName +import com.github.doyaaaaaken.kotlincsv.dsl.csvWriter +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.File +import java.lang.IllegalArgumentException +import java.time.YearMonth +import java.time.format.DateTimeFormatter +import javax.inject.Inject + +@AndroidEntryPoint +class ExportReportActivity: BaseActivity() { + private lateinit var month: YearMonth + @Inject + lateinit var parameters: Parameters + + override fun createBinding(): ActivityMonthlyReportExportBinding = ActivityMonthlyReportExportBinding.inflate(layoutInflater) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + month = intent.getSerializableExtra(EXTRA_MONTH) as? YearMonth ?: throw IllegalArgumentException("Missing month extra") + val currentDb = AccountViewModel.getCurrentDB() + if (currentDb == null) { + Logger.error("Unable to get current DB in ExportReportActivity", Exception("No DB found")) + finish() + return + } + + setSupportActionBar(binding.toolbar) + supportActionBar?.setDisplayShowHomeEnabled(true) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + + binding.exportComposeView.setContent { + AppTheme { + ExportReportScreen( + db = currentDb, + parameters = parameters, + month = month, + ) + } + } + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + val id = item.itemId + + if (id == android.R.id.home) { + finish() + return true + } + + return super.onOptionsItemSelected(item) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + + if (requestCode == REQUEST_CODE_SHARE_CSV && resultCode == Activity.RESULT_OK) { + finish() + } + } + + override fun onDestroy() { + val tempFile = File(cacheDir, tempFileName(month)) + if (tempFile.exists()) { + Logger.debug("Deleting temporary file: ${tempFile.path}") + tempFile.delete() + } + + super.onDestroy() + } + + companion object { + private const val EXTRA_MONTH = "extra_month" + const val REQUEST_CODE_SHARE_CSV = 488230 + + fun createIntent(context: Context, month: YearMonth): Intent = Intent(context, ExportReportActivity::class.java).apply { + putExtra(EXTRA_MONTH, month) + } + + fun tempFileName(month: YearMonth): String = "export_${month.year}_${month.monthValue}.csv" + } +} + +private sealed class State { + data object Loading : State() + data class Loaded(val csvFile: File) : State() + data class Error(val exception: Exception) : State() +} + +@Composable +private fun ExportReportScreen( + db: DB, + parameters: Parameters, + month: YearMonth, +) { + var retryLoadingState by remember { mutableIntStateOf(0) } + var state by remember { mutableStateOf(State.Loading) } + + val context = LocalContext.current + val activity = context as Activity + + LaunchedEffect(month, retryLoadingState) { + state = State.Loading + state = try { + val csvFile = withContext(Dispatchers.IO) { + val expenses = db.getExpensesForMonth(month) + val file = File(context.cacheDir, tempFileName(month)) + val dateFormatter = DateTimeFormatter.ISO_DATE + + Logger.debug("Creating temporary file: ${file.path}") + csvWriter().openAsync(file) { + writeRow(listOf( + context.getString(R.string.monthly_report_data_date_row), + context.getString(R.string.monthly_report_data_title_row), + context.getString(R.string.monthly_report_data_amount_row), + context.getString(R.string.monthly_report_data_recurring_row), + context.getString(R.string.monthly_report_data_checked_row), + )) + for(expense in expenses) { + writeRow(listOf( + dateFormatter.format(expense.date), + expense.title, + CurrencyHelper.getFormattedCurrencyString(parameters, -expense.amount), + expense.associatedRecurringExpense?.recurringExpense?.type?.toFormattedString(context) ?: "", + if (expense.checked) "X" else "", + )) + } + } + + return@withContext file + } + + State.Loaded(csvFile) + } catch (e: Exception) { + if (e is CancellationException) throw e + + State.Error(e) + } + } + + when(val currentState = state) { + is State.Error -> ErrorView( + exception = currentState.exception, + onRetryButtonClicked = { retryLoadingState++ }, + ) + is State.Loaded -> LoadedView( + onDownloadButtonClicked = { + try { + val uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", currentState.csvFile) + + val shareIntent = Intent().apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) // Allow external app to open the file + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/csv" + } + + activity.startActivityForResult(Intent.createChooser(shareIntent, null), REQUEST_CODE_SHARE_CSV) + } catch (e: Exception) { + Logger.error("Error while opening CSV file", e) + MaterialAlertDialogBuilder(context) + .setTitle(R.string.monthly_report_data_open_error_title) + .setMessage(R.string.monthly_report_data_open_error_description) + .setPositiveButton(R.string.ok, null) + .show() + } + } + ) + State.Loading -> LoadingView() + } +} + +@Composable +private fun LoadingView() { + Box( + modifier = Modifier.fillMaxSize(), + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center), + ) + } +} + +@Composable +private fun ErrorView( + exception: Exception, + onRetryButtonClicked: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.monthly_report_data_loading_error_title), + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.monthly_report_data_loading_error_description, exception.localizedMessage ?: "No error message"), + fontSize = 16.sp, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Button( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = onRetryButtonClicked, + ) { + Text(stringResource(R.string.monthly_report_data_loading_error_cta)) + } + } +} + +@Composable +private fun LoadedView( + onDownloadButtonClicked: () -> Unit, +) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp, vertical = 16.dp), + verticalArrangement = Arrangement.Center, + ) { + Text( + modifier = Modifier.fillMaxWidth(), + text = stringResource(id = R.string.monthly_report_export_data_loaded_title), + textAlign = TextAlign.Center, + fontWeight = FontWeight.SemiBold, + fontSize = 16.sp, + ) + + Spacer(modifier = Modifier.height(10.dp)) + + Button( + modifier = Modifier.align(Alignment.CenterHorizontally), + onClick = onDownloadButtonClicked, + ) { + Text( + text = stringResource(id = R.string.monthly_report_export_data_loaded_cta), + ) + } + } +} + +@Composable +@Preview(name = "Loading preview", showSystemUi = true) +private fun LoadingPreview() { + AppTheme { + LoadingView() + } +} + +@Composable +@Preview(name = "Error preview", showSystemUi = true) +private fun ErrorPreview() { + AppTheme { + ErrorView( + exception = IllegalArgumentException("An error occurred"), + onRetryButtonClicked = {}, + ) + } +} + +@Composable +@Preview(name = "Success preview", showSystemUi = true) +private fun SuccessPreview() { + AppTheme { + LoadedView( + onDownloadButtonClicked = {}, + ) + } +} \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/res/drawable/ic_baseline_upload_file_24.xml b/Android/EasyBudget/app/src/main/res/drawable/ic_baseline_upload_file_24.xml new file mode 100644 index 00000000..a028cb36 --- /dev/null +++ b/Android/EasyBudget/app/src/main/res/drawable/ic_baseline_upload_file_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/Android/EasyBudget/app/src/main/res/layout/activity_monthly_report_export.xml b/Android/EasyBudget/app/src/main/res/layout/activity_monthly_report_export.xml new file mode 100644 index 00000000..857b5d8c --- /dev/null +++ b/Android/EasyBudget/app/src/main/res/layout/activity_monthly_report_export.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/res/menu/menu_monthly_report.xml b/Android/EasyBudget/app/src/main/res/menu/menu_monthly_report.xml new file mode 100644 index 00000000..4053858a --- /dev/null +++ b/Android/EasyBudget/app/src/main/res/menu/menu_monthly_report.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/res/values-de/strings.xml b/Android/EasyBudget/app/src/main/res/values-de/strings.xml index 9ee050b5..90332b1e 100644 --- a/Android/EasyBudget/app/src/main/res/values-de/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-de/strings.xml @@ -219,14 +219,13 @@ 2. Teilen Sie Ihre Konten Sie können andere Pro-Benutzer zu Ihren Online-Konten einladen, damit diese Einträge anzeigen und hinzufügen/bearbeiten können. 3. Exportieren oder drucken Sie Ihre Daten - Exportieren Sie Ihre monatlichen Berichte als CSV, um die Daten in einer anderen App zu verwenden, oder als PDF, um sie auszudrucken. + Exportieren Sie Ihre monatlichen Berichte als CSV, um die Daten in einer anderen App zu verwenden oder auszudrucken. Kaufen Premium-Nutzer werden Es sieht so aus als würde Ihnen EasyBudget gefallen!\nWürden Sie gerne in den nächsten Gang schalten und die Premium-Features entdecken? Entdecken Nicht jetzt Nicht wieder fragen - Demnächst… %s/Monat, jederzeit kündbar. Ups Der PlayStore konnte nicht erreicht werden, um die Premium- und Pro-Daten abzurufen. Bitte überprüfen Sie Ihr Netzwerk und versuchen Sie es erneut. @@ -362,9 +361,6 @@ Das Konto wurde erfolgreich verlassen Der Kontoname wurde erfolgreich aktualisiert Oops - Neu in EasyBudget - Verwalten Sie mehrere Konten und teilen Sie sie mit anderen, verfügbar mit EasyBudget Pro! - Entdecken Berechtigung für Benachrichtigungen verweigert Um Ihre täglichen und monatlichen Prämienerinnerungen zu erhalten, müssen Sie Benachrichtigungen zulassen. Möchten Sie Benachrichtigungen erhalten? Erlauben @@ -465,4 +461,21 @@ Beim Laden der Kalenderdaten ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.\n(%s) 1,99€ 4,99€ + Monatsbericht exportieren + Als CSV exportieren + Daten können nicht geladen werden + Beim Vorbereiten der CSV-Datei ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.\n(%s) + Wiederholen + Datum + Titel + Betrag + Geprüft + Wiederkehrend + Ups + Die CSV-Datei kann nicht mit einer App auf Ihrem Telefon geöffnet werden. Stellen Sie sicher, dass Sie über eine App verfügen, die CSV-Dateien öffnen kann, und versuchen Sie es erneut. + Ihre CSV-Datei ist fertig + Exportieren + Neu in EasyBudget + Als Pro-Benutzer können Sie jetzt Ihre monatlichen Berichte als CSV exportieren! + Entdecken diff --git a/Android/EasyBudget/app/src/main/res/values-es/strings.xml b/Android/EasyBudget/app/src/main/res/values-es/strings.xml index a55c65e6..c33f2bd3 100644 --- a/Android/EasyBudget/app/src/main/res/values-es/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-es/strings.xml @@ -219,14 +219,13 @@ 2. Comparte tus cuentas Puede invitar a otros usuarios Pro a sus cuentas en línea para que puedan ver y agregar/editar entradas. 3. Exporta o imprime tus datos - Exporte sus informes mensuales como CSV para usar los datos en otra aplicación o en PDF para imprimirlos. + Exporte sus informes mensuales como CSV para usar los datos en otra aplicación o imprimirlos. Comprar Vuélvete premium ¡Parece que te gusta usar EasyBudget!\n¿Quieres descubrir las funcionalidades premium? Descubrir Ahora no No preguntar más - Muy pronto… %s/mes, cancela en cualquier momento. Ups No se puede acceder a PlayStore para recuperar los datos Premium y Pro. Por favor verifique su red e inténtelo nuevamente. @@ -362,9 +361,6 @@ Cuenta abandonada con éxito Nombre de cuenta actualizado con éxito ¡Ups - Nuevo en EasyBudget - Administre varias cuentas y compártalas con otros, ¡disponible con EasyBudget Pro! - Descubrir Permiso de notificaciones denegado Para recibir los recordatorios diarios y mensuales de su prima, debe permitir las notificaciones. ¿Quieres recibir notificaciones? Permitir @@ -465,4 +461,21 @@ Se produjo un error al cargar los datos del calendario. Por favor, inténtalo de nuevo.\n(%s) 1,99€ 4,99€ + Exportar informe mensual + Exportar como CSV + No se pueden cargar datos + Se produjo un error al preparar el archivo CSV. Por favor, inténtalo de nuevo.\n(%s) + Reintentar + Fecha + Título + Cantidad + Marcado + Recurrente + Ups + No se puede abrir el CSV con una aplicación en su teléfono. Asegúrate de tener una aplicación que pueda abrir archivos CSV y vuelve a intentarlo. + Su archivo CSV está listo + Exportar + Nuevo en EasyBudget + ¡Como usuario Pro, ahora puedes exportar tus informes mensuales como CSV! + Descubrir diff --git a/Android/EasyBudget/app/src/main/res/values-fr/strings.xml b/Android/EasyBudget/app/src/main/res/values-fr/strings.xml index de548c23..ac84232c 100644 --- a/Android/EasyBudget/app/src/main/res/values-fr/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-fr/strings.xml @@ -219,14 +219,13 @@ 2. Partagez vos comptes Vous pouvez inviter d\'autres utilisateurs Pro sur vos comptes en ligne afin qu\'ils puissent voir et ajouter/modifier des entrées. 3. Exportez ou imprimez vos données - Exportez vos rapports mensuels au format CSV pour utiliser les données sur une autre application ou au format PDF pour les imprimer. + Exportez vos rapports mensuels au format CSV pour utiliser les données dans une autre application ou les imprimer. Acheter Devenez utilisateur premium On dirait que vous aimez utiliser EasyBudget !\nVoulez vous passer à la vitesse supérieure et découvrir les fonctionnalités premium ? Découvrir Pas maintenant Ne plus me demander - À venir… %s/mois, annulez quand vous voulez. Oups Impossible de se connecter au PlayStore pour récupérer les données du mode Premium et Pro. Vérifiez votre connexion et réessayez. @@ -362,9 +361,6 @@ Compte quitté avec succès Nom du compte mis à jour avec succès Oups - Nouveau dans EasyBudget - Gérez plusieurs comptes et partagez-les avec d\'autres, disponible avec EasyBudget Pro ! - Découvrir Permission de notification refusée Pour recevoir vos rappels quotidiens et mensuels premium, vous devez autoriser les notifications. Voulez-vous recevoir les notifications? Autoriser @@ -465,4 +461,21 @@ Une erreur est survenue lors du chargement des données du calendrier. Veuillez réessayer.\n(%s) 1,99€ 4,99€ + Exporter le rapport mensuel + Exporter au format CSV + Impossible de charger les données + Une erreur s\'est produite lors de la préparation du fichier CSV. Veuillez réessayer.\n(%s) + Réessayer + Date + Titre + Montant + Marqué + Récurrent + Oups + Impossible d\'ouvrir le CSV avec une application sur votre téléphone. Assurez-vous que vous disposez d\'une app capable d\'ouvrir les fichiers CSV et réessayez. + Votre fichier CSV est prêt + Exporter + Nouveau dans EasyBudget + En tant qu\'utilisateur Pro, vous pouvez désormais exporter vos rapports mensuels au format CSV ! + Découvrir diff --git a/Android/EasyBudget/app/src/main/res/values-it/strings.xml b/Android/EasyBudget/app/src/main/res/values-it/strings.xml index af9409ab..d1244a66 100644 --- a/Android/EasyBudget/app/src/main/res/values-it/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-it/strings.xml @@ -219,14 +219,13 @@ 2. Condividi i tuoi account Puoi invitare altri utenti Pro ai tuoi account online in modo che possano vedere e aggiungere/modificare voci. 3. Esporta o stampa i tuoi dati - Esporta i tuoi rapporti mensili come CSV per utilizzare i dati su un\'altra app o in PDF per stamparli. + Esporta i tuoi report mensili come CSV per utilizzare i dati su un\'altra app o per stamparli. Acquista Diventa premium Si direbbe che EasyBudget ti piace!\nVuoi passare al livello successivo e scoprire le funzionalità premium? Scopri Non ora Non chiedermelo più - Prossimamente… %s/mese, annulla in qualsiasi momento. Spiacenti Impossibile raggiungere il PlayStore per recuperare i dati Premium e Pro. Controlla la tua rete e riprova. @@ -362,9 +361,6 @@ Account lasciato con successo Nome account aggiornato correttamente OPS! - Nuovo in EasyBudget - Gestisci più account e condividili con altri, disponibile con EasyBudget Pro! - Scoprire Autorizzazione notifiche negata Per ricevere i tuoi promemoria giornalieri e mensili premium, devi consentire le notifiche. Vuoi ricevere le notifiche? Permettere @@ -465,4 +461,21 @@ Si è verificato un errore durante il caricamento dei dati del calendario. Per favore riprova.\n(%s) 1,99€ 4,99€ + Esporta rapporto mensile + Esporta come CSV + Impossibile caricare i dati + Si è verificato un errore durante la preparazione del file CSV. Per favore riprova.\n(%s) + Riprova + Data + Titolo + Importo + Contrassegnato + Ricorrente + Spiacenti + Impossibile aprire il CSV con un\'app sul tuo telefono. Assicurati di avere un\'app in grado di aprire file CSV e riprova. + Il tuo file CSV è pronto + Esporta + Novità in EasyBudget + Come utente Pro, ora puoi esportare i tuoi rapporti mensili come CSV! + Scopri diff --git a/Android/EasyBudget/app/src/main/res/values-pt/strings.xml b/Android/EasyBudget/app/src/main/res/values-pt/strings.xml index 502e031a..0adfb054 100644 --- a/Android/EasyBudget/app/src/main/res/values-pt/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-pt/strings.xml @@ -219,14 +219,13 @@ 2. Compartilhe suas contas Você pode convidar outros usuários Pro para suas contas online para que possam ver e adicionar/editar entradas. 3. Exporte ou imprima seus dados - Exporte seus relatórios mensais como CSV para usar os dados em outro aplicativo ou para PDF para imprimi-los. + Exporte seus relatórios mensais como CSV para usar os dados em outro aplicativo ou para imprimi-los. Comprar Torne-se premium Parece que você gosta de usar EasyBudget!\nVocê gostaria de passar ao próximo nível e descobrir as funções premium? Descobrir Agora não Não perguntar novamente - Em breve… %s/mês, cancele a qualquer momento. Ops Não foi possível acessar a PlayStore para buscar os dados Premium e Pro. Verifique sua rede e tente novamente. @@ -362,9 +361,6 @@ Conta encerrada com sucesso Nome da conta atualizado com sucesso Ops - Novidade no EasyBudget - Gerencie várias contas e compartilhe-as com outras pessoas, disponível com o EasyBudget Pro! - Descobrir Permissão de notificações negada Para receber seus lembretes premium diários e mensais, você precisa permitir notificações. Deseja receber notificações? Permitir @@ -465,4 +461,21 @@ Ocorreu um erro ao carregar os dados do calendário. Por favor, tente novamente.\n(%s) 1,99€ 4,99€ + Exportar relatório mensal + Exportar como CSV + Não foi possível carregar dados + Ocorreu um erro ao preparar o arquivo CSV. Por favor, tente novamente.\n(%s) + Tentar novamente + Data + Título + Valor + Marcado + Recorrente + Opa + Não é possível abrir o CSV com um aplicativo no seu telefone. Certifique-se de ter um aplicativo que possa abrir arquivos CSV e tente novamente. + Seu arquivo CSV está pronto + Exportar + Novo no EasyBudget + Como usuário Pro, agora você pode exportar seus relatórios mensais como CSV! + Descubra diff --git a/Android/EasyBudget/app/src/main/res/values-ru/strings.xml b/Android/EasyBudget/app/src/main/res/values-ru/strings.xml index 7066f0a8..566847ff 100644 --- a/Android/EasyBudget/app/src/main/res/values-ru/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values-ru/strings.xml @@ -220,14 +220,13 @@ 2. Поделитесь своими аккаунтами Вы можете приглашать других пользователей Pro в свои онлайн-аккаунты, чтобы они могли просматривать и добавлять/редактировать записи. 3. Экспортируйте или распечатайте свои данные - Экспортируйте ежемесячные отчеты в формате CSV, чтобы использовать данные в другом приложении, или в формате PDF, чтобы распечатать их. + Экспортируйте свои ежемесячные отчеты в формате CSV, чтобы использовать данные в другом приложении или распечатать их. Купить Стать премиум-пользователем Похоже, Вам нравится использовать EasyBudget!\nВы бы хотели перейти на следующий уровень и открыть для себя премиум-функции? Открыть Не сейчас Не спрашивать в дальнейшем - Вскоре… %s/месяц, отмените в любое время. Ой Невозможно получить доступ к PlayStore для получения данных Premium и Pro. Пожалуйста, проверьте свою сеть и повторите попытку. @@ -364,9 +363,6 @@ Учетная запись успешно удалена Имя учетной записи успешно обновлено Ой… - Новое в EasyBudget - Управляйте несколькими учетными записями и делитесь ими с другими, доступными в EasyBudget Pro! - Обнаружить Notifications permission denied To receive your premium daily and monthly reminders, you need to allow notifications. Do you want to receive notifications? Разрешать @@ -467,4 +463,21 @@ Произошла ошибка при загрузке данных календаря. Пожалуйста, попробуйте еще раз.\n(%s) $1.99 $4.99 + Экспортировать ежемесячный отчет + Экспортировать в формате CSV + Невозможно загрузить данные + Произошла ошибка при подготовке CSV-файла. Пожалуйста, попробуйте еще раз.\n(%s) + Повторить + Дата + Заголовок + Сумма + Отмечено + Периодически + Ой + Невозможно открыть CSV-файл с помощью приложения на телефоне. Убедитесь, что у вас есть приложение, которое может открывать файлы CSV, и повторите попытку. + Ваш CSV-файл готов + Экспорт + Новое в EasyBudget + Как пользователь Pro, вы теперь можете экспортировать свои ежемесячные отчеты в формате CSV! + Обнаружить \ No newline at end of file diff --git a/Android/EasyBudget/app/src/main/res/values/strings.xml b/Android/EasyBudget/app/src/main/res/values/strings.xml index 845c1cb5..c26c77fc 100644 --- a/Android/EasyBudget/app/src/main/res/values/strings.xml +++ b/Android/EasyBudget/app/src/main/res/values/strings.xml @@ -231,7 +231,7 @@ 2. Share your accounts You can invite other pro users to your online accounts so that they can see and add/edit entries. 3. Export or print your data - Export your monthly reports as CSV to use the data on another app or to PDF to print it. + Export your monthly reports as CSV to use the data on another app or to print it. Purchase Upgrade Become a premium user @@ -240,7 +240,6 @@ Not now Back Don\'t ask me again - Coming soon… %s/month, cancel any time. Oops Unable to reach the PlayStore to fetch the Premium and Pro data. Please check your network and try again. @@ -483,16 +482,31 @@ Default (offline) %s (online) - New in EasyBudget - Manage multiple accounts and share them with others with the new EasyBudget Pro! - Discover - Unable to load data An error occurred while loading calendar data. Please try again.\n(%s) $1.99 $4.99 + Export monthly report + Export as CSV + Unable to load data + An error occurred while preparing the CSV file. Please try again.\n(%s) + Retry + Date + Title + Amount + Checked + Recurring + Oops + Unable to open the CSV with an app on your phone. Make sure you have an app that can open CSV files and try again. + Your CSV file is ready + Export + + New in EasyBudget + As a Pro user, you can now export your monthly reports as CSV! + Discover + send_bug_report show_welcome_screen diff --git a/Android/EasyBudget/app/src/main/res/xml/provider_paths.xml b/Android/EasyBudget/app/src/main/res/xml/provider_paths.xml new file mode 100644 index 00000000..9121f30a --- /dev/null +++ b/Android/EasyBudget/app/src/main/res/xml/provider_paths.xml @@ -0,0 +1,18 @@ + + + + \ No newline at end of file diff --git a/Android/EasyBudget/build.gradle.kts b/Android/EasyBudget/build.gradle.kts index 8f5f6b7c..2c39ea35 100644 --- a/Android/EasyBudget/build.gradle.kts +++ b/Android/EasyBudget/build.gradle.kts @@ -15,15 +15,15 @@ */ val kotlinVersion by extra("1.9.22") // Change in the plugins below too val hiltVersion by extra("2.51") // Change in the plugins below too -val realmVersion by extra("1.14.0") // Change in the plugins below too +val realmVersion by extra("1.14.1") // Change in the plugins below too plugins { - id("com.android.application") version "8.3.0" apply false - id("com.android.library") version "8.3.0" apply false + id("com.android.application") version "8.3.1" apply false + id("com.android.library") version "8.3.1" apply false id("com.google.firebase.crashlytics") version "2.9.9" apply false id("com.google.gms.google-services") version "4.4.1" apply false id("org.jetbrains.kotlin.android") version "1.9.22" apply false id("com.google.dagger.hilt.android") version "2.51" apply false id("com.google.devtools.ksp") version "1.9.22-1.0.17" apply false - id("io.realm.kotlin") version "1.14.0" apply false + id("io.realm.kotlin") version "1.14.1" apply false }