Skip to content

Commit

Permalink
[Feature] Export list as csv #1622
Browse files Browse the repository at this point in the history
  • Loading branch information
patrickunterwegs committed Nov 17, 2024
1 parent 406d068 commit 333b200
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 0 deletions.
105 changes: 105 additions & 0 deletions app/src/main/java/at/techbee/jtx/database/relations/ICal4ListRel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import at.techbee.jtx.R
import at.techbee.jtx.database.COLUMN_ID
import at.techbee.jtx.database.Classification
import at.techbee.jtx.database.ICalObject
import at.techbee.jtx.database.ICalObject.Companion.TZ_ALLDAY
import at.techbee.jtx.database.Module
import at.techbee.jtx.database.Status
import at.techbee.jtx.database.properties.COLUMN_CATEGORY_ICALOBJECT_ID
Expand Down Expand Up @@ -218,5 +219,109 @@ data class ICal4ListRel(
null -> sortedList.groupBy { it.iCal4List.module }
}
}

/**
* Generates a string of headers for a CSV
* Use getCSVRow(...) to get the row that correponds to this header
* @return a String with the headers delimited by |
*/
fun getCSVHeader(module: Module, context: Context): String {
return mutableListOf<String>().apply {
if (module == Module.JOURNAL)
add(context.getString(R.string.date))
if (module == Module.TODO) {
add(context.getString(R.string.started))
add(context.getString(R.string.due))
add(context.getString(R.string.completed))
}
add(context.getString(R.string.summary))
add(context.getString(R.string.description))

if (module == Module.TODO)
add(context.getString(R.string.progress))

add(context.getString(R.string.categories))
if (module == Module.TODO)
add(context.getString(R.string.resources))

add(context.getString(R.string.status))
add(context.getString(R.string.classification))
if (module == Module.TODO)
add(context.getString(R.string.priority))

add(context.getString(R.string.contact))
add(context.getString(R.string.url))
add(context.getString(R.string.location))
add(context.getString(R.string.latitude) + "/" + context.getString(R.string.longitude))

add(context.getString(R.string.subtasks))
add(context.getString(R.string.view_feedback_linked_notes))
add(context.getString(R.string.attachments))
add(context.getString(R.string.attendees))
add(context.getString(R.string.comments))
add(context.getString(R.string.alarms))

add(context.getString(R.string.account))
add(context.getString(R.string.collection))

add(context.getString(R.string.filter_created))
add(context.getString(R.string.filter_last_modified))
}.joinToString(separator = "|")
}
}


/**
* @return string of values o the current entry delimited by | that corresponds to the headers in getCSVHeader(...)
*/
fun getCSVRow(context: Context): String {
return mutableListOf<String>().apply {
if(iCal4List.module == Module.JOURNAL.name)
add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.dtstart, if(iCal4List.dtstartTimezone == TZ_ALLDAY) "UTC" else iCal4List.dtstartTimezone))
if(iCal4List.module == Module.TODO.name) {
add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.dtstart, if(iCal4List.dtstartTimezone == TZ_ALLDAY) "UTC" else iCal4List.dtstartTimezone))
add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.due, if(iCal4List.dueTimezone == TZ_ALLDAY) "UTC" else iCal4List.dueTimezone))
add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.completed, if(iCal4List.completedTimezone == TZ_ALLDAY) "UTC" else iCal4List.completedTimezone))
}

add(iCal4List.summary?:"")
add(iCal4List.description?:"")

if(iCal4List.module == Module.TODO.name)
add(iCal4List.percent?.toString()?:"")

add(iCal4List.getSplitCategories().joinToString(separator = ", "))
if(iCal4List.module == Module.TODO.name)
add(iCal4List.resources?:"")

add(Status.getStatusFromString(iCal4List.status)?.let {
context.getString(it.stringResource)
}?:iCal4List.status ?:"")
add(Classification.getClassificationFromString(iCal4List.classification)?.let {
context.getString(it.stringResource)
}?:iCal4List.classification ?:"")
if(iCal4List.module == Module.TODO.name)
add(if (iCal4List.priority in 1..9) context.resources.getStringArray(R.array.priority)[iCal4List.priority!!] else "")

add(iCal4List.contact?:"")
add(iCal4List.url?:"")
add(iCal4List.location?:"")
add(if(iCal4List.geoLat != null && iCal4List.geoLong != null)
"(" + iCal4List.geoLat!!.toString() + ", " + iCal4List.geoLong!!.toString() + ")" else ""
)

add(iCal4List.numSubtasks.toString())
add(iCal4List.numSubnotes.toString())
add(iCal4List.numAttachments.toString())
add(iCal4List.numAttendees.toString())
add(iCal4List.numComments.toString())
add(iCal4List.numAlarms.toString())

add(iCal4List.accountName?:"")
add(iCal4List.collectionDisplayName?:"")

