Add option to export minimal library information to a CSV file (#1161)
(cherry picked from commit fab8b17d99c44a08555b1f584c56d62a47737ca0) # Conflicts: # CHANGELOG.md # app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt
This commit is contained in:
parent
3e16adf961
commit
02ace23c38
@ -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
|
||||||
@ -16,6 +18,7 @@ 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.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
|
||||||
@ -24,6 +27,7 @@ import androidx.compose.material3.SegmentedButtonDefaults
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
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
|
||||||
@ -31,6 +35,7 @@ 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
|
||||||
@ -52,6 +57,8 @@ 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.cache.PagePreviewCache
|
import eu.kanade.tachiyomi.data.cache.PagePreviewCache
|
||||||
|
import eu.kanade.tachiyomi.data.export.LibraryExporter
|
||||||
|
import eu.kanade.tachiyomi.data.export.LibraryExporter.ExportOptions
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
import eu.kanade.tachiyomi.data.sync.SyncDataJob
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncManager
|
import eu.kanade.tachiyomi.data.sync.SyncManager
|
||||||
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
import eu.kanade.tachiyomi.data.sync.service.GoogleDriveService
|
||||||
@ -60,6 +67,7 @@ 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 kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
@ -69,9 +77,12 @@ 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.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
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
|
||||||
@ -111,6 +122,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
|
|
||||||
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
getBackupAndRestoreGroup(backupPreferences = backupPreferences),
|
||||||
getDataGroup(),
|
getDataGroup(),
|
||||||
|
getExportGroup(),
|
||||||
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
|
) + getSyncPreferences(syncPreferences = syncPreferences, syncService = syncService)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +367,144 @@ 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))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
//SY -->
|
||||||
@Composable
|
@Composable
|
||||||
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
private fun getSyncPreferences(syncPreferences: SyncPreferences, syncService: Int): List<Preference> {
|
||||||
return listOf(
|
return listOf(
|
||||||
@ -591,4 +741,5 @@ object SettingsDataScreen : SearchableSettings {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
// SY <--
|
||||||
}
|
}
|
||||||
|
@ -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