Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow Copying of Library List #1161

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import android.net.Uri
import androidx.activity.compose.ManagedActivityResultLauncher
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
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.IconButton
import androidx.compose.material3.MultiChoiceSegmentedButtonRow
Expand All @@ -23,11 +27,14 @@ import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
Expand All @@ -49,6 +56,8 @@ import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.persistentMapOf
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.launch
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
Expand All @@ -57,8 +66,10 @@ import tachiyomi.core.common.util.lang.withUIContext
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.backup.service.BackupPreferences
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.interactor.GetFavorites
import tachiyomi.domain.storage.service.StoragePreferences
import tachiyomi.i18n.MR
import tachiyomi.presentation.core.components.material.TextButton
import tachiyomi.presentation.core.i18n.stringResource
import tachiyomi.presentation.core.util.collectAsState
import uy.kohesive.injekt.Injekt
Expand Down Expand Up @@ -95,6 +106,7 @@ object SettingsDataScreen : SearchableSettings {

getBackupAndRestoreGroup(backupPreferences = backupPreferences),
getDataGroup(),
getExportGroup(),
)
}

Expand Down Expand Up @@ -312,4 +324,152 @@ object SettingsDataScreen : SearchableSettings {
),
)
}

@Composable
private fun getExportGroup(): Preference.PreferenceGroup {
var showDialog by remember { mutableStateOf(false) }
var titleSelected by remember { mutableStateOf(true) }
var authorSelected by remember { mutableStateOf(true) }
var artistSelected by remember { mutableStateOf(true) }

val context = LocalContext.current
val coroutineScope = rememberCoroutineScope()
val getFavorites: GetFavorites = Injekt.get()
val favoritesFlow = remember { flow { emit(getFavorites.await()) } }
val favoritesState by favoritesFlow.collectAsState(emptyList())

val saveFileLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.CreateDocument("text/csv"),
) { uri ->
uri?.let {
coroutineScope.launch {
context.contentResolver.openOutputStream(uri)?.use { outputStream ->
val csvData = buildString {
favoritesState.forEach { manga ->
val title = if (titleSelected) escapeCsvField(manga.title) else ""
val author = if (authorSelected) escapeCsvField(manga.author ?: "") else ""
val artist = if (artistSelected) escapeCsvField(manga.artist ?: "") else ""
val row = listOf(title, author, artist).filter {
it.isNotEmpty()
}.joinToString(",") { "\"$it\"" }
appendLine(row)
}
}
outputStream.write(csvData.toByteArray())
outputStream.flush()

context.toast(MR.strings.library_exported)
}
}
}
}

if (showDialog) {
ColumnSelectionDialog(
onDismissRequest = { showDialog = false },
onConfirm = { newTitleSelected, newAuthorSelected, newArtistSelected ->
titleSelected = newTitleSelected
authorSelected = newAuthorSelected
artistSelected = newArtistSelected
saveFileLauncher.launch("library_list.csv")
},
isTitleSelected = titleSelected,
isAuthorSelected = authorSelected,
isArtistSelected = artistSelected,
)
}

return Preference.PreferenceGroup(
title = stringResource(MR.strings.export),
preferenceItems = persistentListOf(
Preference.PreferenceItem.TextPreference(
title = stringResource(MR.strings.library_list),
onClick = { showDialog = true },
),
),
)
}

private fun escapeCsvField(field: String): String {
return field
.replace("\"", "\"\"")
.replace("\r\n", "\n")
.replace("\r", "\n")
}

@Composable
private fun ColumnSelectionDialog(
onDismissRequest: () -> Unit,
onConfirm: (Boolean, Boolean, Boolean) -> Unit,
isTitleSelected: Boolean,
isAuthorSelected: Boolean,
isArtistSelected: Boolean,
) {
var titleSelected by remember { mutableStateOf(isTitleSelected) }
var authorSelected by remember { mutableStateOf(isAuthorSelected) }
var artistSelected by remember { mutableStateOf(isArtistSelected) }

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(titleSelected, authorSelected, artistSelected)
onDismissRequest()
},
) {
Text(text = stringResource(MR.strings.action_save))
}
},
dismissButton = {
TextButton(onClick = onDismissRequest) {
Text(text = stringResource(MR.strings.action_cancel))
}
},
)
}
}
6 changes: 6 additions & 0 deletions i18n/src/commonMain/moko-resources/base/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,10 @@
<string name="cache_deleted">Cache cleared, %1$d files deleted</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="export">Export</string>
<string name="library_list">Library List</string>
<string name="library_exported">Library Exported</string>


<!-- Sync section -->
<string name="syncing_library">Syncing library</string>
Expand Down Expand Up @@ -668,6 +672,8 @@
<string name="ongoing">Ongoing</string>
<string name="unknown">Unknown</string>
<string name="unknown_author">Unknown author</string>
<string name="author">Author</string>
<string name="artist">Artist</string>
<!-- reserved for #6163 -->
<string name="unknown_status">Unknown status</string>
<string name="licensed">Licensed</string>
Expand Down