add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.created, null))
add(DateTimeUtils.convertLongToExcelDateTimeString(iCal4List.lastModified, null))
}.joinToString(separator = "|")
}
}
61 changes: 61 additions & 0 deletions app/src/main/java/at/techbee/jtx/ui/list/ListScreenTabContainer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,20 @@ import android.content.pm.PackageManager
import android.location.LocationListener
import android.location.LocationManager
import android.widget.Toast
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts.CreateDocument
import androidx.biometric.BiometricPrompt
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.pager.HorizontalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Add
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.MoreVert
import androidx.compose.material.icons.outlined.Search
Expand Down Expand Up @@ -65,6 +69,8 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
Expand All @@ -80,6 +86,7 @@ import at.techbee.jtx.database.properties.Alarm
import at.techbee.jtx.database.properties.AlarmRelativeTo
import at.techbee.jtx.database.properties.Attachment
import at.techbee.jtx.database.properties.Category
import at.techbee.jtx.database.relations.ICal4ListRel
import at.techbee.jtx.flavored.BillingManager
import at.techbee.jtx.ui.GlobalStateHolder
import at.techbee.jtx.ui.reusable.appbars.JtxNavigationDrawer
Expand All @@ -92,12 +99,15 @@ import at.techbee.jtx.ui.reusable.elements.CheckboxWithText
import at.techbee.jtx.ui.reusable.elements.RadiobuttonWithText
import at.techbee.jtx.ui.settings.DropdownSettingOption
import at.techbee.jtx.ui.settings.SettingsStateHolder
import at.techbee.jtx.util.DateTimeUtils
import at.techbee.jtx.util.SyncUtil
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
import com.google.accompanist.permissions.rememberMultiplePermissionsState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.io.IOException
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds

Expand Down Expand Up @@ -185,6 +195,28 @@ fun ListScreenTabContainer(
}
}

val launcherExportToCSV = rememberLauncherForActivityResult(CreateDocument("text/csv")) {
it?.let { uri ->
//isProcessing.postValue(true)
scope.launch(Dispatchers.IO) {
try {
context.contentResolver?.openOutputStream(uri)?.use { outputStream ->
val csvData = mutableListOf<String>()
csvData.add(ICal4ListRel.getCSVHeader(listViewModel.module, context))
listViewModel.iCal4ListRel.value?.forEach { ical4list ->
csvData.add(ical4list.getCSVRow(context))
}
//Log.d("CSV", csvData.joinToString(separator = System.lineSeparator()))
outputStream.write(csvData.joinToString(separator = System.lineSeparator()).toByteArray())
}
listViewModel.toastMessage.value = context.getString(R.string.list_toast_export_success)
} catch (e: IOException) {
listViewModel.toastMessage.value = context.getString(R.string.list_toast_export_error)
}
}
}
}

var topBarMenuExpanded by remember { mutableStateOf(false) }
var showDeleteSelectedDialog by rememberSaveable { mutableStateOf(false) }
var showUpdateEntriesDialog by rememberSaveable { mutableStateOf(false) }
Expand Down Expand Up @@ -578,6 +610,35 @@ fun ListScreenTabContainer(
getActiveViewModel().updateSearch(saveListSettings = true, isAuthenticated = globalStateHolder.isAuthenticated.value)
}
)
HorizontalDivider()
DropdownMenuItem(
text = {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = stringResource(id = R.string.export_as_csv),
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(end = 16.dp)
)
Text(
text = stringResource(id = R.string.settings_attention_experimental_feature),
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.widthIn(max = 150.dp),
fontStyle = FontStyle.Italic
)
}
},
leadingIcon = {
Icon(
imageVector = Icons.Outlined.Download,
contentDescription = null
)
},
onClick = {
launcherExportToCSV.launch("jtx_csvExport_${DateTimeUtils.timestampAsFilenameAppendix()}.csv")
topBarMenuExpanded = false
}
)
}
}
)
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/at/techbee/jtx/util/DateTimeUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,19 @@ object DateTimeUtils {
return zonedDateTime.format(formatter)
}

/**
* Creates a string from the date that can be used for the CSV export
*/
fun convertLongToExcelDateTimeString(date: Long?, timezone: String?): String {
if (date == null || date == 0L)
return ""
val zonedDateTime = ZonedDateTime.ofInstant(Instant.ofEpochMilli(date), requireTzId(timezone))
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT, FormatStyle.SHORT)
return zonedDateTime.format(formatter)
}



fun convertLongToFullDateString(date: Long?, timezone: String?): String {
if (date == null || date == 0L)
return ""
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@
<string name="menu_collections_add_remote_to_sync_app">"Add remote Collection (%1$s)"</string>
<string name="menu_collection_popup_show_in_sync_app">"Show in %1$s"</string>
<string name="menu_collection_popup_export_as_ics">"Export as .ics"</string>
<string name="export_as_csv">"Export as .csv"</string>
<string name="journal">"Journal"</string>
<string name="note">"Note"</string>
<string name="task">"Task"</string>
Expand Down Expand Up @@ -346,6 +347,8 @@
<string name="edit_recur_ends_never">"ends never"</string>
<string name="edit_recur_ends_on">"ends on"</string>
<string name="edit_recur_ends_after">"ends after"</string>
<string name="list_toast_export_success">"List successfully exported"</string>
<string name="list_toast_export_error">"Error on exporting list"</string>
<string name="collections_toast_export_success">"Collections successfully exported"</string>
<string name="collections_toast_export_error">"Error on exporting collections"</string>
<string name="menu_collections_export_all">"Export all"</string>
Expand Down

0 comments on commit 333b200

Please sign in to comment.