Migrate ReaderPageSheet to Compose

This commit is contained in:
arkon 2023-06-23 23:17:47 -04:00
parent 42bc2b07ce
commit f2b0d74b4c
9 changed files with 188 additions and 187 deletions

View file

@ -407,6 +407,20 @@ class ReaderActivity : BaseActivity() {
)
}
binding.dialogRoot.setComposeContent {
val state by viewModel.state.collectAsState()
when (state.dialog) {
is ReaderViewModel.Dialog.Page -> ReaderPageDialog(
onDismissRequest = viewModel::closeDialog,
onSetAsCover = viewModel::setAsCover,
onShare = viewModel::shareImage,
onSave = viewModel::saveImage,
)
null -> {}
}
}
// Init listeners on bottom menu
binding.readerNav.setComposeContent {
val state by viewModel.state.collectAsState()
@ -786,7 +800,7 @@ class ReaderActivity : BaseActivity() {
* actions to perform is shown.
*/
fun onPageLongTap(page: ReaderPage) {
ReaderPageSheet(this, page).show()
viewModel.openPageDialog(page)
}
/**
@ -823,14 +837,6 @@ class ReaderActivity : BaseActivity() {
}
}
/**
* Called from the page sheet. It delegates the call to the presenter to do some IO, which
* will call [onShareImageResult] with the path the image was saved on when it's ready.
*/
fun shareImage(page: ReaderPage) {
viewModel.shareImage(page)
}
/**
* Called from the presenter when a page is ready to be shared. It shows Android's default
* sharing tool.
@ -846,14 +852,6 @@ class ReaderActivity : BaseActivity() {
startActivity(Intent.createChooser(intent, getString(R.string.action_share)))
}
/**
* Called from the page sheet. It delegates saving the image of the given [page] on external
* storage to the presenter.
*/
fun saveImage(page: ReaderPage) {
viewModel.saveImage(page)
}
/**
* Called from the presenter when a page is saved or fails. It shows a message or logs the
* event depending on the [result].
@ -869,14 +867,6 @@ class ReaderActivity : BaseActivity() {
}
}
/**
* Called from the page sheet. It delegates setting the image of the given [page] as the
* cover to the presenter.
*/
fun setAsCover(page: ReaderPage) {
viewModel.setAsCover(page)
}
/**
* Called from the presenter when a page is set as cover or fails. It shows a different message
* depending on the [result].

View file

@ -0,0 +1,102 @@
package eu.kanade.tachiyomi.ui.reader
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Photo
import androidx.compose.material.icons.outlined.Save
import androidx.compose.material.icons.outlined.Share
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.components.AdaptiveSheet
import eu.kanade.tachiyomi.R
import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding
@Composable
fun ReaderPageDialog(
onDismissRequest: () -> Unit,
onSetAsCover: () -> Unit,
onShare: () -> Unit,
onSave: () -> Unit,
) {
var showSetCoverDialog by remember { mutableStateOf(false) }
AdaptiveSheet(
onDismissRequest = onDismissRequest,
) {
Row(
modifier = Modifier.padding(vertical = 16.dp),
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) {
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.set_as_cover),
icon = Icons.Outlined.Photo,
onClick = { showSetCoverDialog = true },
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_share),
icon = Icons.Outlined.Share,
onClick = {
onShare()
onDismissRequest()
},
)
ActionButton(
modifier = Modifier.weight(1f),
title = stringResource(R.string.action_save),
icon = Icons.Outlined.Save,
onClick = {
onSave()
onDismissRequest()
},
)
}
}
if (showSetCoverDialog) {
SetCoverDialog(
onConfirm = {
onSetAsCover()
showSetCoverDialog = false
},
onDismiss = { showSetCoverDialog = false },
)
}
}
@Composable
private fun SetCoverDialog(
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
text = {
Text(stringResource(R.string.confirm_set_image_as_cover))
},
confirmButton = {
TextButton(onClick = onConfirm) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.action_cancel))
}
},
onDismissRequest = onDismiss,
)
}

View file

@ -1,62 +0,0 @@
package eu.kanade.tachiyomi.ui.reader
import android.view.LayoutInflater
import android.view.View
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ReaderPageSheetBinding
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
/**
* Sheet to show when a page is long clicked.
*/
class ReaderPageSheet(
private val activity: ReaderActivity,
private val page: ReaderPage,
) : BaseBottomSheetDialog(activity) {
private lateinit var binding: ReaderPageSheetBinding
override fun createView(inflater: LayoutInflater): View {
binding = ReaderPageSheetBinding.inflate(activity.layoutInflater, null, false)
binding.setAsCover.setOnClickListener { setAsCover() }
binding.share.setOnClickListener { share() }
binding.save.setOnClickListener { save() }
return binding.root
}
/**
* Sets the image of this page as the cover of the manga.
*/
private fun setAsCover() {
if (page.status != Page.State.READY) return
MaterialAlertDialogBuilder(activity)
.setMessage(R.string.confirm_set_image_as_cover)
.setPositiveButton(android.R.string.ok) { _, _ ->
activity.setAsCover(page)
}
.setNegativeButton(R.string.action_cancel, null)
.show()
}
/**
* Shares the image of this page with external apps.
*/
private fun share() {
activity.shareImage(page)
dismiss()
}
/**
* Saves the image of this page on external storage.
*/
private fun save() {
activity.saveImage(page)
dismiss()
}
}

View file

@ -719,12 +719,21 @@ class ReaderViewModel(
) + filenameSuffix
}
fun openPageDialog(page: ReaderPage) {
mutableState.update { it.copy(dialog = Dialog.Page(page)) }
}
fun closeDialog() {
mutableState.update { it.copy(dialog = null) }
}
/**
* Saves the image of this [page] on the pictures directory and notifies the UI of the result.
* Saves the image of the selected page on the pictures directory and notifies the UI of the result.
* There's also a notification to allow sharing the image somewhere else or deleting it.
*/
fun saveImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun saveImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -758,14 +767,15 @@ class ReaderViewModel(
}
/**
* Shares the image of this [page] and notifies the UI with the path of the file to share.
* Shares the image of the selected page and notifies the UI with the path of the file to share.
* The image must be first copied to the internal partition because there are many possible
* formats it can come from, like a zipped chapter, in which case it's not possible to directly
* get a path to the file and it has to be decompressed somewhere first. Only the last shared
* image will be kept so it won't be taking lots of internal disk space.
*/
fun shareImage(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun shareImage() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val context = Injekt.get<Application>()
@ -791,10 +801,11 @@ class ReaderViewModel(
}
/**
* Sets the image of this [page] as cover and notifies the UI of the result.
* Sets the image of the selected page as cover and notifies the UI of the result.
*/
fun setAsCover(page: ReaderPage) {
if (page.status != Page.State.READY) return
fun setAsCover() {
val page = (state.value.dialog as? Dialog.Page)?.page
if (page?.status != Page.State.READY) return
val manga = manga ?: return
val stream = page.stream ?: return
@ -907,11 +918,16 @@ class ReaderViewModel(
* Viewer used to display the pages (pager, webtoon, ...).
*/
val viewer: Viewer? = null,
val dialog: Dialog? = null,
) {
val totalPages: Int
get() = viewerChapters?.currChapter?.pages?.size ?: -1
}
sealed class Dialog {
data class Page(val page: ReaderPage) : Dialog()
}
sealed class Event {
object ReloadViewerChapters : Event()
data class SetOrientation(val orientation: Int) : Event()

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/black"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM19,19L5,19L5,5h11.17L19,7.83L19,19zM12,12c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3zM6,6h9v4L6,10z"/>
</vector>

View file

@ -137,4 +137,9 @@
android:layout_height="match_parent"
android:visibility="gone" />
<androidx.compose.ui.platform.ComposeView
android:id="@+id/dialog_root"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

View file

@ -1,53 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/set_as_cover"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/set_as_cover"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_photo_24dp"
app:drawableTint="?attr/colorOnBackground" />
<TextView
android:id="@+id/share"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/action_share"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_share_24dp"
app:drawableTint="?attr/colorOnBackground" />
<TextView
android:id="@+id/save"
android:layout_width="match_parent"
android:layout_height="56dp"
android:drawablePadding="32dp"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:paddingHorizontal="16dp"
android:foreground="?attr/selectableItemBackground"
android:text="@string/action_save"
android:textColor="?attr/colorOnBackground"
app:drawableStartCompat="@drawable/ic_save_24dp"
app:drawableTint="?attr/colorOnBackground" />
</LinearLayout>

View file

@ -0,0 +1,40 @@
package tachiyomi.presentation.core.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
@Composable
fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(
verticalArrangement = Arrangement.spacedBy(4.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
imageVector = icon,
contentDescription = null,
)
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}

View file

@ -4,17 +4,13 @@ import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.paddingFromBaseline
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
@ -24,6 +20,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import tachiyomi.presentation.core.components.ActionButton
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.presentation.core.util.secondaryItemAlpha
import kotlin.random.Random
@ -96,31 +93,6 @@ fun EmptyScreen(
}
}
@Composable
private fun ActionButton(
modifier: Modifier = Modifier,
title: String,
icon: ImageVector,
onClick: () -> Unit,
) {
TextButton(
modifier = modifier,
onClick = onClick,
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Icon(
imageVector = icon,
contentDescription = null,
)
Spacer(Modifier.height(4.dp))
Text(
text = title,
textAlign = TextAlign.Center,
)
}
}
}
private val ERROR_FACES = listOf(
"(・o・;)",
"Σ(ಠ_ಠ)",