mihon/app/src/main/java/eu/kanade/presentation/browse/ExtensionsScreen.kt

421 lines
15 KiB
Kotlin
Raw Normal View History

2022-05-15 17:03:57 -04:00
package eu.kanade.presentation.browse
import androidx.annotation.StringRes
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars
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.Close
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus
2022-05-15 16:19:55 -04:00
import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionState
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
2022-05-15 17:03:57 -04:00
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
@Composable
fun ExtensionScreen(
nestedScrollInterop: NestedScrollConnection,
2022-05-15 17:03:57 -04:00
presenter: ExtensionsPresenter,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
onRefresh: () -> Unit,
onLaunched: () -> Unit,
) {
val state by presenter.state.collectAsState()
val isRefreshing = presenter.isRefreshing
SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(isRefreshing),
onRefresh = onRefresh,
) {
when (state) {
is ExtensionState.Initialized -> {
ExtensionContent(
nestedScrollInterop = nestedScrollInterop,
items = (state as ExtensionState.Initialized).list,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onInstallExtension = onInstallExtension,
onUninstallExtension = onUninstallExtension,
onUpdateExtension = onUpdateExtension,
onTrustExtension = onTrustExtension,
onOpenExtension = onOpenExtension,
onClickUpdateAll = onClickUpdateAll,
onLaunched = onLaunched,
)
}
ExtensionState.Uninitialized -> {}
}
}
}
@Composable
fun ExtensionContent(
nestedScrollInterop: NestedScrollConnection,
items: List<ExtensionUiModel>,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
onUninstallExtension: (Extension) -> Unit,
onUpdateExtension: (Extension.Installed) -> Unit,
onTrustExtension: (Extension.Untrusted) -> Unit,
onOpenExtension: (Extension.Installed) -> Unit,
onClickUpdateAll: () -> Unit,
onLaunched: () -> Unit,
) {
val (trustState, setTrustState) = remember { mutableStateOf<Extension.Untrusted?>(null) }
LazyColumn(
2022-05-15 17:03:57 -04:00
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(
items = items,
key = {
when (it) {
is ExtensionUiModel.Header.Resource -> it.textRes
is ExtensionUiModel.Header.Text -> it.text
is ExtensionUiModel.Item -> it.key()
}
},
contentType = {
when (it) {
is ExtensionUiModel.Item -> "item"
else -> "header"
}
},
) { item ->
when (item) {
is ExtensionUiModel.Header.Resource -> {
val action: @Composable RowScope.() -> Unit =
if (item.textRes == R.string.ext_updates_pending) {
{
Button(onClick = { onClickUpdateAll() }) {
Text(
text = stringResource(id = R.string.ext_update_all),
style = LocalTextStyle.current.copy(
color = MaterialTheme.colorScheme.onPrimary,
),
)
}
}
} else {
{}
}
ExtensionHeader(
textRes = item.textRes,
modifier = Modifier.animateItemPlacement(),
action = action,
)
}
is ExtensionUiModel.Header.Text -> {
ExtensionHeader(
text = item.text,
modifier = Modifier.animateItemPlacement(),
)
}
is ExtensionUiModel.Item -> {
ExtensionItem(
modifier = Modifier.animateItemPlacement(),
item = item,
onClickItem = {
when (it) {
is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> {
if (it.hasUpdate) {
onUpdateExtension(it)
} else {
onOpenExtension(it)
}
}
is Extension.Untrusted -> setTrustState(it)
}
},
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onClickItemAction = {
when (it) {
is Extension.Available -> onInstallExtension(it)
is Extension.Installed -> {
if (it.hasUpdate) {
onUpdateExtension(it)
} else {
onOpenExtension(it)
}
}
is Extension.Untrusted -> setTrustState(it)
}
},
)
LaunchedEffect(Unit) {
onLaunched()
}
}
}
}
}
if (trustState != null) {
ExtensionTrustDialog(
onClickConfirm = {
onTrustExtension(trustState)
setTrustState(null)
},
onClickDismiss = {
onUninstallExtension(trustState)
setTrustState(null)
},
onDismissRequest = {
setTrustState(null)
},
)
}
}
@Composable
fun ExtensionItem(
modifier: Modifier = Modifier,
item: ExtensionUiModel.Item,
onClickItem: (Extension) -> Unit,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onClickItemAction: (Extension) -> Unit,
) {
val (extension, installStep) = item
BaseBrowseItem(
modifier = modifier
.combinedClickable(
onClick = { onClickItem(extension) },
onLongClick = { onLongClickItem(extension) },
),
onClickItem = { onClickItem(extension) },
onLongClickItem = { onLongClickItem(extension) },
icon = {
ExtensionIcon(extension = extension)
},
action = {
ExtensionItemActions(
extension = extension,
installStep = installStep,
onClickItemCancel = onClickItemCancel,
onClickItemAction = onClickItemAction,
)
},
) {
ExtensionItemContent(
extension = extension,
modifier = Modifier.weight(1f),
)
}
}
@Composable
fun ExtensionItemContent(
extension: Extension,
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val warning = remember(extension) {
when {
extension is Extension.Untrusted -> R.string.ext_untrusted
extension is Extension.Installed && extension.isUnofficial -> R.string.ext_unofficial
extension is Extension.Installed && extension.isObsolete -> R.string.ext_obsolete
extension.isNsfw -> R.string.ext_nsfw_short
else -> null
}
}
Column(
modifier = modifier.padding(start = horizontalPadding),
) {
Text(
text = extension.name,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = MaterialTheme.typography.bodyMedium,
)
Row(
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
if (extension.lang.isNullOrEmpty().not()) {
Text(
text = LocaleHelper.getSourceDisplayName(extension.lang, context),
style = MaterialTheme.typography.bodySmall,
)
}
if (extension.versionName.isNotEmpty()) {
Text(
text = extension.versionName,
style = MaterialTheme.typography.bodySmall,
)
}
if (warning != null) {
Text(
text = stringResource(id = warning).uppercase(),
style = MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.error,
),
)
}
}
}
}
@Composable
fun ExtensionItemActions(
extension: Extension,
installStep: InstallStep,
modifier: Modifier = Modifier,
onClickItemCancel: (Extension) -> Unit = {},
onClickItemAction: (Extension) -> Unit = {},
) {
val isIdle = remember(installStep) {
installStep == InstallStep.Idle || installStep == InstallStep.Error
}
Row(modifier = modifier) {
TextButton(
onClick = { onClickItemAction(extension) },
enabled = isIdle,
) {
Text(
text = when (installStep) {
InstallStep.Pending -> stringResource(R.string.ext_pending)
InstallStep.Downloading -> stringResource(R.string.ext_downloading)
InstallStep.Installing -> stringResource(R.string.ext_installing)
InstallStep.Installed -> stringResource(R.string.ext_installed)
InstallStep.Error -> stringResource(R.string.action_retry)
InstallStep.Idle -> {
when (extension) {
is Extension.Installed -> {
if (extension.hasUpdate) {
stringResource(R.string.ext_update)
} else {
stringResource(R.string.action_settings)
}
}
is Extension.Untrusted -> stringResource(R.string.ext_trust)
is Extension.Available -> stringResource(R.string.ext_install)
}
}
},
style = LocalTextStyle.current.copy(
color = if (isIdle) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceTint,
),
)
}
if (isIdle.not()) {
IconButton(onClick = { onClickItemCancel(extension) }) {
Icon(Icons.Default.Close, "")
}
}
}
}
@Composable
fun ExtensionHeader(
@StringRes textRes: Int,
modifier: Modifier = Modifier,
action: @Composable RowScope.() -> Unit = {},
) {
ExtensionHeader(
text = stringResource(id = textRes),
modifier = modifier,
action = action,
)
}
@Composable
fun ExtensionHeader(
text: String,
modifier: Modifier = Modifier,
action: @Composable RowScope.() -> Unit = {},
) {
Row(
modifier = modifier.padding(horizontal = horizontalPadding),
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = text,
modifier = Modifier
.padding(vertical = 8.dp)
.weight(1f),
style = MaterialTheme.typography.header,
)
action()
}
}
@Composable
fun ExtensionTrustDialog(
onClickConfirm: () -> Unit,
onClickDismiss: () -> Unit,
onDismissRequest: () -> Unit,
) {
AlertDialog(
title = {
Text(text = stringResource(id = R.string.untrusted_extension))
},
text = {
Text(text = stringResource(id = R.string.untrusted_extension_message))
},
confirmButton = {
TextButton(onClick = onClickConfirm) {
Text(text = stringResource(id = R.string.ext_trust))
}
},
dismissButton = {
TextButton(onClick = onClickDismiss) {
Text(text = stringResource(id = R.string.ext_uninstall))
}
},
onDismissRequest = onDismissRequest,
)
}