From fa56c0bf4c253ae84f5e5e88bf26b051eac4dead Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Tue, 27 Aug 2024 13:57:59 +1200 Subject: [PATCH] Applying Suggestions --- .../settings/screen/SettingsDataScreen.kt | 151 +++++++++- .../settings/screen/data/LibraryListScreen.kt | 275 ------------------ 2 files changed, 147 insertions(+), 279 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/presentation/more/settings/screen/data/LibraryListScreen.kt diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt index da43161d0..2c4816f87 100644 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/SettingsDataScreen.kt @@ -7,7 +7,9 @@ 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 @@ -15,6 +17,8 @@ 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 @@ -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 @@ -37,7 +44,6 @@ import cafe.adriel.voyager.navigator.currentOrThrow import com.hippo.unifile.UniFile import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen -import eu.kanade.presentation.more.settings.screen.data.LibraryListScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget @@ -50,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 @@ -58,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 @@ -317,16 +327,149 @@ object SettingsDataScreen : SearchableSettings { @Composable private fun getExportGroup(): Preference.PreferenceGroup { - val navigator = LocalNavigator.currentOrThrow + 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 -> + // Prepare CSV data + 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) + } + } + // Write CSV data to output stream + outputStream.write(csvData.toByteArray()) + outputStream.flush() + } + } + } + } + + 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 = "Export", preferenceItems = persistentListOf( Preference.PreferenceItem.TextPreference( - title = LibraryListScreen.TITLE, - onClick = { navigator.push(LibraryListScreen()) }, + title = "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 = "Select Fields") + }, + text = { + Column { + // Title checkbox + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = titleSelected, + onCheckedChange = { checked -> + titleSelected = checked + if (!checked) { + authorSelected = false + artistSelected = false + } + }, + ) + Text(text = stringResource(MR.strings.title)) + } + + // Author checkbox, disabled if Title is not selected + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = authorSelected, + onCheckedChange = { authorSelected = it }, + enabled = titleSelected, + ) + Text(text = "Author") + } + + // Artist checkbox, disabled if Title is not selected + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + Checkbox( + checked = artistSelected, + onCheckedChange = { artistSelected = it }, + enabled = titleSelected, + ) + Text(text = "Artist") + } + } + }, + confirmButton = { + TextButton( + onClick = { + onConfirm(titleSelected, authorSelected, artistSelected) + onDismissRequest() + }, + ) { + Text(text = "Save") + } + }, + dismissButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(MR.strings.action_cancel)) + } + }, + ) + } } diff --git a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/LibraryListScreen.kt b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/LibraryListScreen.kt deleted file mode 100644 index 52debd8e6..000000000 --- a/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/LibraryListScreen.kt +++ /dev/null @@ -1,275 +0,0 @@ -package eu.kanade.presentation.more.settings.screen.data - -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Save -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Checkbox -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.collectAsState -import androidx.compose.runtime.getValue -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.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import cafe.adriel.voyager.navigator.LocalNavigator -import cafe.adriel.voyager.navigator.currentOrThrow -import eu.kanade.presentation.components.AppBar -import eu.kanade.presentation.components.AppBarActions -import eu.kanade.presentation.manga.components.MangaCover -import eu.kanade.presentation.util.Screen -import kotlinx.collections.immutable.persistentListOf -import kotlinx.coroutines.flow.flow -import kotlinx.coroutines.launch -import tachiyomi.domain.manga.interactor.GetFavorites -import tachiyomi.domain.manga.model.Manga -import tachiyomi.i18n.MR -import tachiyomi.presentation.core.components.material.Scaffold -import tachiyomi.presentation.core.components.material.TextButton -import tachiyomi.presentation.core.components.material.padding -import tachiyomi.presentation.core.i18n.stringResource -import tachiyomi.presentation.core.screens.EmptyScreen -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get - -@Composable -fun BaseMangaListItem( - manga: Manga, - modifier: Modifier = Modifier, -) { - Row( - modifier = modifier - .height(56.dp) - .padding(horizontal = MaterialTheme.padding.medium), - verticalAlignment = Alignment.CenterVertically, - ) { - MangaCover.Square( - modifier = Modifier - .padding(vertical = MaterialTheme.padding.small) - .fillMaxHeight(), - data = manga, - ) - - Box(modifier = Modifier.weight(1f)) { - Text( - text = manga.title, - modifier = Modifier - .padding(start = MaterialTheme.padding.medium), - overflow = TextOverflow.Ellipsis, - maxLines = 1, - style = MaterialTheme.typography.bodyMedium, - ) - } - } -} - -class LibraryListScreen : Screen() { - - companion object { - const val TITLE = "Library List" - } - - private fun escapeCsvField(field: String): String { - return field.replace("\"", "\"\"").replace("\r\n", "\n").replace("\r", "\n") - } - - @Composable - override fun Content() { - val context = LocalContext.current - val navigator = LocalNavigator.currentOrThrow - val getFavorites: GetFavorites = Injekt.get() - - val favoritesFlow = remember { flow { emit(getFavorites.await()) } } - val favoritesState by favoritesFlow.collectAsState(emptyList()) - - var showDialog by remember { mutableStateOf(false) } - - // Declare the selection states - var titleSelected by remember { mutableStateOf(true) } - var authorSelected by remember { mutableStateOf(true) } - var artistSelected by remember { mutableStateOf(true) } - - val coroutineScope = rememberCoroutineScope() - - // Setup the activity result launcher to handle the file save - val saveFileLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.CreateDocument("text/csv"), - ) { uri -> - uri?.let { - coroutineScope.launch { - context.contentResolver.openOutputStream(uri)?.use { outputStream -> - // Prepare CSV data - 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) - } - } - // Write CSV data to output stream - outputStream.write(csvData.toByteArray()) - outputStream.flush() - } - } - } - } - - if (showDialog) { - ColumnSelectionDialog( - onDismissRequest = { showDialog = false }, - onConfirm = { selectedTitle, selectedAuthor, selectedArtist -> - titleSelected = selectedTitle - authorSelected = selectedAuthor - artistSelected = selectedArtist - - // Launch the save document intent - saveFileLauncher.launch("manga_list.csv") - showDialog = false - }, - isTitleSelected = titleSelected, - isAuthorSelected = authorSelected, - isArtistSelected = artistSelected, - ) - } - - Scaffold( - topBar = { - AppBar( - title = TITLE, - navigateUp = navigator::pop, - actions = { - AppBarActions( - persistentListOf( - AppBar.Action( - title = stringResource(MR.strings.action_copy_to_clipboard), - icon = Icons.Default.Save, - onClick = { showDialog = true }, - ), - ), - ) - }, - ) - }, - ) { contentPadding -> - - if (favoritesState.isEmpty()) { - EmptyScreen( - stringRes = MR.strings.empty_screen, - modifier = Modifier.padding(contentPadding), - ) - return@Scaffold - } - - LazyColumn( - modifier = Modifier - .padding(contentPadding) - .padding(8.dp), - ) { - items(favoritesState) { manga -> - BaseMangaListItem( - manga = manga, - ) - } - } - } - } -} - -@Composable -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 = "Select Fields") - }, - text = { - Column { - // Title checkbox - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = titleSelected, - onCheckedChange = { checked -> - titleSelected = checked - if (!checked) { - authorSelected = false - artistSelected = false - } - }, - ) - Text(text = stringResource(MR.strings.title)) - } - - // Author checkbox, disabled if Title is not selected - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = authorSelected, - onCheckedChange = { authorSelected = it }, - enabled = titleSelected, - ) - Text(text = "Author") - } - - // Artist checkbox, disabled if Title is not selected - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - Checkbox( - checked = artistSelected, - onCheckedChange = { artistSelected = it }, - enabled = titleSelected, - ) - Text(text = "Artist") - } - } - }, - confirmButton = { - TextButton( - onClick = { - onConfirm(titleSelected, authorSelected, artistSelected) - onDismissRequest() - }, - ) { - Text(text = "Save") - } - }, - dismissButton = { - TextButton(onClick = onDismissRequest) { - Text(text = stringResource(MR.strings.action_cancel)) - } - }, - ) -}