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,
) {