diff --git a/app/src/main/java/com/example/util/simpletimetracker/navigation/ActionResolverImpl.kt b/app/src/main/java/com/example/util/simpletimetracker/navigation/ActionResolverImpl.kt index 4a650bc4..aa0706dd 100644 --- a/app/src/main/java/com/example/util/simpletimetracker/navigation/ActionResolverImpl.kt +++ b/app/src/main/java/com/example/util/simpletimetracker/navigation/ActionResolverImpl.kt @@ -77,7 +77,7 @@ class ActionResolverImpl @Inject constructor( private fun openFile(activity: Activity?, data: OpenFileParams) { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) .addCategory(Intent.CATEGORY_OPENABLE) - .setType("application/*") + .setType(data.type) if (activity.checkIfIntentResolves(intent)) { openFileResultLauncher?.launch(intent) diff --git a/core/src/main/java/com/example/util/simpletimetracker/core/sharedViewModel/BackupViewModel.kt b/core/src/main/java/com/example/util/simpletimetracker/core/sharedViewModel/BackupViewModel.kt index 0860c4bc..5d92582f 100644 --- a/core/src/main/java/com/example/util/simpletimetracker/core/sharedViewModel/BackupViewModel.kt +++ b/core/src/main/java/com/example/util/simpletimetracker/core/sharedViewModel/BackupViewModel.kt @@ -1,6 +1,5 @@ package com.example.util.simpletimetracker.core.sharedViewModel -import androidx.annotation.StringRes import androidx.lifecycle.LiveData import androidx.lifecycle.MediatorLiveData import androidx.lifecycle.MutableLiveData @@ -31,6 +30,7 @@ import com.example.util.simpletimetracker.navigation.params.action.OpenFileParam import com.example.util.simpletimetracker.navigation.params.notification.SnackBarParams import com.example.util.simpletimetracker.navigation.params.screen.DataExportSettingDialogParams import com.example.util.simpletimetracker.navigation.params.screen.DataExportSettingsResult +import com.example.util.simpletimetracker.navigation.params.screen.HelpDialogParams import com.example.util.simpletimetracker.navigation.params.screen.StandardDialogParams import java.text.SimpleDateFormat import java.util.Date @@ -157,6 +157,24 @@ class BackupViewModel @Inject constructor( router.navigate(DataExportSettingDialogParams(CSV_EXPORT_DIALOG_TAG)) } + fun onImportCsvClick() { + router.navigate( + StandardDialogParams( + tag = CSV_IMPORT_ALERT_DIALOG_TAG, + message = resourceRepo.getString(R.string.archive_deletion_alert), + btnPositive = resourceRepo.getString(R.string.ok), + btnNegative = resourceRepo.getString(R.string.cancel) + ) + ) + } + + fun onImportCsvHelpClick() { + HelpDialogParams( + title = resourceRepo.getString(R.string.settings_import_csv), + text = resourceRepo.getString(R.string.settings_import_csv_help), + ).let(router::navigate) + } + fun onExportIcsClick() { router.navigate(DataExportSettingDialogParams(ICS_EXPORT_DIALOG_TAG)) } @@ -166,7 +184,18 @@ class BackupViewModel @Inject constructor( ALERT_DIALOG_TAG -> requestFileWork( requestCode = RequestCode.REQUEST_CODE_OPEN_FILE, work = ::onRestoreBackup, - params = OpenFileParams(::onFileOpenError), + params = OpenFileParams( + type = FILE_TYPE_BIN_OPEN, + notHandledCallback = ::onFileOpenError + ), + ) + CSV_IMPORT_ALERT_DIALOG_TAG -> requestFileWork( + requestCode = RequestCode.REQUEST_CODE_OPEN_FILE, + work = ::onImportCsvFile, + params = OpenFileParams( + type = FILE_TYPE_CSV_OPEN, + notHandledCallback = ::onFileOpenError + ), ) } } @@ -236,10 +265,7 @@ class BackupViewModel @Inject constructor( private fun onSaveBackup(uriString: String?) { if (uriString == null) return - executeFileWork( - successMessageId = R.string.message_backup_saved, - errorMessageId = R.string.message_save_error, - ) { + executeFileWork { backupInteractor.saveBackupFile(uriString) } } @@ -312,20 +338,14 @@ class BackupViewModel @Inject constructor( private fun onRestoreBackup(uriString: String?) { if (uriString == null) return - executeFileWork( - successMessageId = R.string.message_backup_restored, - errorMessageId = R.string.message_restore_error, - ) { + executeFileWork { backupInteractor.restoreBackupFile(uriString) } } private fun onSaveCsvFile(uriString: String?) { if (uriString == null) return - executeFileWork( - successMessageId = R.string.message_export_complete, - errorMessageId = R.string.message_export_error, - ) { + executeFileWork { csvExportInteractor.saveCsvFile( uriString = uriString, range = getRange() @@ -333,12 +353,16 @@ class BackupViewModel @Inject constructor( } } + private fun onImportCsvFile(uriString: String?) { + if (uriString == null) return + executeFileWork { + csvExportInteractor.importCsvFile(uriString) + } + } + private fun onSaveIcsFile(uriString: String?) { if (uriString == null) return - executeFileWork( - successMessageId = R.string.message_export_complete, - errorMessageId = R.string.message_export_error, - ) { + executeFileWork { icsExportInteractor.saveIcsFile( uriString = uriString, range = getRange() @@ -358,32 +382,26 @@ class BackupViewModel @Inject constructor( } private fun executeFileWork( - @StringRes successMessageId: Int, - @StringRes errorMessageId: Int, doWork: suspend () -> ResultCode, ) = viewModelScope.launch { progressVisibility.set(true) val resultCode = doWork() - if (resultCode == ResultCode.SUCCESS) { - successMessageId - } else { - errorMessageId - }.let(::showMessage) + resultCode.message.let(::showMessage) progressVisibility.set(false) } private fun onFileOpenError() { - showMessage(R.string.settings_file_open_error) + showMessage(resourceRepo.getString(R.string.settings_file_open_error)) } private fun onFileCreateError() { - showMessage(R.string.settings_file_create_error) + showMessage(resourceRepo.getString(R.string.settings_file_create_error)) } - private fun showMessage(stringResId: Int) { - val params = SnackBarParams(message = resourceRepo.getString(stringResId)) + private fun showMessage(string: String) { + val params = SnackBarParams(message = string) router.show(params) } @@ -466,9 +484,12 @@ class BackupViewModel @Inject constructor( private const val CSV_EXPORT_DIALOG_TAG = "csv_export_dialog_tag" private const val ICS_EXPORT_DIALOG_TAG = "ics_export_dialog_tag" private const val ALERT_DIALOG_TAG = "alert_dialog_tag" + private const val CSV_IMPORT_ALERT_DIALOG_TAG = "csv_import_alert_dialog_tag" private const val FILE_TYPE_BIN = "application/x-binary" + private const val FILE_TYPE_BIN_OPEN = "application/*" private const val FILE_TYPE_CSV = "text/csv" + private const val FILE_TYPE_CSV_OPEN = "text/*" private const val FILE_TYPE_ICS = "application/ics" } } \ No newline at end of file diff --git a/core/src/main/java/com/example/util/simpletimetracker/core/utils/TestUtils.kt b/core/src/main/java/com/example/util/simpletimetracker/core/utils/TestUtils.kt index 913c7d09..dd174b35 100644 --- a/core/src/main/java/com/example/util/simpletimetracker/core/utils/TestUtils.kt +++ b/core/src/main/java/com/example/util/simpletimetracker/core/utils/TestUtils.kt @@ -5,10 +5,10 @@ import com.example.util.simpletimetracker.core.mapper.IconImageMapper import com.example.util.simpletimetracker.domain.extension.orZero import com.example.util.simpletimetracker.domain.interactor.ActivityFilterInteractor import com.example.util.simpletimetracker.domain.interactor.CategoryInteractor +import com.example.util.simpletimetracker.domain.interactor.ClearDataInteractor import com.example.util.simpletimetracker.domain.interactor.PrefsInteractor import com.example.util.simpletimetracker.domain.interactor.RecordInteractor import com.example.util.simpletimetracker.domain.interactor.RecordTagInteractor -import com.example.util.simpletimetracker.domain.interactor.RecordToRecordTagInteractor import com.example.util.simpletimetracker.domain.interactor.RecordTypeCategoryInteractor import com.example.util.simpletimetracker.domain.interactor.RecordTypeInteractor import com.example.util.simpletimetracker.domain.interactor.RunningRecordInteractor @@ -20,10 +20,9 @@ import com.example.util.simpletimetracker.domain.model.Record import com.example.util.simpletimetracker.domain.model.RecordTag import com.example.util.simpletimetracker.domain.model.RecordType import com.example.util.simpletimetracker.domain.model.RunningRecord -import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo -import kotlinx.coroutines.runBlocking import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlinx.coroutines.runBlocking class TestUtils @Inject constructor( private val recordTypeInteractor: RecordTypeInteractor, @@ -32,23 +31,14 @@ class TestUtils @Inject constructor( private val categoryInteractor: CategoryInteractor, private val recordTypeCategoryInteractor: RecordTypeCategoryInteractor, private val recordTagInteractor: RecordTagInteractor, - private val recordToRecordTagInteractor: RecordToRecordTagInteractor, private val activityFilterInteractor: ActivityFilterInteractor, - private val runningRecordToRecordTagRepo: RunningRecordToRecordTagRepo, private val prefsInteractor: PrefsInteractor, private val iconImageMapper: IconImageMapper, + private val clearDataInteractor: ClearDataInteractor, ) { fun clearDatabase() = runBlocking { - recordTypeInteractor.clear() - recordInteractor.clear() - runningRecordInteractor.clear() - categoryInteractor.clear() - recordTypeCategoryInteractor.clear() - recordTagInteractor.clear() - recordToRecordTagInteractor.clear() - runningRecordToRecordTagRepo.clear() // TODO what's weird, can't use interactor because of test crash - activityFilterInteractor.clear() + clearDataInteractor.execute() } fun clearPrefs() = runBlocking { diff --git a/core/src/main/res/values-ca/strings.xml b/core/src/main/res/values-ca/strings.xml index fa975b8a..8e3faadf 100644 --- a/core/src/main/res/values-ca/strings.xml +++ b/core/src/main/res/values-ca/strings.xml @@ -291,6 +291,20 @@ Si la configuració \"%s\" és habilitada, l\'aplicació enviarà esdeven Voleu restaurar la còpia de seguretat? Exporta a full de càlcul Exporteu els enregistraments de l\'activitat com un fitxer csv per a veure\'ls al programa de fulls de càlcul preferit. + Importació de fulls de càlcul + Importa registres d\'activitat des d\'un fitxer csv. Les dades actuals no es perdran. + +
+- Nom de l\'activitat entre cometes.
+- Hora d\'inici de l\'enregistrament en el format \"aaaa-MM-dd HH:mm:ss\"
+- Hora de finalització del registre en el format \"aaaa-MM-dd HH:mm:ss\"
+- Comenta entre cometes.
+
+Exemple:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Exportació ICS Aquest arxiu no es pot fer servir com a còpia de seguretat. Hi ha un error en obrir el fitxer @@ -303,6 +317,9 @@ Si la configuració \"%s\" és habilitada, l\'aplicació enviarà esdeven No es troba l\'aplicació Exportació complerta Error d\'exportació + Importació completada + Registres afegits: %s + Error d\'importació Hi ha hagut un error amb la còpia de seguretat automàtica. Hi ha hagut un error amb l\'exportació automàtica. Proveu de seleccionar una ubicació diferent diff --git a/core/src/main/res/values-de/strings.xml b/core/src/main/res/values-de/strings.xml index 1dd0263a..c8f0cdcc 100644 --- a/core/src/main/res/values-de/strings.xml +++ b/core/src/main/res/values-de/strings.xml @@ -291,6 +291,20 @@ Wenn die Einstellung \"%s\" aktiviert ist, sendet die App Ereignisse zu g Backup wiederherstellen? Tabellenkalkulation exportieren Exportiere die Aktivitätsdaten als csv-Datei, um sie in einer bevorzugten Tabellenkalkulationsanwendung anzuzeigen. + Tabellenimport + Aktivitätsdatensätze aus einer CSV-Datei importieren. Aktuelle Daten gehen nicht verloren. + +
+- Aktivitätsname in Anführungszeichen.
+– Startzeit der Aufzeichnung im Format „jjjj-MM-tt HH:mm:ss“
+– Endzeit der Aufzeichnung im Format „jjjj-MM-tt HH:mm:ss“
+- Comment in quotes.
+
+Beispiel:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS-Export Diese Datei kann nicht als Backup verwendet werden. Fehler beim Öffnen der Datei @@ -303,6 +317,9 @@ Wenn die Einstellung \"%s\" aktiviert ist, sendet die App Ereignisse zu g App nicht gefunden Export beendet Exportfehler + Import abgeschlossen + Hinzugefügte Datensätze: %s + Importfehler Bei der automatischen Sicherung ist ein Fehler aufgetreten. Beim automatischen Export ist ein Fehler aufgetreten. Versuchen Sie, einen anderen Ort auszuwählen diff --git a/core/src/main/res/values-es/strings.xml b/core/src/main/res/values-es/strings.xml index 4f87cfd4..c0a25762 100644 --- a/core/src/main/res/values-es/strings.xml +++ b/core/src/main/res/values-es/strings.xml @@ -291,6 +291,20 @@ Si la configuración \"%s\" está habilitada, la aplicación enviará eve ¿Desea restaurar la copia de seguridad? Exportar hoja de cálculo Exportar registros como un archivo csv para visualizar los datos en su programa de hojas de cálculo preferido. + Importación de hoja de cálculo + Importar registros de actividad desde un archivo csv. Los datos actuales no se perderán. + +
+- Nombre de la actividad entre comillas.
+- Hora de inicio del registro en el formato \"yyyy-MM-dd HH:mm:ss\"
+- Hora de finalización del registro en el formato \"yyyy-MM-dd HH:mm:ss\"
+- Comentar entre comillas.
+
+Ejemplo:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Exportación ICS Este archivo no se puede utilizar como copia de seguridad. Error abriendo el archivo @@ -303,6 +317,9 @@ Si la configuración \"%s\" está habilitada, la aplicación enviará eve No se encontró la apliación Exportación completada Error de exportación + Importación completada + Registros agregados: %s + Error de importación Hubo un error con la copia de seguridad automática. Hubo un error con la exportación automática. Intenta seleccionar una ubicación diferente diff --git a/core/src/main/res/values-fr/strings.xml b/core/src/main/res/values-fr/strings.xml index 08eb319e..e0546909 100644 --- a/core/src/main/res/values-fr/strings.xml +++ b/core/src/main/res/values-fr/strings.xml @@ -291,6 +291,20 @@ Si le paramètre \"%s\" est activé, l\'application enverra des événeme Restaurer la sauvegarde ? Exporter dans une feuille de calcul Exportez les enregistrements d\'activité sous la forme de fichier CSV pour les afficher dans le tableur de votre choix. + Importation de feuille de calcul + Importez des enregistrements d\'activité à partir d\'un fichier csv. Les données actuelles ne seront pas perdues. + +
+- Nom de l\'activité entre guillemets.
+- Heure de début de l\'enregistrement au format \"aaaa-MM-jj HH:mm:ss\"
+- Heure de fin d\'enregistrement au format \"aaaa-MM-jj HH:mm:ss\"
+- Commentaire entre guillemets.
+
+Exemple:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Exportation ICS Ce fichier ne peut pas être utilisé comme sauvegarde. Erreur lors de l\'ouverture du fichier @@ -303,6 +317,9 @@ Si le paramètre \"%s\" est activé, l\'application enverra des événeme Application non trouvée Exportation terminée Erreur d\'exportation + Importation terminée + Enregistrements ajoutés: %s + Erreur d\'importation Une erreur s\'est produite lors de la sauvegarde automatique. Une erreur s\'est produite lors de l\'exportation automatique. Essayez de sélectionner un emplacement différent diff --git a/core/src/main/res/values-hi/strings.xml b/core/src/main/res/values-hi/strings.xml index 3631d2ce..97ba4e61 100644 --- a/core/src/main/res/values-hi/strings.xml +++ b/core/src/main/res/values-hi/strings.xml @@ -291,6 +291,20 @@ पुनर्स्थापित करे? स्प्रेडशीट निर्यात एक पसंदीदा स्प्रेडशीट एप्लिकेशन में देखने के लिए CSV फ़ाइल के रूप में निर्यात गतिविधि रिकॉर्ड। + स्प्रेडशीट आयात + CSV फ़ाइल से गतिविधि रिकॉर्ड आयात करें। वर्तमान डेटा नष्ट नहीं होगा। + +
+- गतिविधि का नाम उद्धरणों में।
+- रिकॉर्ड का प्रारंभ समय \"yyyy-MM-dd HH:mm:ss\"
प्रारूप में +- रिकॉर्ड का समाप्ति समय \"yyyy-MM-dd HH:mm:ss\"
प्रारूप में +- उद्धरणों में टिप्पणी करें।
+
+उदाहरण:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS निर्यात इस फ़ाइल का उपयोग बैकअप के रूप में नहीं किया जा सकता है। त्रुटि खोलने की फ़ाइल @@ -303,6 +317,9 @@ आवेदन नहीं मिला निर्यात पूरा किया गया निर्यात त्रुटि + आयात पूरा हुआ + रिकॉर्ड जोड़े गए: %s + आयात त्रुटि स्वचालित बैकअप के साथ एक त्रुटि हुई थी। स्वचालित निर्यात में त्रुटि हुई थी। अलग स्थान का चयन करने का प्रयास करें diff --git a/core/src/main/res/values-in/strings.xml b/core/src/main/res/values-in/strings.xml index dd6be0ab..8adc1132 100644 --- a/core/src/main/res/values-in/strings.xml +++ b/core/src/main/res/values-in/strings.xml @@ -291,6 +291,20 @@ Jika setelan \"%s\" diaktifkan, aplikasi akan mengirimkan peristiwa pada Pulihkan cadangan? Ekspor lembar kerja Ekspor rekor-rekor kegiatan sebagai sebuah berkas csv untuk melihatnya dalam aplikasi spreadsheet yang diinginkan. + Impor spreadsheet + Impor rekaman aktivitas dari file csv. Data saat ini tidak akan hilang. + +
+- Nama aktivitas dalam tanda kutip.
+- Waktu mulai pencatatan dalam format \"yyyy-MM-dd HH:mm:ss\"
+- Waktu akhir pencatatan dalam format \"yyyy-MM-dd HH:mm:ss\"
+- Beri komentar dalam tanda kutip.
+
+Contoh:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Ekspor ICS File ini tidak dapat digunakan sebagai cadangan. Galat membuka berkas @@ -303,6 +317,9 @@ Jika setelan \"%s\" diaktifkan, aplikasi akan mengirimkan peristiwa pada Aplikasi tidak ditemukan Ekspor selesai Kesalahan ekspor + Impor selesai + Catatan ditambahkan: %s + Kesalahan impor Terjadi kesalahan dengan pencadangan otomatis. Terjadi kesalahan dengan ekspor otomatis. Cobalah untuk memilih lokasi yang berbeda diff --git a/core/src/main/res/values-it/strings.xml b/core/src/main/res/values-it/strings.xml index 7a03874a..072429b9 100644 --- a/core/src/main/res/values-it/strings.xml +++ b/core/src/main/res/values-it/strings.xml @@ -291,6 +291,20 @@ Se l\'impostazione \"%s\" è abilitata, l\'app invierà eventi sull\'atti Ripristina backup? Esporta foglio di calcolo Esporta le registrazioni dell\'attività come file csv da visualizzare con l\'applicazione predefinita per i fogli di calcolo. + Importazione da foglio di calcolo + Importa i record delle attività da un file csv. I dati correnti non andranno persi. + +
+- Nome dell\'attività tra virgolette.
+- Ora di inizio della registrazione nel formato \"aaaa-MM-gg HH:mm:ss\"
+- Ora di fine registrazione nel formato \"aaaa-MM-gg HH:mm:ss\"
+- Commento tra virgolette.
+
+Esempio:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Esportazione ICS Questo file non può essere utilizzato come backup. Errore nell\'apertura del file @@ -303,6 +317,9 @@ Se l\'impostazione \"%s\" è abilitata, l\'app invierà eventi sull\'atti Applicazione non trovata Esportazione completata Errore di esportazione + Importazione completata + Record aggiunti: %s + Errore di importazione Si è verificato un errore con il backup automatico. Si è verificato un errore con l\'esportazione automatica. Prova a selezionare una posizione diversa diff --git a/core/src/main/res/values-ja/strings.xml b/core/src/main/res/values-ja/strings.xml index 18eb2fbb..e2b34dee 100644 --- a/core/src/main/res/values-ja/strings.xml +++ b/core/src/main/res/values-ja/strings.xml @@ -291,6 +291,20 @@ バックアップを復元しますか? スプレッドシートのエクスポート アクティビティ記録をcsvファイルとしてエクスポートし、お好みのスプレッドシートアプリケーションで表示することができます。 + スプレッドシートのインポート + CSV ファイルからアクティビティ記録をインポートします。現在のデータは失われません。 + +
+- 引用符で囲んだアクティビティ名。
+- 「yyyy-MM-dd HH:mm:ss」形式の記録開始時刻
+- 「yyyy-MM-dd HH:mm:ss」形式の記録の終了時刻
+- 引用符で囲んだコメント。
+
+例:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS エクスポート このファイルはバックアップとして使用できません。 ファイルを開く際にエラーが発生しました @@ -303,6 +317,9 @@ アプリケーションが見つかりません エクスポート完了 エクスポートエラー + インポートが完了しました + 追加されたレコード: %s + インポートエラー 自動バックアップでエラーが発生しました。 自動エクスポートでエラーが発生しました。 別の場所を選択してみてください diff --git a/core/src/main/res/values-nl/strings.xml b/core/src/main/res/values-nl/strings.xml index 86d82d3c..80dc85ca 100644 --- a/core/src/main/res/values-nl/strings.xml +++ b/core/src/main/res/values-nl/strings.xml @@ -291,6 +291,20 @@ Als de instelling \"%s\" is ingeschakeld, stuurt de app gebeurtenissen ov Back-up terugzetten? Spreadsheet exporteren Exporteer activiteitenrecords als een csv-bestand om ze te bekijken in een voorkeurspreadsheet-applicatie. + Spreadsheets importeren + Activiteitenrecords importeren vanuit een csv-bestand. Huidige gegevens gaan niet verloren. + +
+- Activiteitsnaam tussen aanhalingstekens.
+- Starttijd van opname in het formaat \"jjjj-MM-dd UU:mm:ss\"
+- Eindtijd van opname in het formaat \"jjjj-MM-dd UU:mm:ss\"
+- Reageer tussen aanhalingstekens.
+
+Voorbeeld:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS-export Dit bestand kan niet als back-up worden gebruikt. Fout bij openen bestand @@ -303,6 +317,9 @@ Als de instelling \"%s\" is ingeschakeld, stuurt de app gebeurtenissen ov Applicatie niet gevonden Export voltooid Exportfout + Importeren voltooid + Records toegevoegd: %s + Fout importeren Er is een fout opgetreden bij de automatische back-up. Er is een fout opgetreden bij het automatisch exporteren. Probeer een andere locatie te selecteren diff --git a/core/src/main/res/values-pt/strings.xml b/core/src/main/res/values-pt/strings.xml index 458e1726..9f882b33 100644 --- a/core/src/main/res/values-pt/strings.xml +++ b/core/src/main/res/values-pt/strings.xml @@ -291,6 +291,20 @@ Se a configuração \"%s\" estiver habilitada, o aplicativo enviará even Restaurar backup? Exportação de planilha Exporte registros de atividades como um arquivo csv para visualizar no aplicativo de planilhas de sua preferência. + Importação de planilha + Importe registros de atividades de um arquivo csv. Os dados atuais não serão perdidos. + +
+- Nome da atividade entre aspas.
+- Hora de início da gravação no formato \"aaaa-MM-dd HH:mm:ss\"
+- Hora de término do registro no formato \"aaaa-MM-dd HH:mm:ss\"
+- Comente entre aspas.
+
+Exemplo:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
Exportação de ICS Este arquivo não pode ser usado como backup. Erro ao abrir o arquivo @@ -303,6 +317,9 @@ Se a configuração \"%s\" estiver habilitada, o aplicativo enviará even Aplicação não encontrada Exportação concluída Erro de exportação + Importação concluída + Registros adicionados: %s + Erro de importação Ocorreu um erro com o backup automático. Ocorreu um erro com a exportação automática. Tente selecionar um local diferente diff --git a/core/src/main/res/values-ru/strings.xml b/core/src/main/res/values-ru/strings.xml index 2be2f732..d9a8d1ce 100644 --- a/core/src/main/res/values-ru/strings.xml +++ b/core/src/main/res/values-ru/strings.xml @@ -291,6 +291,20 @@ Восстановить из резервной копии? Экспорт таблицы Экспортируйте записи в виде файла CSV для просмотра в предпочтительном приложении для работы с электронными таблицами. + Импорт таблицы + Импорт записей об активности из CSV-файла. Текущие данные не будут потеряны. + +
+- Название действия в кавычках.
+- Время начала записи в формате \"гггг-ММ-дд ЧЧ:мм:сс\"
+- Время окончания записи в формате \"гггг-ММ-дд ЧЧ:мм:сс\"
+- Комментарий в кавычках.
+
+Пример:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS экспорт Этот файл не может быть использован в качестве резервной копии. Ошибка при открытии файла @@ -303,6 +317,9 @@ Приложение не найдено Экспорт завершен Ошибка экспорта + Импорт завершен + Добавлено записей: %s + Ошибка импорта Произошла ошибка с автоматическим резервным копированием. Произошла ошибка при автоматическом экспорте. Попробуйте выбрать другое место diff --git a/core/src/main/res/values-sv/strings.xml b/core/src/main/res/values-sv/strings.xml index 44df8bb9..3e0dcabf 100644 --- a/core/src/main/res/values-sv/strings.xml +++ b/core/src/main/res/values-sv/strings.xml @@ -291,6 +291,20 @@ Om inställningen \"%s\" är aktiverad, kommer appen att skicka händelse Återställa säkerhetskopia? Exportera kalkyblad Exportera händelser som en csv-fil för att öppna med kalkylbladsprogram. + Importera kalkylark + Importera aktivitetsposter från en csv-fil. Aktuell data kommer inte att gå förlorad. + +
+- Aktivitetsnamn inom citattecken.
+- Starttid för registrering i formatet \"åååå-MM-dd HH:mm:ss\"
+- Sluttid för posten i formatet \"åååå-MM-dd HH:mm:ss\"
+- Kommentera inom citattecken.
+
+Exempel:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS export Den här filen kan inte användas som säkerhetskopia. Fel vid öppnandet av fil @@ -303,6 +317,9 @@ Om inställningen \"%s\" är aktiverad, kommer appen att skicka händelse Applikation ej hittad Export slutförd Exportfel + Importen slutförd + Poster tillagda: %s + Importfel Det uppstod ett fel med automatisk säkerhetskopiering. Det uppstod ett fel med automatisk export. Försök att välja en annan plats diff --git a/core/src/main/res/values-tr/strings.xml b/core/src/main/res/values-tr/strings.xml index 0958b906..4e58de67 100644 --- a/core/src/main/res/values-tr/strings.xml +++ b/core/src/main/res/values-tr/strings.xml @@ -291,6 +291,20 @@ Ek olarak:
Yedeklemeyi geri yükle? Çizelge dışa aktar Aktivite kayıtlarını, tercih edilen çizelge uygulamasında görüntülemek için csv dosyası olarak dışa aktar. + E-tablo içe aktarma + Etkinlik kayıtlarını bir csv dosyasından içe aktarın. Mevcut veriler kaybolmaz. + +
+- Tırnak içinde etkinlik adı.
+- \"yyyy-AA-gg SS:dd:ss\"
biçimindeki kaydın başlangıç zamanı +- \"yyyy-AA-gg SS:dd:ss\"
biçimindeki kaydın bitiş zamanı +- Tırnak içinde yorum yapın.
+
+Örnek:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS dışa aktarma Bu dosya yedek olarak kullanılamaz. Dosyayı açarken hata oluştu @@ -303,6 +317,9 @@ Ek olarak:
Uygulama bulunumadı Dışa aktarma tamamlandı Dışa aktarma hatası + İçe aktarma tamamlandı + Eklenen kayıtlar: %s + Alma hatası Otomatik yedeklemede bir hata oluştu. Otomatik dışa aktarmayla ilgili bir hata oluştu. Farklı bir konum seçmeyi deneyin diff --git a/core/src/main/res/values-uk/strings.xml b/core/src/main/res/values-uk/strings.xml index 53e66fda..5fe302b4 100644 --- a/core/src/main/res/values-uk/strings.xml +++ b/core/src/main/res/values-uk/strings.xml @@ -291,6 +291,20 @@ Відновити резервну копію? Експорт електронних таблиць Експортуйте записи діяльності як файл CSV для перегляду у бажаній програмі для перегляду електронних таблиць. + Імпорт таблиці + Імпортуйте записи про дії з файлу CSV. Поточні дані не будуть втрачені. + +
+- Назва діяльності в лапках.
+- Час початку запису у форматі \"рррр-ММ-дд ГГ:хх:сс\"
+- Час завершення запису у форматі \"рррр-ММ-дд ГГ:хх:сс\"
+- Коментар у лапках.
+
+Приклад:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS експорт Цей файл не можна використовувати як резервну копію. Помилка відкриття файлу @@ -303,6 +317,9 @@ Додаток не знайдено Експорт завершено Помилка експорту + Імпорт завершено + Додано записів: %s + Помилка імпорту Під час автоматичного резервного копіювання сталася помилка. Під час автоматичного експорту сталася помилка. Спробуйте вибрати інше місце diff --git a/core/src/main/res/values-zh-rTW/strings.xml b/core/src/main/res/values-zh-rTW/strings.xml index 07d893b9..b7abb313 100644 --- a/core/src/main/res/values-zh-rTW/strings.xml +++ b/core/src/main/res/values-zh-rTW/strings.xml @@ -291,6 +291,20 @@ 還原備份? 匯出工作簿 將活動記錄導出為 csv 文件,以便在首選電子表格應用程序中查看。 + 電子表格導入 + 從 csv 文件導入活動記錄。當前數據不會丟失。 + +
+- 引號中的活動名稱。
+- 記錄開始時間格式為\"yyyy-MM-dd HH:mm:ss\"
+- 記錄結束時間格式為\"yyyy-MM-dd HH:mm:ss\"
+- 評論用引號。
+
+示例:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
匯出 ICS 此檔案不能用在備份上 開啟檔案錯誤 @@ -303,6 +317,9 @@ 應用未找到 已匯出成功 匯出錯誤 + 導入完成 + 添加的記錄:%s + 導入錯誤 自動備份出錯。 自動導出時出錯。 嘗試選擇不同的位置 diff --git a/core/src/main/res/values-zh/strings.xml b/core/src/main/res/values-zh/strings.xml index 69be334f..818b2650 100644 --- a/core/src/main/res/values-zh/strings.xml +++ b/core/src/main/res/values-zh/strings.xml @@ -291,6 +291,20 @@ 恢复备份? 导出为电子表格 将活动记录导出为csv文件以在电子表格应用程序中查看。 + 电子表格导入 + 从 csv 文件导入活动记录。当前数据不会丢失。 + +
+- 引号中的活动名称。
+- 记录开始时间格式为\"yyyy-MM-dd HH:mm:ss\"
+- 记录结束时间格式为\"yyyy-MM-dd HH:mm:ss\"
+- 评论用引号。
+
+示例:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS 出口 此文件不能用作备份。 打开文件时出错 @@ -303,6 +317,9 @@ 找不到应用程序 导出已完成 导出错误 + 导入完成 + 添加的记录:%s + 导入错误 自动备份出错。 自动导出时出错。 尝试选择不同的位置 diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index 2a446f65..eb7f77b6 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -291,6 +291,20 @@ If \"%s\" setting is enabled, app will send events on activity started or Restore backup? Spreadsheet export Export activity records as a csv file to view in a preferred spreadsheet application. + Spreadsheet import + Import activity records from a csv file. Current data will not be lost. + +
+- Activity name in quotes.
+- Start time of record in the format \"yyyy-MM-dd HH:mm:ss\"
+- End time of record in the format \"yyyy-MM-dd HH:mm:ss\"
+- Comment in quotes.
+
+Example:
+
+\"Reading\",2023-05-30 14:45:21,2023-05-30 15:45:21,\"Comment, comment\" + ]]>
ICS export This file cannot be used as a backup. Error opening file @@ -303,6 +317,9 @@ If \"%s\" setting is enabled, app will send events on activity started or Application not found Export completed Export error + Import completed + Records added: %s + Import error There was an error with automatic backup. There was an error with automatic export. Try to select different location diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/BackupRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/BackupRepoImpl.kt index f562ebc3..91cdfc25 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/BackupRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/BackupRepoImpl.kt @@ -3,7 +3,10 @@ package com.example.util.simpletimetracker.data_local.resolver import android.content.ContentResolver import android.net.Uri import android.os.ParcelFileDescriptor +import com.example.util.simpletimetracker.core.R +import com.example.util.simpletimetracker.core.repo.ResourceRepo import com.example.util.simpletimetracker.domain.extension.orZero +import com.example.util.simpletimetracker.domain.interactor.ClearDataInteractor import com.example.util.simpletimetracker.domain.model.ActivityFilter import com.example.util.simpletimetracker.domain.model.AppColor import com.example.util.simpletimetracker.domain.model.Category @@ -19,13 +22,8 @@ import com.example.util.simpletimetracker.domain.repo.RecordTagRepo import com.example.util.simpletimetracker.domain.repo.RecordToRecordTagRepo import com.example.util.simpletimetracker.domain.repo.RecordTypeCategoryRepo import com.example.util.simpletimetracker.domain.repo.RecordTypeRepo -import com.example.util.simpletimetracker.domain.repo.RunningRecordRepo -import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo import com.example.util.simpletimetracker.domain.resolver.BackupRepo import com.example.util.simpletimetracker.domain.resolver.ResultCode -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber import java.io.BufferedReader import java.io.FileOutputStream import java.io.IOException @@ -33,6 +31,9 @@ import java.io.InputStream import java.io.InputStreamReader import javax.inject.Inject import javax.inject.Singleton +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber /** * Do not change backup parts order, always add new to the end. @@ -48,161 +49,156 @@ class BackupRepoImpl @Inject constructor( private val recordToRecordTagRepo: RecordToRecordTagRepo, private val recordTagRepo: RecordTagRepo, private val activityFilterRepo: ActivityFilterRepo, - private val runningRecordRepo: RunningRecordRepo, - private val runningRecordToRecordTagRepo: RunningRecordToRecordTagRepo, + private val clearDataInteractor: ClearDataInteractor, + private val resourceRepo: ResourceRepo, ) : BackupRepo { - override suspend fun saveBackupFile(uriString: String): ResultCode = - withContext(Dispatchers.IO) { - var fileDescriptor: ParcelFileDescriptor? = null - var fileOutputStream: FileOutputStream? = null + override suspend fun saveBackupFile( + uriString: String + ): ResultCode = withContext(Dispatchers.IO) { + var fileDescriptor: ParcelFileDescriptor? = null + var fileOutputStream: FileOutputStream? = null + + try { + val uri = Uri.parse(uriString) + fileDescriptor = contentResolver.openFileDescriptor(uri, "wt") + fileOutputStream = fileDescriptor?.fileDescriptor?.let(::FileOutputStream) + + // Write file identification + val identificationBackupRow: String = BACKUP_IDENTIFICATION + "\n" + fileOutputStream?.write(identificationBackupRow.toByteArray()) + + // Write data + recordTypeRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + recordRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + categoryRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + recordTypeCategoryRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + recordTagRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + recordToRecordTagRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + activityFilterRepo.getAll().forEach { + fileOutputStream?.write( + it.let(::toBackupString).toByteArray() + ) + } + fileOutputStream?.close() + fileDescriptor?.close() + ResultCode.Success(resourceRepo.getString(R.string.message_backup_saved)) + } catch (e: Exception) { + Timber.e(e) + ResultCode.Error(resourceRepo.getString(R.string.message_save_error)) + } finally { try { - val uri = Uri.parse(uriString) - fileDescriptor = contentResolver.openFileDescriptor(uri, "wt") - fileOutputStream = fileDescriptor?.fileDescriptor?.let(::FileOutputStream) - - // Write file identification - val identificationBackupRow: String = BACKUP_IDENTIFICATION + "\n" - fileOutputStream?.write(identificationBackupRow.toByteArray()) - - // Write data - recordTypeRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - recordRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - categoryRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - recordTypeCategoryRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - recordTagRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - recordToRecordTagRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - activityFilterRepo.getAll().forEach { - fileOutputStream?.write( - it.let(::toBackupString).toByteArray() - ) - } - fileOutputStream?.close() fileDescriptor?.close() - ResultCode.SUCCESS - } catch (e: Exception) { - Timber.e(e) - ResultCode.ERROR - } finally { - try { - fileOutputStream?.close() - fileDescriptor?.close() - } catch (e: IOException) { - // Do nothing - } + } catch (e: IOException) { + // Do nothing } } + } - override suspend fun restoreBackupFile(uriString: String): ResultCode = - withContext(Dispatchers.IO) { - var inputStream: InputStream? = null - var reader: BufferedReader? = null - - try { - val uri = Uri.parse(uriString) - inputStream = contentResolver.openInputStream(uri) - reader = inputStream?.let(::InputStreamReader)?.let(::BufferedReader) - - var line: String - var parts: List - - // Check file identification - line = reader?.readLine().orEmpty() - if (line != BACKUP_IDENTIFICATION) return@withContext ResultCode.ERROR - - recordTypeRepo.clear() - recordRepo.clear() - categoryRepo.clear() - recordTypeCategoryRepo.clear() - recordTagRepo.clear() - recordToRecordTagRepo.clear() - activityFilterRepo.clear() - runningRecordRepo.clear() - runningRecordToRecordTagRepo.clear() - - // Read data - while (reader?.readLine()?.also { line = it } != null) { - parts = line.split("\t") - when (parts[0]) { - ROW_RECORD_TYPE -> { - recordTypeFromBackupString(parts).let { - recordTypeRepo.add(it) - } + override suspend fun restoreBackupFile( + uriString: String + ): ResultCode = withContext(Dispatchers.IO) { + var inputStream: InputStream? = null + var reader: BufferedReader? = null + val errorCode = ResultCode.Error(resourceRepo.getString(R.string.message_restore_error)) + + try { + val uri = Uri.parse(uriString) + inputStream = contentResolver.openInputStream(uri) + reader = inputStream?.let(::InputStreamReader)?.let(::BufferedReader) + + var line: String + var parts: List + + // Check file identification + line = reader?.readLine().orEmpty() + if (line != BACKUP_IDENTIFICATION) return@withContext errorCode + + clearDataInteractor.execute() + + // Read data + while (reader?.readLine()?.also { line = it } != null) { + parts = line.split("\t") + when (parts[0]) { + ROW_RECORD_TYPE -> { + recordTypeFromBackupString(parts).let { + recordTypeRepo.add(it) } - ROW_RECORD -> { - recordFromBackupString(parts).let { (record, recordToRecordTag) -> - recordRepo.add(record) - if (recordToRecordTag != null) { - recordToRecordTagRepo.add(recordToRecordTag) - } + } + ROW_RECORD -> { + recordFromBackupString(parts).let { (record, recordToRecordTag) -> + recordRepo.add(record) + if (recordToRecordTag != null) { + recordToRecordTagRepo.add(recordToRecordTag) } } - ROW_CATEGORY -> { - categoryFromBackupString(parts).let { - categoryRepo.add(it) - } + } + ROW_CATEGORY -> { + categoryFromBackupString(parts).let { + categoryRepo.add(it) } - ROW_TYPE_CATEGORY -> { - typeCategoryFromBackupString(parts).let { - recordTypeCategoryRepo.add(it) - } + } + ROW_TYPE_CATEGORY -> { + typeCategoryFromBackupString(parts).let { + recordTypeCategoryRepo.add(it) } - ROW_RECORD_TAG -> { - recordTagFromBackupString(parts).let { - recordTagRepo.add(it) - } + } + ROW_RECORD_TAG -> { + recordTagFromBackupString(parts).let { + recordTagRepo.add(it) } - ROW_RECORD_TO_RECORD_TAG -> { - recordToRecordTagFromBackupString(parts).let { - recordToRecordTagRepo.add(it) - } + } + ROW_RECORD_TO_RECORD_TAG -> { + recordToRecordTagFromBackupString(parts).let { + recordToRecordTagRepo.add(it) } - ROW_ACTIVITY_FILTER -> { - activityFilterFromBackupString(parts).let { - activityFilterRepo.add(it) - } + } + ROW_ACTIVITY_FILTER -> { + activityFilterFromBackupString(parts).let { + activityFilterRepo.add(it) } } } - ResultCode.SUCCESS - } catch (e: Exception) { - Timber.e(e) - ResultCode.ERROR - } finally { - try { - inputStream?.close() - reader?.close() - } catch (e: IOException) { - // Do nothing - } + } + ResultCode.Success(resourceRepo.getString(R.string.message_backup_restored)) + } catch (e: Exception) { + Timber.e(e) + errorCode + } finally { + try { + inputStream?.close() + reader?.close() + } catch (e: IOException) { + // Do nothing } } + } private fun toBackupString(recordType: RecordType): String { return String.format( diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/CsvRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/CsvRepoImpl.kt index a8b8102c..e6373a1a 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/CsvRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/CsvRepoImpl.kt @@ -3,6 +3,10 @@ package com.example.util.simpletimetracker.data_local.resolver import android.content.ContentResolver import android.net.Uri import android.os.ParcelFileDescriptor +import com.example.util.simpletimetracker.core.R +import com.example.util.simpletimetracker.core.mapper.ColorMapper +import com.example.util.simpletimetracker.core.repo.ResourceRepo +import com.example.util.simpletimetracker.domain.model.AppColor import com.example.util.simpletimetracker.domain.model.Category import com.example.util.simpletimetracker.domain.model.Range import com.example.util.simpletimetracker.domain.model.Record @@ -14,15 +18,18 @@ import com.example.util.simpletimetracker.domain.repo.RecordTypeCategoryRepo import com.example.util.simpletimetracker.domain.repo.RecordTypeRepo import com.example.util.simpletimetracker.domain.resolver.CsvRepo import com.example.util.simpletimetracker.domain.resolver.ResultCode -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext -import timber.log.Timber +import java.io.BufferedReader import java.io.FileOutputStream import java.io.IOException +import java.io.InputStream +import java.io.InputStreamReader import java.text.SimpleDateFormat import java.util.Locale import java.util.concurrent.TimeUnit import javax.inject.Inject +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import timber.log.Timber class CsvRepoImpl @Inject constructor( private val contentResolver: ContentResolver, @@ -30,6 +37,7 @@ class CsvRepoImpl @Inject constructor( private val recordRepo: RecordRepo, private val recordTypeCategoryRepo: RecordTypeCategoryRepo, private val recordTagRepo: RecordTagRepo, + private val resourceRepo: ResourceRepo, ) : CsvRepo { override suspend fun saveCsvFile( @@ -74,10 +82,10 @@ class CsvRepoImpl @Inject constructor( fileOutputStream?.close() fileDescriptor?.close() - ResultCode.SUCCESS + ResultCode.Success(resourceRepo.getString(R.string.message_export_complete)) } catch (e: Exception) { Timber.e(e) - ResultCode.ERROR + ResultCode.Error(resourceRepo.getString(R.string.message_export_error)) } finally { try { fileOutputStream?.close() @@ -88,6 +96,90 @@ class CsvRepoImpl @Inject constructor( } } + override suspend fun importCsvFile( + uriString: String + ): ResultCode = withContext(Dispatchers.IO) { + var inputStream: InputStream? = null + var reader: BufferedReader? = null + + try { + val uri = Uri.parse(uriString) + inputStream = contentResolver.openInputStream(uri) + reader = inputStream?.let(::InputStreamReader)?.let(::BufferedReader) + + var line = "" + var addedRecords = 0L + val currentTypes = recordTypeRepo.getAll() + val newAddedTypes = mutableListOf() + + // Read data + while (reader?.readLine()?.also { line = it } != null) { + line = line.removePrefix("\"") + val typeName = line.substringBefore(delimiter = "\"", missingDelimiterValue = "") + line = line.removePrefix("$typeName\",") + + val timeStartedString = line.substringBefore(delimiter = ",", missingDelimiterValue = "") + val timeStarted = parseDateTime(timeStartedString) + line = line.removePrefix("$timeStartedString,") + + val timeEndedString = line.substringBefore(delimiter = ",", missingDelimiterValue = "") + val timeEnded = parseDateTime(timeEndedString) + line = line.removePrefix("$timeEndedString,") + + line = line.removePrefix("\"") + val comment = line.substringBefore(delimiter = "\"", missingDelimiterValue = "") + + if ( + typeName.isNotEmpty() && + timeStarted != null && + timeEnded != null + ) { + val typeId: Long = currentTypes.firstOrNull { it.name == typeName }?.id + ?: newAddedTypes.firstOrNull { it.name == typeName }?.id + ?: run { + val newType = RecordType( + name = typeName, + icon = "", + color = AppColor( + colorId = (0..ColorMapper.colorsNumber).random(), + colorInt = "", + ), + hidden = false, + goalTime = 0, + dailyGoalTime = 0, + weeklyGoalTime = 0, + monthlyGoalTime = 0, + ) + val newTypeId = recordTypeRepo.add(newType) + newType.copy(id = newTypeId).let(newAddedTypes::add) + newTypeId + } + val record = Record( + typeId = typeId, + timeStarted = timeStarted, + timeEnded = timeEnded, + comment = comment, + ) + recordRepo.add(record) + addedRecords++ + } + } + val messageText = resourceRepo.getString(R.string.message_import_complete) + val messageHint = resourceRepo.getString(R.string.message_import_complete_hint, addedRecords) + ResultCode.Success("$messageText\n$messageHint") + } catch (e: Exception) { + Timber.e(e) + ResultCode.Error(resourceRepo.getString(R.string.message_import_error)) + } finally { + try { + inputStream?.close() + reader?.close() + } catch (e: IOException) { + // Do nothing + } + } + } + private fun toCsvString( record: Record, recordType: RecordType?, @@ -136,6 +228,14 @@ class CsvRepoImpl @Inject constructor( return "$hr:$min:$sec" } + private fun parseDateTime(timeString: String): Long? { + return synchronized(dateTimeFormat) { + runCatching { + dateTimeFormat.parse(timeString) + }.getOrNull()?.time + } + } + companion object { private const val CSV_HEADER = "activity name,time started,time ended,comment,categories,record tags,duration,duration minutes\n" diff --git a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/IcsRepoImpl.kt b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/IcsRepoImpl.kt index da8efefa..53707147 100644 --- a/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/IcsRepoImpl.kt +++ b/data_local/src/main/java/com/example/util/simpletimetracker/data_local/resolver/IcsRepoImpl.kt @@ -91,10 +91,10 @@ class IcsRepoImpl @Inject constructor( fileOutputStream?.close() fileDescriptor?.close() - ResultCode.SUCCESS + ResultCode.Success(resourceRepo.getString(R.string.message_export_complete)) } catch (e: Exception) { Timber.e(e) - ResultCode.ERROR + ResultCode.Error(resourceRepo.getString(R.string.message_export_error)) } finally { try { fileOutputStream?.close() diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ActivityFilterInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ActivityFilterInteractor.kt index 28ad2dcb..3fc2c60d 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ActivityFilterInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ActivityFilterInteractor.kt @@ -29,8 +29,4 @@ class ActivityFilterInteractor @Inject constructor( suspend fun remove(id: Long) { activityFilterRepo.remove(id) } - - suspend fun clear() { - activityFilterRepo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CategoryInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CategoryInteractor.kt index 626d9e06..5a23651d 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CategoryInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CategoryInteractor.kt @@ -28,8 +28,4 @@ class CategoryInteractor @Inject constructor( categoryRepo.remove(id) recordTypeCategoryRepo.removeAll(id) } - - suspend fun clear() { - categoryRepo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ClearDataInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ClearDataInteractor.kt new file mode 100644 index 00000000..1214473f --- /dev/null +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/ClearDataInteractor.kt @@ -0,0 +1,37 @@ +package com.example.util.simpletimetracker.domain.interactor + +import com.example.util.simpletimetracker.domain.repo.ActivityFilterRepo +import com.example.util.simpletimetracker.domain.repo.CategoryRepo +import com.example.util.simpletimetracker.domain.repo.RecordRepo +import com.example.util.simpletimetracker.domain.repo.RecordTagRepo +import com.example.util.simpletimetracker.domain.repo.RecordToRecordTagRepo +import com.example.util.simpletimetracker.domain.repo.RecordTypeCategoryRepo +import com.example.util.simpletimetracker.domain.repo.RecordTypeRepo +import com.example.util.simpletimetracker.domain.repo.RunningRecordRepo +import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo +import javax.inject.Inject + +class ClearDataInteractor @Inject constructor( + private val recordTypeRepo: RecordTypeRepo, + private val recordRepo: RecordRepo, + private val categoryRepo: CategoryRepo, + private val recordTypeCategoryRepo: RecordTypeCategoryRepo, + private val recordToRecordTagRepo: RecordToRecordTagRepo, + private val recordTagRepo: RecordTagRepo, + private val activityFilterRepo: ActivityFilterRepo, + private val runningRecordRepo: RunningRecordRepo, + private val runningRecordToRecordTagRepo: RunningRecordToRecordTagRepo, +) { + + suspend fun execute() { + recordTypeRepo.clear() + recordRepo.clear() + categoryRepo.clear() + recordTypeCategoryRepo.clear() + recordTagRepo.clear() + recordToRecordTagRepo.clear() + activityFilterRepo.clear() + runningRecordRepo.clear() + runningRecordToRecordTagRepo.clear() + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CsvExportInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CsvExportInteractor.kt index f6ad752d..0c0ff3b0 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CsvExportInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/CsvExportInteractor.kt @@ -12,4 +12,8 @@ class CsvExportInteractor @Inject constructor( suspend fun saveCsvFile(uriString: String, range: Range?): ResultCode { return csvRepo.saveCsvFile(uriString = uriString, range = range) } + + suspend fun importCsvFile(uriString: String): ResultCode { + return csvRepo.importCsvFile(uriString) + } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordInteractor.kt index ac0d1972..f8bc299c 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordInteractor.kt @@ -81,8 +81,4 @@ class RecordInteractor @Inject constructor( recordToRecordTagRepo.removeAllByRecordId(id) recordRepo.remove(id) } - - suspend fun clear() { - recordRepo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTagInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTagInteractor.kt index f7c8a8a7..694d6cf0 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTagInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTagInteractor.kt @@ -67,10 +67,6 @@ class RecordTagInteractor @Inject constructor( repo.removeByType(typeId) } - suspend fun clear() { - repo.clear() - } - // TODO remove sort and sort when needed. private fun sort(items: List): List { return items.sortedBy { it.name.lowercase(Locale.getDefault()) } diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordToRecordTagInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordToRecordTagInteractor.kt index 7538e510..97555511 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordToRecordTagInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordToRecordTagInteractor.kt @@ -10,8 +10,4 @@ class RecordToRecordTagInteractor @Inject constructor( suspend fun getRecordIdsByTagId(tagId: Long): List { return repo.getRecordIdsByTagId(tagId) } - - suspend fun clear() { - repo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt index 44d15f84..8ca8d457 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeCategoryInteractor.kt @@ -39,8 +39,4 @@ class RecordTypeCategoryInteractor @Inject constructor( suspend fun removeAllByType(typeId: Long) { recordTypeCategoryRepo.removeAllByType(typeId) } - - suspend fun clear() { - recordTypeCategoryRepo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeInteractor.kt index 24626f04..12ac7d01 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RecordTypeInteractor.kt @@ -67,10 +67,6 @@ class RecordTypeInteractor @Inject constructor( recordTypeRepo.remove(id) } - suspend fun clear() { - recordTypeRepo.clear() - } - private suspend fun sort( cardOrder: CardOrder?, records: List, diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordInteractor.kt index 9b480b3b..bbc0ec3b 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordInteractor.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordInteractor.kt @@ -28,8 +28,4 @@ class RunningRecordInteractor @Inject constructor( runningRecordToRecordTagRepo.removeAllByRunningRecordId(id) runningRecordRepo.remove(id) } - - suspend fun clear() { - runningRecordRepo.clear() - } } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordToRecordTagInteractor.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordToRecordTagInteractor.kt deleted file mode 100644 index 815d0003..00000000 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/interactor/RunningRecordToRecordTagInteractor.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.example.util.simpletimetracker.domain.interactor - -import com.example.util.simpletimetracker.domain.repo.RunningRecordToRecordTagRepo -import javax.inject.Inject - -class RunningRecordToRecordTagInteractor @Inject constructor( - private val repo: RunningRecordToRecordTagRepo, -) { - - suspend fun clear() { - repo.clear() - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/CsvRepo.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/CsvRepo.kt index 32215075..b1940798 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/CsvRepo.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/CsvRepo.kt @@ -8,4 +8,8 @@ interface CsvRepo { uriString: String, range: Range?, ): ResultCode + + suspend fun importCsvFile( + uriString: String, + ): ResultCode } \ No newline at end of file diff --git a/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/ResultCode.kt b/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/ResultCode.kt index 8edd50ea..0c16fc0e 100644 --- a/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/ResultCode.kt +++ b/domain/src/main/java/com/example/util/simpletimetracker/domain/resolver/ResultCode.kt @@ -1,5 +1,8 @@ package com.example.util.simpletimetracker.domain.resolver -enum class ResultCode { - SUCCESS, ERROR +sealed interface ResultCode { + val message: String + + data class Success(override val message: String) : ResultCode + data class Error(override val message: String) : ResultCode } \ No newline at end of file diff --git a/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticBackup/interactor/AutomaticBackupInteractorImpl.kt b/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticBackup/interactor/AutomaticBackupInteractorImpl.kt index df7fb5f5..df4094cd 100644 --- a/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticBackup/interactor/AutomaticBackupInteractorImpl.kt +++ b/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticBackup/interactor/AutomaticBackupInteractorImpl.kt @@ -42,7 +42,7 @@ class AutomaticBackupInteractorImpl @Inject constructor( } val result = backupInteractor.saveBackupFile(uri) - if (result == ResultCode.SUCCESS) { + if (result is ResultCode.Success) { schedule() prefsInteractor.setAutomaticBackupLastSaveTime(System.currentTimeMillis()) } else { diff --git a/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticExport/interactor/AutomaticExportInteractorImpl.kt b/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticExport/interactor/AutomaticExportInteractorImpl.kt index 672990a2..4afbb8df 100644 --- a/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticExport/interactor/AutomaticExportInteractorImpl.kt +++ b/features/feature_notification/src/main/java/com/example/util/simpletimetracker/feature_notification/automaticExport/interactor/AutomaticExportInteractorImpl.kt @@ -42,7 +42,7 @@ class AutomaticExportInteractorImpl @Inject constructor( } val result = csvExportInteractor.saveCsvFile(uri, range = null) - if (result == ResultCode.SUCCESS) { + if (result is ResultCode.Success) { schedule() prefsInteractor.setAutomaticExportLastSaveTime(System.currentTimeMillis()) } else { diff --git a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/view/SettingsFragment.kt b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/view/SettingsFragment.kt index ab0e1dd1..7c7d5e57 100644 --- a/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/view/SettingsFragment.kt +++ b/features/feature_settings/src/main/java/com/example/util/simpletimetracker/feature_settings/view/SettingsFragment.kt @@ -126,6 +126,8 @@ class SettingsFragment : layoutSettingsSaveBackup.setOnClick(backupViewModel::onSaveClick) layoutSettingsRestoreBackup.setOnClick(backupViewModel::onRestoreClick) layoutSettingsExportCsv.setOnClick(backupViewModel::onExportCsvClick) + layoutSettingsImportCsv.setOnClick(backupViewModel::onImportCsvClick) + btnSettingsImportCsvHelp.setOnClick(backupViewModel::onImportCsvHelpClick) layoutSettingsExportIcs.setOnClick(backupViewModel::onExportIcsClick) checkboxSettingsAutomaticBackup.setOnClick(backupViewModel::onAutomaticBackupClick) checkboxSettingsAutomaticExport.setOnClick(backupViewModel::onAutomaticExportClick) diff --git a/features/feature_settings/src/main/res/layout/settings_backup_layout.xml b/features/feature_settings/src/main/res/layout/settings_backup_layout.xml index 1fa76dfa..42fba64f 100644 --- a/features/feature_settings/src/main/res/layout/settings_backup_layout.xml +++ b/features/feature_settings/src/main/res/layout/settings_backup_layout.xml @@ -220,6 +220,76 @@ android:layout_width="match_parent" android:layout_marginTop="12dp" /> + + + + + + + + + + + + + + + + + + Unit), ) : ActionParams \ No newline at end of file