From c14b7879a4e461bccbf47268670cc89f390d3878 Mon Sep 17 00:00:00 2001 From: Jobobby04 Date: Wed, 30 Nov 2022 13:59:58 -0500 Subject: [PATCH] Convert Batch Add to Compose + Voyager --- .../exh/ui/batchadd/BatchAddController.kt | 166 +---------------- .../java/exh/ui/batchadd/BatchAddPresenter.kt | 93 ---------- .../java/exh/ui/batchadd/BatchAddScreen.kt | 174 ++++++++++++++++++ .../exh/ui/batchadd/BatchAddScreenModel.kt | 141 ++++++++++++++ .../main/res/layout/eh_fragment_batch_add.xml | 115 ------------ 5 files changed, 322 insertions(+), 367 deletions(-) delete mode 100644 app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt create mode 100644 app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt create mode 100644 app/src/main/java/exh/ui/batchadd/BatchAddScreenModel.kt delete mode 100755 app/src/main/res/layout/eh_fragment_batch_add.xml diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddController.kt b/app/src/main/java/exh/ui/batchadd/BatchAddController.kt index 495b84d0e..02ae194d8 100755 --- a/app/src/main/java/exh/ui/batchadd/BatchAddController.kt +++ b/app/src/main/java/exh/ui/batchadd/BatchAddController.kt @@ -1,168 +1,16 @@ package exh.ui.batchadd -import android.view.LayoutInflater -import android.view.View -import android.widget.TextView -import androidx.core.view.isVisible -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.chrisbanes.insetter.applyInsetter -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.databinding.EhFragmentBatchAddBinding -import eu.kanade.tachiyomi.ui.base.controller.NucleusController -import kotlinx.coroutines.channels.BufferOverflow -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.buffer -import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.launchIn -import kotlinx.coroutines.flow.mapLatest -import kotlinx.coroutines.flow.onEach -import kotlinx.coroutines.launch -import reactivecircus.flowbinding.android.view.clicks -import kotlin.time.Duration.Companion.seconds +import androidx.compose.runtime.Composable +import cafe.adriel.voyager.navigator.Navigator +import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController /** * Batch add screen */ -class BatchAddController : NucleusController() { - override fun getTitle() = activity!!.getString(R.string.batch_add) +class BatchAddController : BasicFullComposeController() { - override fun createPresenter() = BatchAddPresenter() - - override fun createBinding(inflater: LayoutInflater) = EhFragmentBatchAddBinding.inflate(inflater) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.btnAddGalleries.clicks() - .onEach { - addGalleries(binding.galleriesBox.text.toString()) - } - .launchIn(viewScope) - - binding.progressDismissBtn.clicks() - .onEach { - presenter.currentlyAddingFlow.value = BatchAddPresenter.STATE_PROGRESS_TO_INPUT - } - .launchIn(viewScope) - - binding.scrollView.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - presenter.currentlyAddingFlow - .buffer(capacity = 0, onBufferOverflow = BufferOverflow.SUSPEND) - .mapLatest { event -> - when (event) { - BatchAddPresenter.STATE_INPUT_TO_PROGRESS -> coroutineScope { - showProgress(binding) - presenter.progressFlow - .buffer(capacity = Channel.RENDEZVOUS) - .combine(presenter.progressTotalFlow) { progress, total -> - // Show hide dismiss button - binding.progressDismissBtn.isVisible = progress == total - formatProgress(progress, total) - } - .onEach { - binding.progressText.text = it - } - .launchIn(this) - - presenter.progressTotalFlow - .buffer(capacity = Channel.RENDEZVOUS) - .onEach { - binding.progressBar.max = it - } - .launchIn(this) - - presenter.progressFlow - .buffer(capacity = Channel.RENDEZVOUS) - .onEach { - binding.progressBar.progress = it - } - .launchIn(this) - - presenter.eventFlow - ?.buffer(capacity = Channel.RENDEZVOUS) - ?.onEach { - binding.progressLog.append("$it\n") - } - ?.launchIn(this) - Unit - } - BatchAddPresenter.STATE_PROGRESS_TO_INPUT -> { - hideProgress(binding) - presenter.currentlyAddingFlow.value = BatchAddPresenter.STATE_IDLE - } - } - } - .launchIn(viewScope) - } - - private val EhFragmentBatchAddBinding.progressViews - get() = listOf( - progressTitleView, - progressLog, - progressBar, - progressText, - ) - - private val EhFragmentBatchAddBinding.inputViews - get() = listOf( - inputTitleView, - galleriesBox, - btnAddGalleries, - ) - - private var List.isVisible: Boolean - get() = throw UnsupportedOperationException() - set(v) { - forEach { it.isVisible = v } - } - - private fun showProgress(target: EhFragmentBatchAddBinding = binding) { - target.apply { - viewScope.launch { - inputViews.isVisible = false - delay(0.5.seconds) - progressViews.isVisible = true - } - }.progressLog.text = "" - } - - private fun hideProgress(target: EhFragmentBatchAddBinding = binding) { - target.apply { - viewScope.launch { - progressViews.isVisible = false - binding.progressDismissBtn.isVisible = false - delay(0.5.seconds) - inputViews.isVisible = true - } - }.galleriesBox.setText("", TextView.BufferType.EDITABLE) - } - - private fun formatProgress(progress: Int, total: Int) = "$progress/$total" - - private fun addGalleries(galleries: String) { - // Check text box has content - if (galleries.isBlank()) { - noGalleriesSpecified() - return - } - - presenter.addGalleries(applicationContext!!, galleries) - } - - private fun noGalleriesSpecified() { - activity?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.batch_add_no_valid_galleries) - .setMessage(R.string.batch_add_no_valid_galleries_message) - .setPositiveButton(android.R.string.ok, null) - .show() - } + @Composable + override fun ComposeContent() { + Navigator(screen = BatchAddScreen()) } } diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt b/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt deleted file mode 100644 index d680ac460..000000000 --- a/app/src/main/java/exh/ui/batchadd/BatchAddPresenter.kt +++ /dev/null @@ -1,93 +0,0 @@ -package exh.ui.batchadd - -import android.content.Context -import eu.kanade.domain.UnsortedPreferences -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.util.lang.withIOContext -import exh.GalleryAddEvent -import exh.GalleryAdder -import exh.log.xLogE -import exh.util.trimOrNull -import kotlinx.coroutines.CoroutineExceptionHandler -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.ensureActive -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.launch -import uy.kohesive.injekt.injectLazy - -class BatchAddPresenter : BasePresenter() { - private val preferences: UnsortedPreferences by injectLazy() - - private val galleryAdder by lazy { GalleryAdder() } - - val progressTotalFlow = MutableStateFlow(0) - val progressFlow = MutableStateFlow(0) - var eventFlow: MutableSharedFlow? = null - val currentlyAddingFlow = MutableStateFlow(STATE_IDLE) - - fun addGalleries(context: Context, galleries: String) { - eventFlow = MutableSharedFlow(1) - - val splitGalleries = if (ehVisitedRegex.containsMatchIn(galleries)) { - val url = if (preferences.enableExhentai().get()) { - "https://exhentai.org/g/" - } else { - "https://e-hentai.org/g/" - } - ehVisitedRegex.findAll(galleries).map { galleryKeys -> - val linkParts = galleryKeys.value.split(".") - url + linkParts[0] + "/" + linkParts[1].replace(":", "") - }.toList() - } else { - galleries.split("\n") - .mapNotNull(String::trimOrNull) - } - - progressFlow.value = 0 - progressTotalFlow.value = splitGalleries.size - - currentlyAddingFlow.value = STATE_INPUT_TO_PROGRESS - - val handler = CoroutineExceptionHandler { _, throwable -> - xLogE("Batch add error", throwable) - } - - presenterScope.launch(Dispatchers.IO + handler) { - val succeeded = mutableListOf() - val failed = mutableListOf() - - splitGalleries.forEachIndexed { i, s -> - ensureActive() - val result = withIOContext { galleryAdder.addGallery(context, s, true) } - if (result is GalleryAddEvent.Success) { - succeeded.add(s) - } else { - failed.add(s) - } - progressFlow.value = i + 1 - eventFlow?.emit( - ( - when (result) { - is GalleryAddEvent.Success -> context.getString(R.string.batch_add_ok) - is GalleryAddEvent.Fail -> context.getString(R.string.batch_add_error) - } - ) + " " + result.logMessage, - ) - } - - // Show report - val summary = context.getString(R.string.batch_add_summary, succeeded.size, failed.size) - eventFlow?.emit(summary) - } - } - - companion object { - const val STATE_IDLE = 0 - const val STATE_INPUT_TO_PROGRESS = 1 - const val STATE_PROGRESS_TO_INPUT = 2 - - val ehVisitedRegex = """[0-9]*?\.[a-z0-9]*?:""".toRegex() - } -} diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt b/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt new file mode 100644 index 000000000..e5a020a08 --- /dev/null +++ b/app/src/main/java/exh/ui/batchadd/BatchAddScreen.kt @@ -0,0 +1,174 @@ +package exh.ui.batchadd + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import cafe.adriel.voyager.core.model.rememberScreenModel +import cafe.adriel.voyager.core.screen.Screen +import cafe.adriel.voyager.navigator.LocalNavigator +import cafe.adriel.voyager.navigator.currentOrThrow +import eu.kanade.presentation.components.AppBar +import eu.kanade.presentation.components.Button +import eu.kanade.presentation.components.LazyColumn +import eu.kanade.presentation.components.Scaffold +import eu.kanade.presentation.util.LocalRouter +import eu.kanade.presentation.util.padding +import eu.kanade.presentation.util.plus +import eu.kanade.tachiyomi.R + +class BatchAddScreen : Screen { + + @Composable + override fun Content() { + val screenModel = rememberScreenModel { BatchAddScreenModel() } + val state by screenModel.state.collectAsState() + val navigator = LocalNavigator.currentOrThrow + val router = LocalRouter.currentOrThrow + val context = LocalContext.current + + Scaffold( + topBar = { scrollBehavior -> + AppBar( + title = stringResource(R.string.batch_add), + navigateUp = { + when { + navigator.canPop -> navigator.pop() + else -> router.popCurrentController() + } + }, + scrollBehavior = scrollBehavior, + ) + }, + ) { paddingValues -> + when (state.state) { + BatchAddScreenModel.State.INPUT -> { + Column( + Modifier + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(MaterialTheme.padding.medium), + ) { + Text(text = stringResource(R.string.eh_batch_add_title), style = MaterialTheme.typography.titleLarge) + Spacer(Modifier.height(8.dp)) + TextField( + value = state.galleries, + onValueChange = screenModel::updateGalleries, + modifier = Modifier.fillMaxWidth(), + placeholder = { + Text( + text = stringResource(R.string.eh_batch_add_description), + ) + }, + keyboardOptions = KeyboardOptions(autoCorrect = false), + textStyle = MaterialTheme.typography.bodyLarge, + + ) + Spacer(Modifier.height(8.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = { screenModel.addGalleries(context) }, + ) { + Text(text = stringResource(R.string.eh_batch_add_button)) + } + } + } + BatchAddScreenModel.State.PROGRESS -> { + LazyColumn( + contentPadding = paddingValues + PaddingValues(MaterialTheme.padding.medium), + ) { + item(key = "top") { + Column { + Text(text = stringResource(R.string.eh_batch_add_adding_galleries), style = MaterialTheme.typography.titleLarge) + Spacer(modifier = Modifier.height(8.dp)) + Row( + Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + ) { + LinearProgressIndicator( + progress = state.progress.toFloat() / state.progressTotal, + Modifier + .padding(top = 2.dp) + .weight(1f), + ) + Text( + text = state.progress.toString() + "/" + state.progressTotal, + modifier = Modifier.weight(0.15f), + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyMedium, + ) + } + Spacer(modifier = Modifier.height(12.dp)) + } + } + itemsIndexed( + state.events, + key = { index, text -> index + text.hashCode() }, + ) { _, text -> + Text( + text = text, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.fillMaxWidth(), + textAlign = TextAlign.Center, + ) + } + if (state.progress == state.progressTotal) { + item(key = "finish") { + Column { + Spacer(modifier = Modifier.height(8.dp)) + Button( + modifier = Modifier.fillMaxWidth(), + onClick = screenModel::finish, + ) { + Text(text = stringResource(R.string.eh_batch_add_finish)) + } + } + } + } + } + } + } + } + + val onDismissRequest = screenModel::dismissDialog + when (state.dialog) { + BatchAddScreenModel.Dialog.NoGalleriesSpecified -> AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + TextButton(onClick = onDismissRequest) { + Text(text = stringResource(android.R.string.ok)) + } + }, + title = { + Text(text = stringResource(R.string.batch_add_no_valid_galleries)) + }, + text = { + Text(text = stringResource(R.string.batch_add_no_valid_galleries_message)) + }, + ) + null -> Unit + } + } +} diff --git a/app/src/main/java/exh/ui/batchadd/BatchAddScreenModel.kt b/app/src/main/java/exh/ui/batchadd/BatchAddScreenModel.kt new file mode 100644 index 000000000..404c91b22 --- /dev/null +++ b/app/src/main/java/exh/ui/batchadd/BatchAddScreenModel.kt @@ -0,0 +1,141 @@ +package exh.ui.batchadd + +import android.content.Context +import cafe.adriel.voyager.core.model.StateScreenModel +import cafe.adriel.voyager.core.model.coroutineScope +import eu.kanade.domain.UnsortedPreferences +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.util.lang.withIOContext +import exh.GalleryAddEvent +import exh.GalleryAdder +import exh.log.xLogE +import exh.util.trimOrNull +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ensureActive +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get + +class BatchAddScreenModel( + private val unsortedPreferences: UnsortedPreferences = Injekt.get(), +) : StateScreenModel(BatchAddState()) { + private val galleryAdder by lazy { GalleryAdder() } + + fun addGalleries(context: Context) { + val galleries = state.value.galleries + // Check text box has content + if (galleries.isBlank()) { + mutableState.update { it.copy(dialog = Dialog.NoGalleriesSpecified) } + return + } + + addGalleries(context, galleries) + } + + private fun addGalleries(context: Context, galleries: String) { + val splitGalleries = if (ehVisitedRegex.containsMatchIn(galleries)) { + val url = if (unsortedPreferences.enableExhentai().get()) { + "https://exhentai.org/g/" + } else { + "https://e-hentai.org/g/" + } + ehVisitedRegex.findAll(galleries).map { galleryKeys -> + val linkParts = galleryKeys.value.split(".") + url + linkParts[0] + "/" + linkParts[1].replace(":", "") + }.toList() + } else { + galleries.split("\n") + .mapNotNull(String::trimOrNull) + } + + mutableState.update { state -> + state.copy( + progress = 0, + progressTotal = splitGalleries.size, + state = State.PROGRESS, + ) + } + + val handler = CoroutineExceptionHandler { _, throwable -> + xLogE("Batch add error", throwable) + } + + coroutineScope.launch(Dispatchers.IO + handler) { + val succeeded = mutableListOf() + val failed = mutableListOf() + + splitGalleries.forEachIndexed { i, s -> + ensureActive() + val result = withIOContext { galleryAdder.addGallery(context, s, true) } + if (result is GalleryAddEvent.Success) { + succeeded.add(s) + } else { + failed.add(s) + } + mutableState.update { state -> + state.copy( + progress = i + 1, + events = state.events.plus( + when (result) { + is GalleryAddEvent.Success -> context.getString(R.string.batch_add_ok) + is GalleryAddEvent.Fail -> context.getString(R.string.batch_add_error) + } + " " + result.logMessage, + ), + ) + } + } + + // Show report + val summary = context.getString(R.string.batch_add_summary, succeeded.size, failed.size) + mutableState.update { state -> + state.copy( + events = state.events + summary, + ) + } + } + } + + fun finish() { + mutableState.update { state -> + state.copy( + progressTotal = 0, + progress = 0, + galleries = "", + state = State.INPUT, + events = emptyList(), + ) + } + } + + fun updateGalleries(galleries: String) { + mutableState.update { it.copy(galleries = galleries) } + } + + fun dismissDialog() { + mutableState.update { it.copy(dialog = null) } + } + + enum class State { + INPUT, + PROGRESS, + } + + sealed class Dialog { + object NoGalleriesSpecified : Dialog() + } + + companion object { + val ehVisitedRegex = """[0-9]*?\.[a-z0-9]*?:""".toRegex() + } +} + +data class BatchAddState( + val progressTotal: Int = 0, + val progress: Int = 0, + val galleries: String = "", + val state: BatchAddScreenModel.State = BatchAddScreenModel.State.INPUT, + val events: List = emptyList(), + val dialog: BatchAddScreenModel.Dialog? = null, +) diff --git a/app/src/main/res/layout/eh_fragment_batch_add.xml b/app/src/main/res/layout/eh_fragment_batch_add.xml deleted file mode 100755 index e24bc7652..000000000 --- a/app/src/main/res/layout/eh_fragment_batch_add.xml +++ /dev/null @@ -1,115 +0,0 @@ - - - - - - - - - - - -