Add: Initial AppSheet Compose

This commit is contained in:
machiav3lli
2022-05-29 03:44:54 +02:00
parent 27b14d63e1
commit 9cf2cb5172
2 changed files with 268 additions and 0 deletions

View File

@ -11,20 +11,42 @@ import android.provider.Settings
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.isSystemInDarkTheme 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.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.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.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.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.looker.droidify.R 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.Preferences
import com.looker.droidify.content.ProductPreferences import com.looker.droidify.content.ProductPreferences
import com.looker.droidify.database.entity.Release import com.looker.droidify.database.entity.Release
import com.looker.droidify.entity.AntiFeature
import com.looker.droidify.entity.Cancelable import com.looker.droidify.entity.Cancelable
import com.looker.droidify.entity.Connecting import com.looker.droidify.entity.Connecting
import com.looker.droidify.entity.Details import com.looker.droidify.entity.Details
import com.looker.droidify.entity.DonateType
import com.looker.droidify.entity.Install import com.looker.droidify.entity.Install
import com.looker.droidify.entity.Launch import com.looker.droidify.entity.Launch
import com.looker.droidify.entity.PackageState 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.Uninstall
import com.looker.droidify.entity.Update import com.looker.droidify.entity.Update
import com.looker.droidify.installer.AppInstaller import com.looker.droidify.installer.AppInstaller
import com.looker.droidify.network.CoilDownloader
import com.looker.droidify.screen.MessageDialog import com.looker.droidify.screen.MessageDialog
import com.looker.droidify.screen.ScreenshotsFragment import com.looker.droidify.screen.ScreenshotsFragment
import com.looker.droidify.service.Connection import com.looker.droidify.service.Connection
import com.looker.droidify.service.DownloadService import com.looker.droidify.service.DownloadService
import com.looker.droidify.ui.activities.MainActivityX 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.theme.AppTheme
import com.looker.droidify.ui.compose.utils.Callbacks import com.looker.droidify.ui.compose.utils.Callbacks
import com.looker.droidify.ui.viewmodels.AppViewModelX import com.looker.droidify.ui.viewmodels.AppViewModelX
import com.looker.droidify.utility.Utils.rootInstallerEnabled import com.looker.droidify.utility.Utils.rootInstallerEnabled
import com.looker.droidify.utility.Utils.startUpdate import com.looker.droidify.utility.Utils.startUpdate
import com.looker.droidify.utility.extension.android.Android 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.findSuggestedProduct
import com.looker.droidify.utility.generateLinks
import com.looker.droidify.utility.generatePermissionGroups
import com.looker.droidify.utility.isDarkTheme import com.looker.droidify.utility.isDarkTheme
import com.looker.droidify.utility.onLaunchClick import com.looker.droidify.utility.onLaunchClick
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -384,6 +419,238 @@ class AppSheetX() : FullscreenBottomSheetDialogFragment(), Callbacks {
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AppSheet() { 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)
}
}
}
}
}
} }
} }

View File

@ -163,6 +163,7 @@
<string name="update">Update</string> <string name="update">Update</string>
<string name="updates">Updates</string> <string name="updates">Updates</string>
<string name="upstream_source_code_is_not_free">The upstream source code is not libre</string> <string name="upstream_source_code_is_not_free">The upstream source code is not libre</string>
<string name="not_safe_for_work">NSFW (may contain disturbing content)</string>
<string name="username">Username</string> <string name="username">Username</string>
<string name="username_missing">Username missing</string> <string name="username_missing">Username missing</string>
<string name="validation_index_error_DESC">Could not validate index.</string> <string name="validation_index_error_DESC">Could not validate index.</string>