mirror of
https://github.com/mihonapp/mihon.git
synced 2025-03-01 18:34:13 +01:00
Add option to export minimal library information to a CSV file (#1161)
This commit is contained in:
parent
8b28a9bcee
commit
fab8b17d99
@ -18,6 +18,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||||||
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
|
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
|
||||||
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
|
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
|
||||||
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
|
- Add private tracking support for Kitsu ([@MajorTanya](https://github.com/MajorTanya)) ([#1774](https://github.com/mihonapp/mihon/pull/1774))
|
||||||
|
- Add option to export minimal library information to a CSV file ([@Animeboynz](https://github.com/Animeboynz), [@AntsyLich](https://github.com/AntsyLich)) ([#1161](https://github.com/mihonapp/mihon/pull/1161))
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))
|
||||||
|
@ -7,7 +7,9 @@ import android.net.Uri
|
|||||||
import androidx.activity.compose.ManagedActivityResultLauncher
|
import androidx.activity.compose.ManagedActivityResultLauncher
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@ -15,6 +17,8 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
|
||||||
@ -22,12 +26,15 @@ import androidx.compose.material3.SegmentedButton
|
|||||||
import androidx.compose.material3.SegmentedButtonDefaults
|
import androidx.compose.material3.SegmentedButtonDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.ReadOnlyComposable
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
@ -45,10 +52,14 @@ import eu.kanade.presentation.util.relativeTimeSpanString
|
|||||||
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
import eu.kanade.tachiyomi.data.backup.create.BackupCreateJob
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
import eu.kanade.tachiyomi.data.backup.restore.BackupRestoreJob
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
|
import eu.kanade.tachiyomi.data.export.LibraryExporter
|
||||||
|
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
|
||||||
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
import eu.kanade.tachiyomi.util.system.DeviceUtil
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.storage.displayablePath
|
import tachiyomi.core.common.storage.displayablePath
|
||||||
@ -57,8 +68,11 @@ import tachiyomi.core.common.util.lang.withUIContext
|
|||||||
import tachiyomi.core.common.util.system.logcat
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.domain.backup.service.BackupPreferences
|
import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
import tachiyomi.domain.manga.interactor.GetFavorites
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
|
import tachiyomi.presentation.core.components.material.TextButton
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
@ -95,6 +109,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
|
|
||||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||||
getDataGroup(),
|
getDataGroup(),
|
||||||
|
getExportGroup(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,4 +327,141 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun getExportGroup(): Preference.PreferenceGroup {
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var exportOptions by remember {
|
||||||
|
mutableStateOf(
|
||||||
|
ExportOptions(
|
||||||
|
includeTitle = true,
|
||||||
|
includeAuthor = true,
|
||||||
|
includeArtist = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val getFavorites = remember { Injekt.get<GetFavorites>() }
|
||||||
|
var favorites by remember { mutableStateOf<List<Manga>>(emptyList()) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
favorites = getFavorites.await()
|
||||||
|
}
|
||||||
|
|
||||||
|
val saveFileLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.CreateDocument("text/csv"),
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
scope.launch {
|
||||||
|
LibraryExporter.exportToCsv(
|
||||||
|
context = context,
|
||||||
|
uri = it,
|
||||||
|
favorites = favorites,
|
||||||
|
options = exportOptions,
|
||||||
|
onExportComplete = {
|
||||||
|
scope.launch(Dispatchers.Main) {
|
||||||
|
context.toast(MR.strings.library_exported)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
ColumnSelectionDialog(
|
||||||
|
options = exportOptions,
|
||||||
|
onConfirm = { options ->
|
||||||
|
exportOptions = options
|
||||||
|
saveFileLauncher.launch("mihon_library.csv")
|
||||||
|
},
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Preference.PreferenceGroup(
|
||||||
|
title = stringResource(MR.strings.export),
|
||||||
|
preferenceItems = persistentListOf(
|
||||||
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
title = stringResource(MR.strings.library_list),
|
||||||
|
onClick = { showDialog = true },
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColumnSelectionDialog(
|
||||||
|
options: ExportOptions,
|
||||||
|
onConfirm: (ExportOptions) -> Unit,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
) {
|
||||||
|
var titleSelected by remember { mutableStateOf(options.includeTitle) }
|
||||||
|
var authorSelected by remember { mutableStateOf(options.includeAuthor) }
|
||||||
|
var artistSelected by remember { mutableStateOf(options.includeArtist) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(MR.strings.migration_dialog_what_to_include))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = titleSelected,
|
||||||
|
onCheckedChange = { checked ->
|
||||||
|
titleSelected = checked
|
||||||
|
if (!checked) {
|
||||||
|
authorSelected = false
|
||||||
|
artistSelected = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Text(text = stringResource(MR.strings.title))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = authorSelected,
|
||||||
|
onCheckedChange = { authorSelected = it },
|
||||||
|
enabled = titleSelected,
|
||||||
|
)
|
||||||
|
Text(text = stringResource(MR.strings.author))
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Checkbox(
|
||||||
|
checked = artistSelected,
|
||||||
|
onCheckedChange = { artistSelected = it },
|
||||||
|
enabled = titleSelected,
|
||||||
|
)
|
||||||
|
Text(text = stringResource(MR.strings.artist))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(
|
||||||
|
onClick = {
|
||||||
|
onConfirm(
|
||||||
|
ExportOptions(
|
||||||
|
includeTitle = titleSelected,
|
||||||
|
includeAuthor = authorSelected,
|
||||||
|
includeArtist = artistSelected,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
onDismissRequest()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(MR.strings.action_save))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(MR.strings.action_cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.export
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import tachiyomi.domain.manga.model.Manga
|
||||||
|
|
||||||
|
object LibraryExporter {
|
||||||
|
|
||||||
|
data class ExportOptions(
|
||||||
|
val includeTitle: Boolean,
|
||||||
|
val includeAuthor: Boolean,
|
||||||
|
val includeArtist: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun exportToCsv(
|
||||||
|
context: Context,
|
||||||
|
uri: Uri,
|
||||||
|
favorites: List<Manga>,
|
||||||
|
options: ExportOptions,
|
||||||
|
onExportComplete: () -> Unit,
|
||||||
|
) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
|
||||||
|
val csvData = generateCsvData(favorites, options)
|
||||||
|
outputStream.write(csvData.toByteArray())
|
||||||
|
}
|
||||||
|
onExportComplete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val escapeRequired = listOf("\r", "\n", "\"", ",")
|
||||||
|
|
||||||
|
private fun generateCsvData(favorites: List<Manga>, options: ExportOptions): String {
|
||||||
|
val columnSize = listOf(
|
||||||
|
options.includeTitle,
|
||||||
|
options.includeAuthor,
|
||||||
|
options.includeArtist,
|
||||||
|
)
|
||||||
|
.count { it }
|
||||||
|
|
||||||
|
val rows = buildList(favorites.size) {
|
||||||
|
favorites.forEach { manga ->
|
||||||
|
buildList(columnSize) {
|
||||||
|
if (options.includeTitle) add(manga.title)
|
||||||
|
if (options.includeAuthor) add(manga.author)
|
||||||
|
if (options.includeArtist) add(manga.artist)
|
||||||
|
}
|
||||||
|
.let(::add)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.joinToString("\r\n") { columns ->
|
||||||
|
columns.joinToString(",") columns@{ column ->
|
||||||
|
if (column.isNullOrBlank()) return@columns ""
|
||||||
|
if (escapeRequired.any { column.contains(it) }) {
|
||||||
|
column.replace("\"", "\"\"").let { "\"$it\"" }
|
||||||
|
} else {
|
||||||
|
column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -573,6 +573,9 @@
|
|||||||
<string name="cache_deleted">Cache cleared, %1$d files deleted</string>
|
<string name="cache_deleted">Cache cleared, %1$d files deleted</string>
|
||||||
<string name="cache_delete_error">Error occurred while clearing</string>
|
<string name="cache_delete_error">Error occurred while clearing</string>
|
||||||
<string name="pref_auto_clear_chapter_cache">Clear chapter cache on app launch</string>
|
<string name="pref_auto_clear_chapter_cache">Clear chapter cache on app launch</string>
|
||||||
|
<string name="export">Export</string>
|
||||||
|
<string name="library_list">Library List</string>
|
||||||
|
<string name="library_exported">Library Exported</string>
|
||||||
|
|
||||||
<!-- Sync section -->
|
<!-- Sync section -->
|
||||||
<string name="syncing_library">Syncing library</string>
|
<string name="syncing_library">Syncing library</string>
|
||||||
@ -688,6 +691,8 @@
|
|||||||
<string name="ongoing">Ongoing</string>
|
<string name="ongoing">Ongoing</string>
|
||||||
<string name="unknown">Unknown</string>
|
<string name="unknown">Unknown</string>
|
||||||
<string name="unknown_author">Unknown author</string>
|
<string name="unknown_author">Unknown author</string>
|
||||||
|
<string name="author">Author</string>
|
||||||
|
<string name="artist">Artist</string>
|
||||||
<!-- reserved for #6163 -->
|
<!-- reserved for #6163 -->
|
||||||
<string name="unknown_status">Unknown status</string>
|
<string name="unknown_status">Unknown status</string>
|
||||||
<string name="licensed">Licensed</string>
|
<string name="licensed">Licensed</string>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user