From c00193f8cad12d8b5bdfd035946c09c244a6ed51 Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Mon, 26 Aug 2024 16:17:35 +1200 Subject: [PATCH 1/7] Add Library List --- .../settings/screen/SettingsDataScreen.kt | 17 +++ .../settings/screen/data/LibraryListScreen.kt | 129 ++++++++++++++++++ 2 files changed, 146 insertions(+) create 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 ecfd2ec756..ca0cc1e5c6 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 @@ -39,6 +39,7 @@ import eu.kanade.presentation.more.settings.Preference import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo +import eu.kanade.presentation.more.settings.screen.data.LibraryDebugListScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString @@ -95,6 +96,7 @@ object SettingsDataScreen : SearchableSettings { getBackupAndRestoreGroup(backupPreferences = backupPreferences), getDataGroup(), + getExportGroup(), ) } @@ -312,4 +314,19 @@ object SettingsDataScreen : SearchableSettings { ), ) } + + @Composable + private fun getExportGroup(): Preference.PreferenceGroup { + val navigator = LocalNavigator.currentOrThrow + + return Preference.PreferenceGroup( + title = "Export", + preferenceItems = persistentListOf( + Preference.PreferenceItem.TextPreference( + title = LibraryDebugListScreen.TITLE, + onClick = { navigator.push(LibraryDebugListScreen()) }, + ), + ), + ) + } } 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 new file mode 100644 index 0000000000..77bc22ac96 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/more/settings/screen/data/LibraryListScreen.kt @@ -0,0 +1,129 @@ +package eu.kanade.presentation.more.settings.screen.data + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ContentCopy +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +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 eu.kanade.tachiyomi.util.system.copyToClipboard +import kotlinx.collections.immutable.persistentListOf +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.i18n.stringResource +import uy.kohesive.injekt.Injekt +import kotlinx.coroutines.flow.flow +import tachiyomi.presentation.core.components.material.padding +import tachiyomi.presentation.core.screens.EmptyScreen +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 LibraryDebugListScreen : Screen() { + + companion object { + const val TITLE = "Library List" + } + + @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()) + + Scaffold( + topBar = { + AppBar( + title = TITLE, + navigateUp = navigator::pop, + actions = { + AppBarActions( + persistentListOf( + AppBar.Action( + title = stringResource(MR.strings.action_copy_to_clipboard), + icon = Icons.Default.ContentCopy, + onClick = { + val csvData = favoritesState.joinToString("\n") { manga -> + val author = manga.author ?: "" + val artist = manga.artist ?: "" + "${manga.title}, $author, $artist" + } + context.copyToClipboard(TITLE, csvData) + }, + ), + ), + ) + }, + ) + }, + ) { 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, + ) + } + } + } + } +} From b9b955befde2b19e81d0d282a04b79e3baa23ffe Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Mon, 26 Aug 2024 17:50:34 +1200 Subject: [PATCH 2/7] Spotless --- .../settings/screen/SettingsDataScreen.kt | 2 +- .../settings/screen/data/LibraryListScreen.kt | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) 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 ca0cc1e5c6..95208a581e 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 @@ -37,9 +37,9 @@ 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.LibraryDebugListScreen import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen import eu.kanade.presentation.more.settings.screen.data.StorageInfo -import eu.kanade.presentation.more.settings.screen.data.LibraryDebugListScreen import eu.kanade.presentation.more.settings.widget.BasePreferenceWidget import eu.kanade.presentation.more.settings.widget.PrefsHorizontalPadding import eu.kanade.presentation.util.relativeTimeSpanString 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 index 77bc22ac96..6f0fd8e8c9 100644 --- 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 @@ -1,13 +1,20 @@ package eu.kanade.presentation.more.settings.screen.data -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Box +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.ContentCopy import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -21,15 +28,15 @@ import eu.kanade.presentation.manga.components.MangaCover import eu.kanade.presentation.util.Screen import eu.kanade.tachiyomi.util.system.copyToClipboard import kotlinx.collections.immutable.persistentListOf +import kotlinx.coroutines.flow.flow 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.i18n.stringResource -import uy.kohesive.injekt.Injekt -import kotlinx.coroutines.flow.flow 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 @@ -116,7 +123,7 @@ class LibraryDebugListScreen : Screen() { LazyColumn( modifier = Modifier .padding(contentPadding) - .padding(8.dp) + .padding(8.dp), ) { items(favoritesState) { manga -> BaseMangaListItem( From 5f505cf028f1ed77022f457a3a6cd2e18b41b848 Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:41:06 +1200 Subject: [PATCH 3/7] Save as CSV --- .../settings/screen/data/LibraryListScreen.kt | 163 ++++++++++++++++-- 1 file changed, 151 insertions(+), 12 deletions(-) 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 index 6f0fd8e8c9..52debd8e61 100644 --- 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 @@ -1,6 +1,9 @@ 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 @@ -8,13 +11,18 @@ 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.ContentCopy +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 @@ -26,13 +34,14 @@ 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 eu.kanade.tachiyomi.util.system.copyToClipboard 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 @@ -70,12 +79,16 @@ fun BaseMangaListItem( } } -class LibraryDebugListScreen : Screen() { +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 @@ -85,6 +98,60 @@ class LibraryDebugListScreen : Screen() { 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( @@ -95,15 +162,8 @@ class LibraryDebugListScreen : Screen() { persistentListOf( AppBar.Action( title = stringResource(MR.strings.action_copy_to_clipboard), - icon = Icons.Default.ContentCopy, - onClick = { - val csvData = favoritesState.joinToString("\n") { manga -> - val author = manga.author ?: "" - val artist = manga.artist ?: "" - "${manga.title}, $author, $artist" - } - context.copyToClipboard(TITLE, csvData) - }, + icon = Icons.Default.Save, + onClick = { showDialog = true }, ), ), ) @@ -134,3 +194,82 @@ class LibraryDebugListScreen : Screen() { } } } + +@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)) + } + }, + ) +} From f1574b12cde3bfc20335f89ad94eda117f8c6bf0 Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:45:14 +1200 Subject: [PATCH 4/7] Fixing Refactor --- .../presentation/more/settings/screen/SettingsDataScreen.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 95208a581e..da43161d0c 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 @@ -37,7 +37,7 @@ 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.LibraryDebugListScreen +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 @@ -323,8 +323,8 @@ object SettingsDataScreen : SearchableSettings { title = "Export", preferenceItems = persistentListOf( Preference.PreferenceItem.TextPreference( - title = LibraryDebugListScreen.TITLE, - onClick = { navigator.push(LibraryDebugListScreen()) }, + title = LibraryListScreen.TITLE, + onClick = { navigator.push(LibraryListScreen()) }, ), ), ) 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 5/7] 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 da43161d0c..2c4816f87f 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 52debd8e61..0000000000 --- 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)) - } - }, - ) -} From 410168a44a6d6a9d65d049313d407fa49050321f Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Tue, 27 Aug 2024 16:35:10 +1200 Subject: [PATCH 6/7] i18n --- .../settings/screen/SettingsDataScreen.kt | 19 ++++++++++++------- .../moko-resources/base/strings.xml | 6 ++++++ 2 files changed, 18 insertions(+), 7 deletions(-) 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 2c4816f87f..6231c26771 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 @@ -359,6 +359,8 @@ object SettingsDataScreen : SearchableSettings { // Write CSV data to output stream outputStream.write(csvData.toByteArray()) outputStream.flush() + + context.toast(MR.strings.library_exported) } } } @@ -380,10 +382,10 @@ object SettingsDataScreen : SearchableSettings { } return Preference.PreferenceGroup( - title = "Export", + title = stringResource(MR.strings.export), preferenceItems = persistentListOf( Preference.PreferenceItem.TextPreference( - title = "Library List", + title = stringResource(MR.strings.library_list), onClick = { showDialog = true }, ), ), @@ -391,7 +393,10 @@ object SettingsDataScreen : SearchableSettings { } private fun escapeCsvField(field: String): String { - return field.replace("\"", "\"\"").replace("\r\n", "\n").replace("\r", "\n") + return field + .replace("\"", "\"\"") + .replace("\r\n", "\n") + .replace("\r", "\n") } @Composable @@ -409,7 +414,7 @@ object SettingsDataScreen : SearchableSettings { AlertDialog( onDismissRequest = onDismissRequest, title = { - Text(text = "Select Fields") + Text(text = stringResource(MR.strings.migration_dialog_what_to_include)) }, text = { Column { @@ -439,7 +444,7 @@ object SettingsDataScreen : SearchableSettings { onCheckedChange = { authorSelected = it }, enabled = titleSelected, ) - Text(text = "Author") + Text(text = stringResource(MR.strings.author)) } // Artist checkbox, disabled if Title is not selected @@ -451,7 +456,7 @@ object SettingsDataScreen : SearchableSettings { onCheckedChange = { artistSelected = it }, enabled = titleSelected, ) - Text(text = "Artist") + Text(text = stringResource(MR.strings.artist)) } } }, @@ -462,7 +467,7 @@ object SettingsDataScreen : SearchableSettings { onDismissRequest() }, ) { - Text(text = "Save") + Text(text = stringResource(MR.strings.action_save)) } }, dismissButton = { diff --git a/i18n/src/commonMain/moko-resources/base/strings.xml b/i18n/src/commonMain/moko-resources/base/strings.xml index 3f1a37d772..526b96d86a 100644 --- a/i18n/src/commonMain/moko-resources/base/strings.xml +++ b/i18n/src/commonMain/moko-resources/base/strings.xml @@ -554,6 +554,10 @@ Cache cleared, %1$d files deleted Error occurred while clearing Clear chapter cache on app launch + Export + Library List + Library Exported + Syncing library @@ -668,6 +672,8 @@ Ongoing Unknown Unknown author + Author + Artist Unknown status Licensed From 1048ccc7c1fa9d51a07e95f07b4afc2303bd25e6 Mon Sep 17 00:00:00 2001 From: Roshan Varughese <40583749+Animeboynz@users.noreply.github.com> Date: Thu, 29 Aug 2024 23:15:32 +1200 Subject: [PATCH 7/7] Removing Comments --- .../presentation/more/settings/screen/SettingsDataScreen.kt | 5 ----- 1 file changed, 5 deletions(-) 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 6231c26771..2ff34dd996 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 @@ -344,7 +344,6 @@ object SettingsDataScreen : SearchableSettings { 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 "" @@ -356,7 +355,6 @@ object SettingsDataScreen : SearchableSettings { appendLine(row) } } - // Write CSV data to output stream outputStream.write(csvData.toByteArray()) outputStream.flush() @@ -418,7 +416,6 @@ object SettingsDataScreen : SearchableSettings { }, text = { Column { - // Title checkbox Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -435,7 +432,6 @@ object SettingsDataScreen : SearchableSettings { Text(text = stringResource(MR.strings.title)) } - // Author checkbox, disabled if Title is not selected Row( verticalAlignment = Alignment.CenterVertically, ) { @@ -447,7 +443,6 @@ object SettingsDataScreen : SearchableSettings { Text(text = stringResource(MR.strings.author)) } - // Artist checkbox, disabled if Title is not selected Row( verticalAlignment = Alignment.CenterVertically, ) {