diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index c288d33a5..02d107a65 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -31,7 +31,6 @@ import android.widget.RelativeLayout import android.widget.TextView import android.widget.Toast import androidx.activity.viewModels -import androidx.annotation.ColorInt import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -96,7 +95,6 @@ import eu.kanade.tachiyomi.util.view.popupMenu import eu.kanade.tachiyomi.util.view.setComposeContent import eu.kanade.tachiyomi.util.view.setTooltip import eu.kanade.tachiyomi.widget.listener.SimpleAnimationListener -import exh.log.xLogE import exh.source.isEhBasedSource import exh.util.defaultReaderType import exh.util.mangaType @@ -530,6 +528,23 @@ 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, + onShareCombined = viewModel::shareImages, + onSaveCombined = viewModel::saveImages, + hasExtraPage = (state.dialog as? ReaderViewModel.Dialog.Page)?.extraPage != null, + ) + null -> {} + } + } + // SY --> val sliderContent: @Composable (Boolean) -> Unit = a@{ isVertical -> val state by viewModel.state.collectAsState() @@ -1325,18 +1340,7 @@ class ReaderActivity : BaseActivity() { */ fun onPageLongTap(page: ReaderPage, extraPage: ReaderPage? = null) { // SY --> - try { - val viewer = viewModel.state.value.viewer as? PagerViewer - ReaderPageSheet( - this, - page, - extraPage, - (viewer !is R2LPagerViewer) xor (viewer?.config?.invertDoublePages ?: false), - viewer?.config?.pageCanvasColor, - ).show() - } catch (e: WindowManager.BadTokenException) { - xLogE("Caught and ignoring reader page sheet launch exception!", e) - } + viewModel.openPageDialog(page, extraPage) // SY <-- } @@ -1374,20 +1378,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) - } - - // SY --> - fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - viewModel.shareImages(firstPage, secondPage, isLTR, bg) - } - // SY <-- - /** * Called from the presenter when a page is ready to be shared. It shows Android's default * sharing tool. @@ -1411,20 +1401,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) - } - - // SY --> - fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { - viewModel.saveImages(firstPage, secondPage, isLTR, bg) - } - // SY <-- - /** * Called from the presenter when a page is saved or fails. It shows a message or logs the * event depending on the [result]. @@ -1440,14 +1416,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]. diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt new file mode 100644 index 000000000..82009642b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageDialog.kt @@ -0,0 +1,198 @@ +package eu.kanade.tachiyomi.ui.reader + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +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, + // SY --> + onSetAsCover: (useExtraPage: Boolean) -> Unit, + onShare: (useExtraPage: Boolean) -> Unit, + onSave: (useExtraPage: Boolean) -> Unit, + onShareCombined: () -> Unit, + onSaveCombined: () -> Unit, + hasExtraPage: Boolean, + // SY <-- +) { + var showSetCoverDialog by remember { mutableStateOf(false) } + // SY --> + var useExtraPage by remember { mutableStateOf(false) } + // SY <-- + + AdaptiveSheet( + onDismissRequest = onDismissRequest, + ) { + Column(modifier = Modifier.padding(vertical = 16.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource( + // SY --> + if (hasExtraPage) { + R.string.action_set_first_page_cover + } else { + R.string.set_as_cover + }, + // SY <-- + ), + icon = Icons.Outlined.Photo, + onClick = { showSetCoverDialog = true }, + ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource( + // SY --> + if (hasExtraPage) { + R.string.action_share_first_page + } else { + R.string.action_share + }, + // SY <-- + ), + icon = Icons.Outlined.Share, + onClick = { + // SY --> + onShare(false) + // SY <-- + onDismissRequest() + }, + ) + + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource( + // SY --> + if (hasExtraPage) { + R.string.action_save_first_page + } else { + R.string.action_save + }, + // SY <-- + ), + icon = Icons.Outlined.Save, + onClick = { + // SY --> + onSave(false) + // SY <-- + onDismissRequest() + }, + ) + } + if (hasExtraPage) { + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(R.string.action_set_second_page_cover), + icon = Icons.Outlined.Photo, + onClick = { + showSetCoverDialog = true + }, + ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(R.string.action_share_second_page), + icon = Icons.Outlined.Share, + onClick = { + onShare(true) + onDismissRequest() + }, + ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(R.string.action_save_second_page), + icon = Icons.Outlined.Save, + onClick = { + onSave(true) + onDismissRequest() + }, + ) + } + Row( + horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small), + ) { + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(R.string.action_share_combined_page), + icon = Icons.Outlined.Share, + onClick = { + onShareCombined() + onDismissRequest() + }, + ) + ActionButton( + modifier = Modifier.weight(1f), + title = stringResource(R.string.action_save_combined_page), + icon = Icons.Outlined.Save, + onClick = { + onSaveCombined() + onDismissRequest() + }, + ) + } + } + } + } + + if (showSetCoverDialog) { + SetCoverDialog( + onConfirm = { + // SY --> + onSetAsCover(useExtraPage) + showSetCoverDialog = false + useExtraPage = false + // SY <-- + }, + 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, + ) +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt deleted file mode 100644 index 78163fc0e..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPageSheet.kt +++ /dev/null @@ -1,94 +0,0 @@ -package eu.kanade.tachiyomi.ui.reader - -import android.view.LayoutInflater -import android.view.View -import androidx.core.view.isVisible -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, - private val extraPage: ReaderPage? = null, - private val isLTR: Boolean = false, - private val bg: Int? = null, -) : BaseBottomSheetDialog(activity) { - - private lateinit var binding: ReaderPageSheetBinding - - override fun createView(inflater: LayoutInflater): View { - binding = ReaderPageSheetBinding.inflate(activity.layoutInflater, null, false) - - binding.setAsCover.setOnClickListener { setAsCover(page) } - binding.share.setOnClickListener { share(page) } - binding.save.setOnClickListener { save(page) } - - if (extraPage != null) { - binding.setAsCover.setText(R.string.action_set_first_page_cover) - binding.share.setText(R.string.action_share_first_page) - binding.save.setText(R.string.action_save_first_page) - - binding.setAsCoverExtra.isVisible = true - binding.setAsCoverExtra.setOnClickListener { setAsCover(extraPage) } - binding.shareExtra.isVisible = true - binding.shareExtra.setOnClickListener { share(extraPage) } - binding.saveExtra.isVisible = true - binding.saveExtra.setOnClickListener { save(extraPage) } - - binding.shareCombined.isVisible = true - binding.shareCombined.setOnClickListener { shareCombined() } - binding.saveCombined.isVisible = true - binding.saveCombined.setOnClickListener { saveCombined() } - } - - return binding.root - } - - /** - * Sets the image of this page as the cover of the manga. - */ - private fun setAsCover(page: ReaderPage) { - 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(page: ReaderPage) { - activity.shareImage(page) - dismiss() - } - - fun shareCombined() { - activity.shareImages(page, extraPage!!, isLTR, bg!!) - dismiss() - } - - /** - * Saves the image of this page on external storage. - */ - private fun save(page: ReaderPage) { - activity.saveImage(page) - dismiss() - } - - fun saveCombined() { - activity.saveImages(page, extraPage!!, isLTR, bg!!) - dismiss() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt index 947be852f..6f8440050 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderViewModel.kt @@ -40,12 +40,15 @@ import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.viewer.Viewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer +import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer import eu.kanade.tachiyomi.util.chapter.filterDownloaded import eu.kanade.tachiyomi.util.chapter.removeDuplicates import eu.kanade.tachiyomi.util.editCover import eu.kanade.tachiyomi.util.lang.byteSize import eu.kanade.tachiyomi.util.lang.takeBytes import eu.kanade.tachiyomi.util.storage.DiskUtil +import eu.kanade.tachiyomi.util.storage.DiskUtil.MAX_FILE_NAME_BYTES import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.system.isOnline import exh.md.utils.FollowStatus @@ -183,7 +186,7 @@ class ReaderViewModel( getMergedChapterByMangaId.await(manga.id) to getMergedMangaById.await(manga.id) .associateBy { it.id } } else { - getChapterByMangaId.await(manga.id) to null + getChapterByMangaId.await(manga.id) to null } } fun isChapterDownloaded(chapter: Chapter): Boolean { @@ -192,7 +195,7 @@ class ReaderViewModel( chapterName = chapter.name, chapterScanlator = chapter.scanlator, mangaTitle = chapterManga.ogTitle, - sourceId = chapterManga.source + sourceId = chapterManga.source, ) } // SY <-- @@ -659,7 +662,7 @@ class ReaderViewModel( // SY --> readerChapter.requestedPage = readerChapter.chapter.last_page_read // SY <-- - if (incognitoMode) return + if (incognitoMode) return val chapter = readerChapter.chapter getCurrentChapter()?.requestedPage = chapter.last_page_read @@ -860,12 +863,27 @@ class ReaderViewModel( ) + filenameSuffix } + fun openPageDialog(page: ReaderPage/* SY --> */, extraPage: ReaderPage? = null/* SY <-- */) { + mutableState.update { it.copy(dialog = Dialog.Page(page, extraPage)) } + } + + 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(useExtraPage: Boolean) { + // SY --> + val page = if (useExtraPage) { + (state.value.dialog as? Dialog.Page)?.extraPage + } else { + (state.value.dialog as? Dialog.Page)?.page + } + // SY <-- + if (page?.status != Page.State.READY) return val manga = manga ?: return val context = Injekt.get() @@ -899,9 +917,15 @@ class ReaderViewModel( } // SY --> - fun saveImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { + fun saveImages() { + val (firstPage, secondPage) = (state.value.dialog as? Dialog.Page ?: return) + val viewer = state.value.viewer as? PagerViewer ?: return + val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages) + val bg = viewer.config.pageCanvasColor + if (firstPage.status != Page.State.READY) return - if (secondPage.status != Page.State.READY) return + if (secondPage?.status != Page.State.READY) return + val manga = manga ?: return val context = Injekt.get() @@ -964,14 +988,21 @@ class ReaderViewModel( // SY <-- /** - * 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(useExtraPage: Boolean) { + // SY --> + val page = if (useExtraPage) { + (state.value.dialog as? Dialog.Page)?.extraPage + } else { + (state.value.dialog as? Dialog.Page)?.page + } + // SY <-- + if (page?.status != Page.State.READY) return val manga = manga ?: return val context = Injekt.get() @@ -997,9 +1028,14 @@ class ReaderViewModel( } // SY --> - fun shareImages(firstPage: ReaderPage, secondPage: ReaderPage, isLTR: Boolean, @ColorInt bg: Int) { + fun shareImages() { + val (firstPage, secondPage) = (state.value.dialog as? Dialog.Page ?: return) + val viewer = state.value.viewer as? PagerViewer ?: return + val isLTR = (viewer !is R2LPagerViewer) xor (viewer.config.invertDoublePages) + val bg = viewer.config.pageCanvasColor + if (firstPage.status != Page.State.READY) return - if (secondPage.status != Page.State.READY) return + if (secondPage?.status != Page.State.READY) return val manga = manga ?: return val context = Injekt.get() @@ -1025,10 +1061,17 @@ class ReaderViewModel( // SY <-- /** - * 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(useExtraPage: Boolean) { + // SY --> + val page = if (useExtraPage) { + (state.value.dialog as? Dialog.Page)?.extraPage + } else { + (state.value.dialog as? Dialog.Page)?.page + } + // SY <-- + if (page?.status != Page.State.READY) return val manga = manga ?: return val stream = page.stream ?: return @@ -1143,10 +1186,12 @@ class ReaderViewModel( val viewerChapters: ViewerChapters? = null, val isLoadingAdjacentChapter: Boolean = false, val currentPage: Int = -1, + /** * Viewer used to display the pages (pager, webtoon, ...). */ val viewer: Viewer? = null, + val dialog: Dialog? = null, // SY --> val currentPageText: String = "", @@ -1158,6 +1203,10 @@ class ReaderViewModel( get() = viewerChapters?.currChapter?.pages?.size ?: -1 } + sealed class Dialog { + data class Page(val page: ReaderPage/* SY --> */, val extraPage: ReaderPage? = null /* SY <-- */) : Dialog() + } + sealed class Event { object ReloadViewerChapters : Event() data class SetOrientation(val orientation: Int) : Event() diff --git a/app/src/main/res/drawable/ic_save_24dp.xml b/app/src/main/res/drawable/ic_save_24dp.xml deleted file mode 100644 index 4435923ad..000000000 --- a/app/src/main/res/drawable/ic_save_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/reader_activity.xml b/app/src/main/res/layout/reader_activity.xml index 308f5b4e6..01de4887b 100755 --- a/app/src/main/res/layout/reader_activity.xml +++ b/app/src/main/res/layout/reader_activity.xml @@ -348,4 +348,9 @@ android:layout_height="match_parent" android:visibility="gone" /> + + diff --git a/app/src/main/res/layout/reader_page_sheet.xml b/app/src/main/res/layout/reader_page_sheet.xml deleted file mode 100644 index e0559b65b..000000000 --- a/app/src/main/res/layout/reader_page_sheet.xml +++ /dev/null @@ -1,134 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt new file mode 100644 index 000000000..54cbd9cdd --- /dev/null +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/components/ActionButton.kt @@ -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, + ) + } + } +} diff --git a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt index 632763239..c3a0cbe79 100644 --- a/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt +++ b/presentation-core/src/main/java/tachiyomi/presentation/core/screens/EmptyScreen.kt @@ -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・;)", "Σ(ಠ_ಠ)",