diff --git a/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt b/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt index 53489946..1c3a1bf1 100644 --- a/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt +++ b/src/main/kotlin/com/looker/droidify/ui/fragments/AppSheetX.kt @@ -11,20 +11,42 @@ import android.provider.Settings import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.livedata.observeAsState +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.fragment.app.viewModels import androidx.lifecycle.lifecycleScope import com.google.android.material.snackbar.Snackbar import com.looker.droidify.R +import com.looker.droidify.RELEASE_STATE_INSTALLED +import com.looker.droidify.RELEASE_STATE_NONE +import com.looker.droidify.RELEASE_STATE_SUGGESTED import com.looker.droidify.content.Preferences import com.looker.droidify.content.ProductPreferences import com.looker.droidify.database.entity.Release +import com.looker.droidify.entity.AntiFeature import com.looker.droidify.entity.Cancelable import com.looker.droidify.entity.Connecting import com.looker.droidify.entity.Details +import com.looker.droidify.entity.DonateType import com.looker.droidify.entity.Install import com.looker.droidify.entity.Launch import com.looker.droidify.entity.PackageState @@ -35,18 +57,31 @@ import com.looker.droidify.entity.Share import com.looker.droidify.entity.Uninstall import com.looker.droidify.entity.Update import com.looker.droidify.installer.AppInstaller +import com.looker.droidify.network.CoilDownloader import com.looker.droidify.screen.MessageDialog import com.looker.droidify.screen.ScreenshotsFragment import com.looker.droidify.service.Connection import com.looker.droidify.service.DownloadService import com.looker.droidify.ui.activities.MainActivityX +import com.looker.droidify.ui.compose.components.ScreenshotItem +import com.looker.droidify.ui.compose.components.ScreenshotList +import com.looker.droidify.ui.compose.components.SwitchPreference +import com.looker.droidify.ui.compose.pages.app_detail.components.AppInfoHeader +import com.looker.droidify.ui.compose.pages.app_detail.components.HtmlTextBlock +import com.looker.droidify.ui.compose.pages.app_detail.components.LinkItem +import com.looker.droidify.ui.compose.pages.app_detail.components.PermissionsItem +import com.looker.droidify.ui.compose.pages.app_detail.components.ReleaseItem +import com.looker.droidify.ui.compose.pages.app_detail.components.TopBarHeader import com.looker.droidify.ui.compose.theme.AppTheme import com.looker.droidify.ui.compose.utils.Callbacks import com.looker.droidify.ui.viewmodels.AppViewModelX import com.looker.droidify.utility.Utils.rootInstallerEnabled import com.looker.droidify.utility.Utils.startUpdate import com.looker.droidify.utility.extension.android.Android +import com.looker.droidify.utility.extension.text.formatSize import com.looker.droidify.utility.findSuggestedProduct +import com.looker.droidify.utility.generateLinks +import com.looker.droidify.utility.generatePermissionGroups import com.looker.droidify.utility.isDarkTheme import com.looker.droidify.utility.onLaunchClick import kotlinx.coroutines.Dispatchers @@ -384,6 +419,238 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks { @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppSheet() { + val incompatible = Preferences[Preferences.Key.IncompatibleVersions] + val installed by viewModel.installedItem.observeAsState() + val products by viewModel.products.observeAsState() + val repos by viewModel.repositories.observeAsState() + val packageState by viewModel.state.observeAsState(if (installed == null) Install else Launch) + val actions by viewModel.actions.observeAsState() // TODO add rest actions to UI + val secondaryAction by viewModel.secondaryAction.observeAsState() // TODO add secondaryAction + val productRepos = products?.mapNotNull { product -> + repos?.firstOrNull { it.id == product.repositoryId } + ?.let { Pair(product, it) } + } ?: emptyList() + viewModel.productRepos = productRepos + val suggestedProductRepo = findSuggestedProduct(productRepos, installed) { it.first } + val compatibleReleasePairs = productRepos.asSequence() + .flatMap { (product, repository) -> + product.releases.asSequence() + .filter { incompatible || it.incompatibilities.isEmpty() } + .map { Pair(it, repository) } + } + .toList() + val releaseItems = compatibleReleasePairs.asSequence() + .map { (release, repository) -> + Triple( + release, + repository, + when { + installed?.versionCode == release.versionCode && installed?.signature == release.signature -> RELEASE_STATE_INSTALLED + release.incompatibilities.firstOrNull() == null && release.selected && repository.id == suggestedProductRepo?.second?.id -> RELEASE_STATE_SUGGESTED + else -> RELEASE_STATE_NONE + } + ) + } + .sortedByDescending { it.first.versionCode } + .toList() + val imageData by remember(suggestedProductRepo) { + mutableStateOf( + suggestedProductRepo?.let { + CoilDownloader.createIconUri( + it.first.packageName, + it.first.icon, + it.first.metadataIcon, + it.second.address, + it.second.authentication + ).toString() + } + ) + } + suggestedProductRepo?.let { (product, repo) -> + Scaffold( + // TODO add the topBar to the activity instead of the fragments + topBar = { + TopBarHeader( + appName = product.label, + packageName = product.packageName, + icon = imageData, + state = packageState + ) + } + ) { paddingValues -> + LazyColumn( + modifier = Modifier + .padding(paddingValues), + verticalArrangement = Arrangement.spacedBy(8.dp), + contentPadding = PaddingValues(8.dp) + ) { + item { + AppInfoHeader( + versionCode = product.versionCode.toString(), + appSize = product.displayRelease?.size?.formatSize().orEmpty(), + appDev = product.author.name.replaceFirstChar { it.titlecase() }, + state = packageState, + secondaryAction = secondaryAction, + onSource = { + product.source.let { link -> + if (link.isNotEmpty()) { + requireContext().startActivity( + Intent(Intent.ACTION_VIEW, link.toUri()) + ) + } + } + }, + onSourceLong = { + product.source.let { link -> + if (link.isNotEmpty()) { + copyLinkToClipboard( + requireActivity().window.decorView.rootView, + link + ) + } + } + }, + onAction = { onActionClick(packageState) }, + onSecondaryAction = { onActionClick(secondaryAction) } + ) + } + item { + AnimatedVisibility(visible = product.canUpdate(installed)) { + SwitchPreference(text = stringResource(id = R.string.ignore_this_update), + initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, + onCheckedChanged = { + ProductPreferences[product.packageName].let { + it.copy( + ignoreVersionCode = + if (it.ignoreVersionCode == product.versionCode) 0 else product.versionCode + ) + } + }) + } + } + item { + AnimatedVisibility(visible = installed != null) { + SwitchPreference(text = stringResource(id = R.string.ignore_all_updates), + initSelected = ProductPreferences[product.packageName].ignoreVersionCode == product.versionCode, + onCheckedChanged = { + ProductPreferences[product.packageName].let { + it.copy( + ignoreUpdates = !it.ignoreUpdates + ) + } + }) + } + } + item { + ScreenshotList(screenShots = suggestedProductRepo.first.screenshots.map { + ScreenshotItem( + screenShot = it, + repository = repo, + packageName = product.packageName + ) + }) { + onScreenshotClick(it) + } + } + item { + // TODO add markdown parsing + if (product.description.isNotEmpty()) HtmlTextBlock(description = product.description) + } + item { + val links = product.generateLinks(requireContext()) + if (links.isNotEmpty()) { + Text( + text = stringResource(id = R.string.links), + color = MaterialTheme.colorScheme.primary + ) + links.forEach { link -> + LinkItem( + linkType = link, + onClick = { it?.let { onUriClick(it, true) } }, + onLongClick = { link -> + copyLinkToClipboard( + requireActivity().window.decorView.rootView, + link.toString() + ) + } + ) + } + } + } + item { + if (product.donates.isNotEmpty()) { + Text( + text = stringResource(id = R.string.donate), + color = MaterialTheme.colorScheme.primary + ) + product.donates.forEach { + LinkItem(linkType = DonateType(it, requireContext()), + onClick = { link -> + link?.let { onUriClick(it, true) } + }, + onLongClick = { link -> + copyLinkToClipboard( + requireActivity().window.decorView.rootView, + link.toString() + ) + } + ) + } + } + } + item { + product.displayRelease?.generatePermissionGroups(requireContext()) + ?.let { list -> + Text( + text = stringResource(id = R.string.permissions), + color = MaterialTheme.colorScheme.primary + ) + list.forEach { p -> + PermissionsItem(permissionsType = p) { group, permissions -> + onPermissionsClick(group, permissions) + } + } + } + } + item { + if (product.antiFeatures.isNotEmpty()) { + Text( + text = stringResource(id = R.string.anti_features), + color = MaterialTheme.colorScheme.secondary + ) + Text( + text = product.antiFeatures.map { af -> + val titleId = + AntiFeature.values().find { it.key == af }?.titleResId + if (titleId != null) stringResource(id = titleId) + else stringResource(id = R.string.unknown_FORMAT, af) + } + .joinToString(separator = "\n") { "\u2022 $it" } + ) + } + } + item { + if (product.whatsNew.isNotEmpty()) { + Text( + text = stringResource(id = R.string.changes), + color = MaterialTheme.colorScheme.primary + ) + HtmlTextBlock(description = product.whatsNew) + } + } + + items(items = releaseItems) { + ReleaseItem( + release = it.first, + repository = it.second, + releaseState = it.third + ) { release -> + onReleaseClick(release) + } + } + } + } + } } } diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 6fee94ce..a96f394c 100644 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -163,6 +163,7 @@ Update Updates The upstream source code is not libre + NSFW (may contain disturbing content) Username Username missing Could not validate index.