Export PDF transactions without requiring storage permission.
This commit is contained in:
parent
96faf9bffd
commit
ac06a3d708
18 changed files with 249 additions and 326 deletions
|
@ -74,8 +74,10 @@ dependencies {
|
||||||
implementation project(':PDFJet')
|
implementation project(':PDFJet')
|
||||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||||
// AndroidX
|
// AndroidX
|
||||||
|
implementation 'androidx.activity:activity-ktx:1.2.4'
|
||||||
implementation 'androidx.appcompat:appcompat:1.2.0'
|
implementation 'androidx.appcompat:appcompat:1.2.0'
|
||||||
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
|
||||||
|
implementation "androidx.fragment:fragment-ktx:1.3.2"
|
||||||
implementation "androidx.preference:preference-ktx:$preference_version"
|
implementation "androidx.preference:preference-ktx:$preference_version"
|
||||||
// Google
|
// Google
|
||||||
implementation 'com.google.zxing:core:3.4.0'
|
implementation 'com.google.zxing:core:3.4.0'
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
package cy.agorise.bitsybitshareswallet.domain.usecase
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.core.os.ConfigurationCompat
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import com.pdfjet.*
|
||||||
|
import cy.agorise.bitsybitshareswallet.R
|
||||||
|
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
class ExportTransactionsToPdfUseCase(private val applicationContext: Context) {
|
||||||
|
|
||||||
|
suspend operator fun invoke(
|
||||||
|
transactions: List<TransferDetail>,
|
||||||
|
folderDocumentFile: DocumentFile
|
||||||
|
) = withContext(Dispatchers.IO) { // TODO Inject Dispatcher
|
||||||
|
// Create the PDF file name
|
||||||
|
val fileName = applicationContext.resources.let {
|
||||||
|
"${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}.pdf"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtains the path to store the PDF
|
||||||
|
val pdfDocUri = folderDocumentFile.createFile("application/pdf", fileName)?.uri
|
||||||
|
val contentResolver = applicationContext.contentResolver
|
||||||
|
val pdfOutputStream = contentResolver.openOutputStream(pdfDocUri!!, "w")
|
||||||
|
|
||||||
|
// Creates a new PDF object
|
||||||
|
val pdf = PDF(BufferedOutputStream(pdfOutputStream), Compliance.PDF_A_1B)
|
||||||
|
|
||||||
|
// Font used for the table headers
|
||||||
|
val f1 = Font(pdf, CoreFont.HELVETICA_BOLD).apply { size = 7f }
|
||||||
|
|
||||||
|
// Font used for the table contents
|
||||||
|
val f2 = Font(pdf, CoreFont.HELVETICA).apply { size = 7f }
|
||||||
|
|
||||||
|
// Creates a new PDF table
|
||||||
|
val table = Table()
|
||||||
|
|
||||||
|
// 2D array of cells used to populate the PDF table
|
||||||
|
val tableData = mutableListOf<List<Cell>>()
|
||||||
|
|
||||||
|
// Add column names/headers
|
||||||
|
val columnNames = intArrayOf(R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date,
|
||||||
|
R.string.title_time, R.string.title_amount, R.string.title_equivalent_value)
|
||||||
|
|
||||||
|
val header = columnNames.map { columnName ->
|
||||||
|
Cell(f1, applicationContext.getString(columnName)).apply { setPadding(2f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the table headers
|
||||||
|
tableData.add(header)
|
||||||
|
|
||||||
|
// Add the table contents
|
||||||
|
applicationContext.let { context ->
|
||||||
|
val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
|
||||||
|
tableData.addAll(getData(transactions, f2, locale))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure the PDF table
|
||||||
|
table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS)
|
||||||
|
table.setCellBordersWidth(0.2f)
|
||||||
|
// The A4 size has 595 points of width, with the below we are trying to assign the same
|
||||||
|
// width to all cells and also keep them centered.
|
||||||
|
for (i in 0..6) {
|
||||||
|
table.setColumnWidth(i, 65f)
|
||||||
|
}
|
||||||
|
table.wrapAroundCellText()
|
||||||
|
table.mergeOverlaidBorders()
|
||||||
|
|
||||||
|
// Populate the PDF table
|
||||||
|
while (table.hasMoreData()) {
|
||||||
|
// Configures the PDF page
|
||||||
|
val page = Page(pdf, Letter.PORTRAIT)
|
||||||
|
table.setLocation(45f, 30f)
|
||||||
|
table.drawOn(page)
|
||||||
|
}
|
||||||
|
|
||||||
|
pdf.close()
|
||||||
|
|
||||||
|
Log.d("ExportTransactionsToPdf","PDF generated and saved")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getData(
|
||||||
|
transferDetails: List<TransferDetail>,
|
||||||
|
font: Font,
|
||||||
|
locale: Locale
|
||||||
|
): List<List<Cell>> = withContext(Dispatchers.Default) { // TODO Inject Dispatcher
|
||||||
|
|
||||||
|
val tableData = mutableListOf<List<Cell>>()
|
||||||
|
|
||||||
|
// Configure date and time formats to reuse in all the transfers
|
||||||
|
val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale)
|
||||||
|
val timeFormat = SimpleDateFormat("HH:mm:ss", locale)
|
||||||
|
var date: Date
|
||||||
|
|
||||||
|
// Save all the transfers information
|
||||||
|
for ((index, transferDetail) in transferDetails.withIndex()) {
|
||||||
|
val cols = mutableListOf<String>()
|
||||||
|
|
||||||
|
date = Date(transferDetail.date * 1000)
|
||||||
|
|
||||||
|
cols.add(transferDetail.from ?: "") // From
|
||||||
|
cols.add(transferDetail.to ?: "") // To
|
||||||
|
cols.add(transferDetail.memo) // Memo
|
||||||
|
cols.add(dateFormat.format(date)) // Date
|
||||||
|
cols.add(timeFormat.format(date)) // Time
|
||||||
|
|
||||||
|
// Asset Amount
|
||||||
|
val assetPrecision = transferDetail.assetPrecision
|
||||||
|
val assetAmount =
|
||||||
|
transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble())
|
||||||
|
cols.add(
|
||||||
|
String.format(
|
||||||
|
"%.${assetPrecision}f %s",
|
||||||
|
assetAmount,
|
||||||
|
transferDetail.assetSymbol
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fiat Equivalent
|
||||||
|
if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
|
||||||
|
val currency = Currency.getInstance(transferDetail.fiatSymbol)
|
||||||
|
val fiatAmount = transferDetail.fiatAmount.toDouble() /
|
||||||
|
Math.pow(10.0, currency.defaultFractionDigits.toDouble())
|
||||||
|
cols.add(
|
||||||
|
String.format(
|
||||||
|
"%.${currency.defaultFractionDigits}f %s",
|
||||||
|
fiatAmount, currency.currencyCode
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val row = cols.map { col ->
|
||||||
|
Cell(font, col).apply { setPadding(2f) }
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData.add(row)
|
||||||
|
|
||||||
|
appendMissingCells(tableData, font)
|
||||||
|
|
||||||
|
// TODO update progress
|
||||||
|
}
|
||||||
|
|
||||||
|
tableData
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun appendMissingCells(tableData: List<List<Cell>>, font: Font) {
|
||||||
|
val firstRow = tableData[0]
|
||||||
|
val numOfColumns = firstRow.size
|
||||||
|
for (i in tableData.indices) {
|
||||||
|
val dataRow = tableData[i] as ArrayList<Cell>
|
||||||
|
val dataRowColumns = dataRow.size
|
||||||
|
if (dataRowColumns < numOfColumns) {
|
||||||
|
for (j in 0 until numOfColumns - dataRowColumns) {
|
||||||
|
dataRow.add(Cell(font))
|
||||||
|
}
|
||||||
|
dataRow[dataRowColumns - 1].colSpan = numOfColumns - dataRowColumns + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,6 @@
|
||||||
package cy.agorise.bitsybitshareswallet.models
|
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment
|
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
|
||||||
/**
|
/**
|
|
@ -1,4 +1,4 @@
|
||||||
package cy.agorise.bitsybitshareswallet.fragments
|
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||||
|
|
||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
|
@ -18,7 +18,6 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||||
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
|
import cy.agorise.bitsybitshareswallet.adapters.BalancesDetailsAdapter
|
||||||
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
import cy.agorise.bitsybitshareswallet.database.joins.BalanceDetail
|
||||||
import cy.agorise.bitsybitshareswallet.databinding.DialogFilterOptionsBinding
|
import cy.agorise.bitsybitshareswallet.databinding.DialogFilterOptionsBinding
|
||||||
import cy.agorise.bitsybitshareswallet.models.FilterOptions
|
|
||||||
import cy.agorise.bitsybitshareswallet.utils.Constants
|
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||||
import cy.agorise.bitsybitshareswallet.utils.Helper
|
import cy.agorise.bitsybitshareswallet.utils.Helper
|
||||||
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
|
import cy.agorise.bitsybitshareswallet.viewmodels.BalanceDetailViewModel
|
||||||
|
@ -110,7 +109,7 @@ class FilterOptionsDialog : DialogFragment() {
|
||||||
binding.cbAsset.isChecked = mFilterOptions.assetAll
|
binding.cbAsset.isChecked = mFilterOptions.assetAll
|
||||||
|
|
||||||
// Configure BalanceDetailViewModel to obtain the user's Balances
|
// Configure BalanceDetailViewModel to obtain the user's Balances
|
||||||
viewModel.getAll().observe(viewLifecycleOwner, { balancesDetails ->
|
viewModel.getAll().observe(viewLifecycleOwner) { balancesDetails ->
|
||||||
mBalanceDetails.clear()
|
mBalanceDetails.clear()
|
||||||
mBalanceDetails.addAll(balancesDetails)
|
mBalanceDetails.addAll(balancesDetails)
|
||||||
mBalanceDetails.sortWith { a, b -> a.toString().compareTo(b.toString(), true) }
|
mBalanceDetails.sortWith { a, b -> a.toString().compareTo(b.toString(), true) }
|
||||||
|
@ -128,7 +127,7 @@ class FilterOptionsDialog : DialogFragment() {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// Initialize Equivalent Value
|
// Initialize Equivalent Value
|
||||||
binding.cbEquivalentValue.setOnCheckedChangeListener { _, isChecked ->
|
binding.cbEquivalentValue.setOnCheckedChangeListener { _, isChecked ->
|
|
@ -1,13 +1,10 @@
|
||||||
package cy.agorise.bitsybitshareswallet.fragments
|
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Point
|
import android.graphics.Point
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.os.Environment
|
|
||||||
import android.view.*
|
import android.view.*
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts.OpenDocumentTree
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentActivity
|
import androidx.fragment.app.FragmentActivity
|
||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
|
@ -20,12 +17,10 @@ import com.jakewharton.rxbinding3.appcompat.queryTextChangeEvents
|
||||||
import cy.agorise.bitsybitshareswallet.R
|
import cy.agorise.bitsybitshareswallet.R
|
||||||
import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter
|
import cy.agorise.bitsybitshareswallet.adapters.TransfersDetailsAdapter
|
||||||
import cy.agorise.bitsybitshareswallet.databinding.FragmentTransactionsBinding
|
import cy.agorise.bitsybitshareswallet.databinding.FragmentTransactionsBinding
|
||||||
import cy.agorise.bitsybitshareswallet.models.FilterOptions
|
import cy.agorise.bitsybitshareswallet.utils.BounceTouchListener
|
||||||
import cy.agorise.bitsybitshareswallet.utils.*
|
import cy.agorise.bitsybitshareswallet.utils.Constants
|
||||||
import cy.agorise.bitsybitshareswallet.viewmodels.TransactionsViewModel
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.CompositeDisposable
|
import io.reactivex.disposables.CompositeDisposable
|
||||||
import java.io.File
|
|
||||||
import java.util.*
|
import java.util.*
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
@ -35,17 +30,14 @@ import java.util.concurrent.TimeUnit
|
||||||
*/
|
*/
|
||||||
class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSelectedListener {
|
class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSelectedListener {
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "TransactionsFragment"
|
|
||||||
|
|
||||||
private const val REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION = 100
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel: TransactionsViewModel by viewModels()
|
private val viewModel: TransactionsViewModel by viewModels()
|
||||||
|
|
||||||
private var _binding: FragmentTransactionsBinding? = null
|
private var _binding: FragmentTransactionsBinding? = null
|
||||||
private val binding get() = _binding!!
|
private val binding get() = _binding!!
|
||||||
|
|
||||||
|
private var isPdfRequested: Boolean = false
|
||||||
|
private var isCsvRequested: Boolean = false
|
||||||
|
|
||||||
private var mDisposables = CompositeDisposable()
|
private var mDisposables = CompositeDisposable()
|
||||||
|
|
||||||
override fun onCreateView(
|
override fun onCreateView(
|
||||||
|
@ -78,7 +70,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
||||||
binding.rvTransactions.layoutManager = LinearLayoutManager(context)
|
binding.rvTransactions.layoutManager = LinearLayoutManager(context)
|
||||||
|
|
||||||
// Configure TransactionsViewModel to fetch the transaction history
|
// Configure TransactionsViewModel to fetch the transaction history
|
||||||
viewModel.getFilteredTransactions(userId).observe(viewLifecycleOwner, { transactions ->
|
viewModel.getFilteredTransactions(userId).observe(viewLifecycleOwner) { transactions ->
|
||||||
if (transactions.isEmpty()) {
|
if (transactions.isEmpty()) {
|
||||||
binding.rvTransactions.visibility = View.GONE
|
binding.rvTransactions.visibility = View.GONE
|
||||||
binding.tvEmpty.visibility = View.VISIBLE
|
binding.tvEmpty.visibility = View.VISIBLE
|
||||||
|
@ -94,7 +86,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
||||||
if (shouldScrollUp)
|
if (shouldScrollUp)
|
||||||
binding.rvTransactions.scrollToPosition(0)
|
binding.rvTransactions.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
// Set custom touch listener to handle bounce/stretch effect
|
// Set custom touch listener to handle bounce/stretch effect
|
||||||
val bounceTouchListener = BounceTouchListener(binding.rvTransactions)
|
val bounceTouchListener = BounceTouchListener(binding.rvTransactions)
|
||||||
|
@ -136,7 +128,7 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
R.id.menu_export -> {
|
R.id.menu_export -> {
|
||||||
verifyStoragePermission()
|
showExportOptionsDialog()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
else -> super.onOptionsItemSelected(item)
|
else -> super.onOptionsItemSelected(item)
|
||||||
|
@ -162,75 +154,28 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
||||||
viewModel.applyFilterOptions(filterOptions)
|
viewModel.applyFilterOptions(filterOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verifies that the storage permission has been granted before attempting to generate the export options */
|
|
||||||
private fun verifyStoragePermission() {
|
|
||||||
if (ContextCompat.checkSelfPermission(
|
|
||||||
requireActivity(),
|
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
|
||||||
)
|
|
||||||
!= PackageManager.PERMISSION_GRANTED
|
|
||||||
) {
|
|
||||||
// Permission is not already granted
|
|
||||||
requestPermissions(
|
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
|
||||||
REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Permission is already granted
|
|
||||||
showExportOptionsDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Received the result of the storage permission request and if it was accepted then shows the export options
|
|
||||||
* dialog, but if it was not accepted then shows a toast explaining that the permission is necessary to generate
|
|
||||||
* the export options */
|
|
||||||
override fun onRequestPermissionsResult(
|
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<out String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
if (requestCode == REQUEST_WRITE_EXTERNAL_STORAGE_PERMISSION) {
|
|
||||||
if ((grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED)) {
|
|
||||||
showExportOptionsDialog()
|
|
||||||
} else {
|
|
||||||
context?.toast(getString(R.string.msg__storage_permission_necessary_export))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showExportOptionsDialog() {
|
private fun showExportOptionsDialog() {
|
||||||
|
// Make export options selected by default
|
||||||
|
val indices = intArrayOf(0, 1)
|
||||||
|
|
||||||
MaterialDialog(requireContext()).show {
|
MaterialDialog(requireContext()).show {
|
||||||
title(R.string.title_export_transactions)
|
title(R.string.title_export_transactions)
|
||||||
listItemsMultiChoice(
|
listItemsMultiChoice(
|
||||||
R.array.export_options,
|
R.array.export_options,
|
||||||
initialSelection = intArrayOf(0, 1)
|
initialSelection = indices,
|
||||||
|
waitForPositiveButton = true
|
||||||
) { _, indices, _ ->
|
) { _, indices, _ ->
|
||||||
val exportPDF = indices.contains(0)
|
// Update export options selected
|
||||||
val exportCSV = indices.contains(1)
|
isPdfRequested = indices.contains(0)
|
||||||
exportFilteredTransactions(exportPDF, exportCSV)
|
isCsvRequested = indices.contains(1)
|
||||||
}
|
}
|
||||||
positiveButton(R.string.title_export)
|
negativeButton(android.R.string.cancel) { dismiss() }
|
||||||
|
positiveButton(R.string.title_export) { getFolderForExport.launch(null) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Creates the export procedures for PDF and CSV, depending on the user selection. */
|
private val getFolderForExport = registerForActivityResult(OpenDocumentTree()) { folderUri ->
|
||||||
private fun exportFilteredTransactions(exportPDF: Boolean, exportCSV: Boolean) {
|
viewModel.exportFilteredTransactions(folderUri, isPdfRequested, isCsvRequested)
|
||||||
// Verifies the BiTSy folder exists in the external storage and if it doesn't then it tries to create it
|
|
||||||
val dir = File(Environment.getExternalStorageDirectory(), Constants.EXTERNAL_STORAGE_FOLDER)
|
|
||||||
if (!dir.exists()) {
|
|
||||||
if (!dir.mkdirs())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.getFilteredTransactionsOnce()?.let { filteredTransactions ->
|
|
||||||
if (exportPDF)
|
|
||||||
activity?.let { PDFGeneratorTask(it).execute(filteredTransactions) }
|
|
||||||
|
|
||||||
if (exportCSV)
|
|
||||||
activity?.let { CSVGenerationTask(it).execute(filteredTransactions) }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
@ -238,4 +183,8 @@ class TransactionsFragment : Fragment(), FilterOptionsDialog.OnFilterOptionsSele
|
||||||
|
|
||||||
if (!mDisposables.isDisposed) mDisposables.dispose()
|
if (!mDisposables.isDisposed) mDisposables.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "TransactionsFragment"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,10 +1,13 @@
|
||||||
package cy.agorise.bitsybitshareswallet.viewmodels
|
package cy.agorise.bitsybitshareswallet.ui.transactions
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.*
|
import androidx.lifecycle.*
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
import com.google.android.material.datepicker.MaterialDatePicker
|
||||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
||||||
import cy.agorise.bitsybitshareswallet.models.FilterOptions
|
import cy.agorise.bitsybitshareswallet.domain.usecase.ExportTransactionsToPdfUseCase
|
||||||
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
|
import cy.agorise.bitsybitshareswallet.repositories.TransferDetailRepository
|
||||||
import cy.agorise.bitsybitshareswallet.utils.Helper
|
import cy.agorise.bitsybitshareswallet.utils.Helper
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -14,17 +17,17 @@ import java.util.*
|
||||||
|
|
||||||
|
|
||||||
class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
|
class TransactionsViewModel(application: Application) : AndroidViewModel(application) {
|
||||||
companion object {
|
|
||||||
const val TAG = "TransactionsViewModel"
|
// TODO Inject below dependencies
|
||||||
}
|
private val exportTransactionsToPdfUseCase = ExportTransactionsToPdfUseCase(application)
|
||||||
private var mRepository = TransferDetailRepository(application)
|
private val mRepository = TransferDetailRepository(application)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* [FilterOptions] used to filter the list of [TransferDetail] taken from the database
|
* [FilterOptions] used to filter the list of [TransferDetail] taken from the database
|
||||||
*/
|
*/
|
||||||
private var mFilterOptions = FilterOptions()
|
private var mFilterOptions = FilterOptions()
|
||||||
|
|
||||||
private lateinit var transactions : LiveData<List<TransferDetail>>
|
private lateinit var transactions: LiveData<List<TransferDetail>>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This [MediatorLiveData] is used to combine two sources of information into one, keeping the
|
* This [MediatorLiveData] is used to combine two sources of information into one, keeping the
|
||||||
|
@ -64,7 +67,8 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
|
||||||
return mFilterOptions
|
return mFilterOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun applyFilterOptions(filterOptions: FilterOptions) = transactions.value?.let { transactions ->
|
internal fun applyFilterOptions(filterOptions: FilterOptions) =
|
||||||
|
transactions.value?.let { transactions ->
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
filteredTransactions.value = filter(transactions, filterOptions)
|
filteredTransactions.value = filter(transactions, filterOptions)
|
||||||
}
|
}
|
||||||
|
@ -83,7 +87,10 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
|
||||||
* Filters the given list of [TransferDetail] given the [FilterOptions] and returns a filtered list
|
* Filters the given list of [TransferDetail] given the [FilterOptions] and returns a filtered list
|
||||||
* of [TransferDetail], doing all the work in a background thread using kotlin coroutines
|
* of [TransferDetail], doing all the work in a background thread using kotlin coroutines
|
||||||
*/
|
*/
|
||||||
private suspend fun filter(transactions: List<TransferDetail>, filterOptions: FilterOptions) : List<TransferDetail> {
|
private suspend fun filter(
|
||||||
|
transactions: List<TransferDetail>,
|
||||||
|
filterOptions: FilterOptions
|
||||||
|
): List<TransferDetail> {
|
||||||
return withContext(Dispatchers.Default) {
|
return withContext(Dispatchers.Default) {
|
||||||
|
|
||||||
// Create a list to store the filtered transactions
|
// Create a list to store the filtered transactions
|
||||||
|
@ -107,7 +114,8 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
|
||||||
|
|
||||||
// Filter by date range
|
// Filter by date range
|
||||||
if (!filterOptions.dateRangeAll && (transaction.date < startDate ||
|
if (!filterOptions.dateRangeAll && (transaction.date < startDate ||
|
||||||
transaction.date > endDate))
|
transaction.date > endDate)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Filter by asset
|
// Filter by asset
|
||||||
|
@ -115,8 +123,10 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Filter by equivalent value
|
// Filter by equivalent value
|
||||||
if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount ?: -1 ) < filterOptions.fromEquivalentValue
|
if (!filterOptions.equivalentValueAll && ((transaction.fiatAmount
|
||||||
|| (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue))
|
?: -1) < filterOptions.fromEquivalentValue
|
||||||
|
|| (transaction.fiatAmount ?: -1) > filterOptions.toEquivalentValue)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
// Filter transactions sent to agorise
|
// Filter transactions sent to agorise
|
||||||
|
@ -133,4 +143,27 @@ class TransactionsViewModel(application: Application) : AndroidViewModel(applica
|
||||||
filteredTransactions
|
filteredTransactions
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Creates the export procedures for PDF and CSV, depending on the user selection. */
|
||||||
|
fun exportFilteredTransactions(folderUri: Uri, exportPdf: Boolean, exportCsv: Boolean) {
|
||||||
|
Log.d(TAG, "Export options selected. PDF: $exportPdf, CSV: $exportCsv")
|
||||||
|
|
||||||
|
if (!exportPdf && !exportCsv) return
|
||||||
|
|
||||||
|
// TODO Use injected context
|
||||||
|
val folderDocumentFile = DocumentFile.fromTreeUri(getApplication(), folderUri) ?: return
|
||||||
|
|
||||||
|
if (exportPdf) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
filteredTransactions.value?.let { transactions ->
|
||||||
|
// TODO Show success/failure message
|
||||||
|
exportTransactionsToPdfUseCase(transactions, folderDocumentFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "TransactionsViewModel"
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -1,215 +0,0 @@
|
||||||
package cy.agorise.bitsybitshareswallet.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.AsyncTask
|
|
||||||
import android.os.Environment
|
|
||||||
import android.util.Log
|
|
||||||
import androidx.core.os.ConfigurationCompat
|
|
||||||
import com.pdfjet.*
|
|
||||||
|
|
||||||
import cy.agorise.bitsybitshareswallet.R
|
|
||||||
import cy.agorise.bitsybitshareswallet.database.joins.TransferDetail
|
|
||||||
import java.io.*
|
|
||||||
import java.lang.Exception
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.text.SimpleDateFormat
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AsyncTask subclass used to move the PDF generation procedure to a background thread
|
|
||||||
* and inform the UI of the progress.
|
|
||||||
*/
|
|
||||||
class PDFGeneratorTask(context: Context) : AsyncTask<List<TransferDetail>, Int, String>() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "PDFGeneratorTask"
|
|
||||||
}
|
|
||||||
|
|
||||||
private val mContextRef: WeakReference<Context> = WeakReference(context)
|
|
||||||
|
|
||||||
override fun doInBackground(vararg params: List<TransferDetail>): String {
|
|
||||||
return createPDFDocument(params[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun combinePath(path1: String, path2: String): String {
|
|
||||||
val file1 = File(path1)
|
|
||||||
val file2 = File(file1, path2)
|
|
||||||
return file2.path
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Creates an empty file with the given name, in case it does not exist */
|
|
||||||
private fun createEmptyFile(path: String) {
|
|
||||||
try {
|
|
||||||
val file = File(path)
|
|
||||||
val writer = FileWriter(file)
|
|
||||||
writer.flush()
|
|
||||||
writer.close()
|
|
||||||
} catch (e: IOException) {
|
|
||||||
e.printStackTrace()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createPDFDocument(transferDetails: List<TransferDetail>): String {
|
|
||||||
return try {
|
|
||||||
// Create the PDF file name
|
|
||||||
val fileName = mContextRef.get()?.resources?.let {
|
|
||||||
"${it.getString(R.string.app_name)}-${it.getString(R.string.title_transactions)}"} + ".pdf"
|
|
||||||
|
|
||||||
// Obtains the path to store the PDF
|
|
||||||
val externalStorageFolder = Environment.getExternalStorageDirectory().absolutePath + File.separator +
|
|
||||||
Constants.EXTERNAL_STORAGE_FOLDER
|
|
||||||
val filePath = combinePath(externalStorageFolder, fileName)
|
|
||||||
createEmptyFile(filePath)
|
|
||||||
|
|
||||||
// Creates a new PDF object
|
|
||||||
val pdf = PDF(
|
|
||||||
BufferedOutputStream(
|
|
||||||
FileOutputStream(filePath)), Compliance.PDF_A_1B)
|
|
||||||
|
|
||||||
|
|
||||||
// Font used for the table headers
|
|
||||||
val f1 = Font(pdf, CoreFont.HELVETICA_BOLD)
|
|
||||||
f1.size = 7f
|
|
||||||
|
|
||||||
// Font used for the table contents
|
|
||||||
val f2 = Font(pdf, CoreFont.HELVETICA)
|
|
||||||
f2.size = 7f
|
|
||||||
|
|
||||||
// Creates a new PDF table
|
|
||||||
val table = Table()
|
|
||||||
|
|
||||||
// 2D array of cells used to populate the PDF table
|
|
||||||
val tableData = ArrayList<List<Cell>>()
|
|
||||||
|
|
||||||
// Add column names/headers
|
|
||||||
val columnNames = intArrayOf(R.string.title_from, R.string.title_to, R.string.title_memo, R.string.title_date,
|
|
||||||
R.string.title_time, R.string.title_amount, R.string.title_equivalent_value)
|
|
||||||
|
|
||||||
val header = ArrayList<Cell>()
|
|
||||||
|
|
||||||
for (columnName in columnNames) {
|
|
||||||
val cell = Cell(f1, mContextRef.get()?.getString(columnName))
|
|
||||||
cell.setTopPadding(2f)
|
|
||||||
cell.setBottomPadding(2f)
|
|
||||||
cell.setLeftPadding(2f)
|
|
||||||
cell.setRightPadding(2f)
|
|
||||||
header.add(cell)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the table headers
|
|
||||||
tableData.add(header)
|
|
||||||
|
|
||||||
// Add the table contents
|
|
||||||
mContextRef.get()?.let { context ->
|
|
||||||
val locale = ConfigurationCompat.getLocales(context.resources.configuration)[0]
|
|
||||||
tableData.addAll(getData(transferDetails, f2, locale))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configure the PDF table
|
|
||||||
table.setData(tableData, Table.DATA_HAS_1_HEADER_ROWS)
|
|
||||||
table.setCellBordersWidth(0.2f)
|
|
||||||
// The A4 size has 595 points of width, with the below we are trying to assign the same
|
|
||||||
// width to all cells and also keep them centered.
|
|
||||||
for (i in 0..6) {
|
|
||||||
table.setColumnWidth(i, 65f)
|
|
||||||
}
|
|
||||||
table.wrapAroundCellText()
|
|
||||||
table.mergeOverlaidBorders()
|
|
||||||
|
|
||||||
// Populate the PDF table
|
|
||||||
while (table.hasMoreData()) {
|
|
||||||
// Configures the PDF page
|
|
||||||
val page = Page(pdf, Letter.PORTRAIT)
|
|
||||||
table.setLocation(45f, 30f)
|
|
||||||
table.drawOn(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
pdf.close()
|
|
||||||
|
|
||||||
"PDF generated and saved: $filePath"
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Exception while trying to generate a PDF. Msg: " + e.message)
|
|
||||||
"Unable to generate PDF. Please retry. Error: ${e.message}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getData(transferDetails: List<TransferDetail>, font: Font, locale: Locale): List<List<Cell>> {
|
|
||||||
|
|
||||||
val tableData = ArrayList<List<Cell>>()
|
|
||||||
|
|
||||||
// Configure date and time formats to reuse in all the transfers
|
|
||||||
val dateFormat = SimpleDateFormat("MM-dd-yyyy", locale)
|
|
||||||
val timeFormat = SimpleDateFormat("HH:mm:ss", locale)
|
|
||||||
var date: Date
|
|
||||||
|
|
||||||
// Save all the transfers information
|
|
||||||
for ( (index, transferDetail) in transferDetails.withIndex()) {
|
|
||||||
val row = ArrayList<Cell>()
|
|
||||||
|
|
||||||
val cols = ArrayList<String>()
|
|
||||||
|
|
||||||
date = Date(transferDetail.date * 1000)
|
|
||||||
|
|
||||||
cols.add(transferDetail.from ?: "") // From
|
|
||||||
cols.add(transferDetail.to ?: "") // To
|
|
||||||
cols.add(transferDetail.memo) // Memo
|
|
||||||
cols.add(dateFormat.format(date)) // Date
|
|
||||||
cols.add(timeFormat.format(date)) // Time
|
|
||||||
|
|
||||||
// Asset Amount
|
|
||||||
val assetPrecision = transferDetail.assetPrecision
|
|
||||||
val assetAmount = transferDetail.assetAmount.toDouble() / Math.pow(10.0, assetPrecision.toDouble())
|
|
||||||
cols.add(String.format("%.${assetPrecision}f %s", assetAmount, transferDetail.assetSymbol))
|
|
||||||
|
|
||||||
// Fiat Equivalent
|
|
||||||
if (transferDetail.fiatAmount != null && transferDetail.fiatSymbol != null) {
|
|
||||||
val currency = Currency.getInstance(transferDetail.fiatSymbol)
|
|
||||||
val fiatAmount = transferDetail.fiatAmount.toDouble() /
|
|
||||||
Math.pow(10.0, currency.defaultFractionDigits.toDouble())
|
|
||||||
cols.add(String.format("%.${currency.defaultFractionDigits}f %s",
|
|
||||||
fiatAmount, currency.currencyCode))
|
|
||||||
}
|
|
||||||
|
|
||||||
for (col in cols) {
|
|
||||||
val cell = Cell(font, col)
|
|
||||||
cell.setTopPadding(2f)
|
|
||||||
cell.setBottomPadding(2f)
|
|
||||||
cell.setLeftPadding(2f)
|
|
||||||
cell.setRightPadding(2f)
|
|
||||||
row.add(cell)
|
|
||||||
}
|
|
||||||
tableData.add(row)
|
|
||||||
|
|
||||||
appendMissingCells(tableData, font)
|
|
||||||
|
|
||||||
// TODO update progress
|
|
||||||
}
|
|
||||||
|
|
||||||
return tableData
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun appendMissingCells(tableData: List<List<Cell>>, font: Font) {
|
|
||||||
val firstRow = tableData[0]
|
|
||||||
val numOfColumns = firstRow.size
|
|
||||||
for (i in tableData.indices) {
|
|
||||||
val dataRow = tableData[i] as ArrayList<Cell>
|
|
||||||
val dataRowColumns = dataRow.size
|
|
||||||
if (dataRowColumns < numOfColumns) {
|
|
||||||
for (j in 0 until numOfColumns - dataRowColumns) {
|
|
||||||
dataRow.add(Cell(font))
|
|
||||||
}
|
|
||||||
dataRow[dataRowColumns - 1].colSpan = numOfColumns - dataRowColumns + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
override fun onProgressUpdate(values: Array<Int>) {
|
|
||||||
// TODO show progress
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPostExecute(message: String) {
|
|
||||||
mContextRef.get()?.toast(message)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -50,7 +50,7 @@
|
||||||
|
|
||||||
<fragment
|
<fragment
|
||||||
android:id="@+id/transactions_dest"
|
android:id="@+id/transactions_dest"
|
||||||
android:name="cy.agorise.bitsybitshareswallet.fragments.TransactionsFragment"
|
android:name="cy.agorise.bitsybitshareswallet.ui.transactions.TransactionsFragment"
|
||||||
android:label="@string/title_transactions"
|
android:label="@string/title_transactions"
|
||||||
tools:layout="@layout/fragment_transactions">
|
tools:layout="@layout/fragment_transactions">
|
||||||
|
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">Exportieren Sie gefilterte Transaktionen</string>
|
<string name="title_export_transactions">Exportieren Sie gefilterte Transaktionen</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">Zum Exportieren von PDF / CSV-Dateien ist eine Speichererlaubnis erforderlich.</string>
|
|
||||||
<string name="title_from">Von</string>
|
<string name="title_from">Von</string>
|
||||||
<string name="title_to">Zu</string>
|
<string name="title_to">Zu</string>
|
||||||
<string name="title_memo">Memo</string>
|
<string name="title_memo">Memo</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">Exportar transacciones filtradas</string>
|
<string name="title_export_transactions">Exportar transacciones filtradas</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">El permiso de almacenamiento es necesario para exportar los archivos PDF/CSV.</string>
|
|
||||||
<string name="title_from">De</string>
|
<string name="title_from">De</string>
|
||||||
<string name="title_to">Para</string>
|
<string name="title_to">Para</string>
|
||||||
<string name="title_memo">Memo</string>
|
<string name="title_memo">Memo</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">Exporter les transactions filtrées</string>
|
<string name="title_export_transactions">Exporter les transactions filtrées</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">Une autorisation de stockage est nécessaire pour exporter des fichiers PDF / CSV.</string>
|
|
||||||
<string name="title_from">De</string>
|
<string name="title_from">De</string>
|
||||||
<string name="title_to">À</string>
|
<string name="title_to">À</string>
|
||||||
<string name="title_memo">Note</string>
|
<string name="title_memo">Note</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">फ़िल्टर्ड लेनदेन को निर्यात करें</string>
|
<string name="title_export_transactions">फ़िल्टर्ड लेनदेन को निर्यात करें</string>
|
||||||
<string name="text__pdf">पीडीएफ</string>
|
<string name="text__pdf">पीडीएफ</string>
|
||||||
<string name="text__csv">सीएसवी</string>
|
<string name="text__csv">सीएसवी</string>
|
||||||
<string name="msg__storage_permission_necessary_export">PDF / CSV फ़ाइलों को निर्यात करने के लिए भंडारण अनुमति आवश्यक है।</string>
|
|
||||||
<string name="title_from">से</string>
|
<string name="title_from">से</string>
|
||||||
<string name="title_to">सेवा मेरे</string>
|
<string name="title_to">सेवा मेरे</string>
|
||||||
<string name="title_memo">मेमो</string>
|
<string name="title_memo">मेमो</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">フィルタリングされたトランザクションをエクスポートする</string>
|
<string name="title_export_transactions">フィルタリングされたトランザクションをエクスポートする</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">PDF / CSVファイルをエクスポートするには、ストレージ許可が必要です。</string>
|
|
||||||
<string name="title_from">から</string>
|
<string name="title_from">から</string>
|
||||||
<string name="title_to">に</string>
|
<string name="title_to">に</string>
|
||||||
<string name="title_memo">メモ</string>
|
<string name="title_memo">メモ</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">ਫਿਲਟਰ ਟ੍ਰਾਂਜੈਕਸ਼ਨਾਂ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ</string>
|
<string name="title_export_transactions">ਫਿਲਟਰ ਟ੍ਰਾਂਜੈਕਸ਼ਨਾਂ ਨੂੰ ਐਕਸਪੋਰਟ ਕਰੋ</string>
|
||||||
<string name="text__pdf">ਪੀਡੀਐਫ</string>
|
<string name="text__pdf">ਪੀਡੀਐਫ</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">ਸਟੋਰੇਜ਼ ਦੀ ਇਜ਼ਾਜ਼ਤ PDF / CSV ਫਾਈਲਾਂ ਨੂੰ ਨਿਰਯਾਤ ਕਰਨ ਲਈ ਜ਼ਰੂਰੀ ਹੈ.</string>
|
|
||||||
<string name="title_from">ਤੋਂ</string>
|
<string name="title_from">ਤੋਂ</string>
|
||||||
<string name="title_to">ਨੂੰ</string>
|
<string name="title_to">ਨੂੰ</string>
|
||||||
<string name="title_memo">ਮੀਮੋ</string>
|
<string name="title_memo">ਮੀਮੋ</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">Exportar transações filtradas</string>
|
<string name="title_export_transactions">Exportar transações filtradas</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">É necessária permissão de armazenamento para exportar arquivos PDF / CSV.</string>
|
|
||||||
<string name="title_from">De</string>
|
<string name="title_from">De</string>
|
||||||
<string name="title_to">Para</string>
|
<string name="title_to">Para</string>
|
||||||
<string name="title_memo">Memorando</string>
|
<string name="title_memo">Memorando</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">Экспорт отфильтрованных транзакций</string>
|
<string name="title_export_transactions">Экспорт отфильтрованных транзакций</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">Разрешение на хранение необходимо для экспорта файлов PDF / CSV.</string>
|
|
||||||
<string name="title_from">От</string>
|
<string name="title_from">От</string>
|
||||||
<string name="title_to">к</string>
|
<string name="title_to">к</string>
|
||||||
<string name="title_memo">напоминание</string>
|
<string name="title_memo">напоминание</string>
|
||||||
|
|
|
@ -65,7 +65,6 @@
|
||||||
<string name="title_export_transactions">导出已过滤的交易</string>
|
<string name="title_export_transactions">导出已过滤的交易</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">导出PDF / CSV文件需要存储权限。</string>
|
|
||||||
<string name="title_from">从</string>
|
<string name="title_from">从</string>
|
||||||
<string name="title_to">至</string>
|
<string name="title_to">至</string>
|
||||||
<string name="title_memo">备忘录</string>
|
<string name="title_memo">备忘录</string>
|
||||||
|
|
|
@ -67,7 +67,6 @@
|
||||||
<string name="title_export_transactions">Export filtered transactions</string>
|
<string name="title_export_transactions">Export filtered transactions</string>
|
||||||
<string name="text__pdf">PDF</string>
|
<string name="text__pdf">PDF</string>
|
||||||
<string name="text__csv">CSV</string>
|
<string name="text__csv">CSV</string>
|
||||||
<string name="msg__storage_permission_necessary_export">Storage permission is necessary to export PDF/CSV files.</string>
|
|
||||||
<string name="title_from">From</string>
|
<string name="title_from">From</string>
|
||||||
<string name="title_to">To</string>
|
<string name="title_to">To</string>
|
||||||
<string name="title_memo">Memo</string>
|
<string name="title_memo">Memo</string>
|
||||||
|
|
Loading…
Reference in a new issue