package details screen and other various fixes and improvements

This commit is contained in:
2023-07-14 08:34:45 +03:00
parent b233d44c5c
commit 2178a0c9a4
14 changed files with 778 additions and 47 deletions
@@ -0,0 +1,8 @@
package com.illusionware.npsbrowser
sealed class Routes(val route: String) {
object Home : Routes("home")
object Onboarding : Routes("onboarding")
object Settings : Routes("settings")
object PackageDetails : Routes("package_details")
}
@@ -46,6 +46,7 @@ import androidx.compose.material3.TextButton
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@@ -82,19 +83,24 @@ import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.Routes
import com.illusionware.npsbrowser.data.ItemLayout
import com.illusionware.npsbrowser.data.SettingsPreferences
import com.illusionware.npsbrowser.model.ConsoleType
import com.illusionware.npsbrowser.model.PackageItem
import com.illusionware.npsbrowser.ui.components.*
import com.illusionware.npsbrowser.ui.screens.OnBoarding
import com.illusionware.npsbrowser.ui.screens.PackageDetails
import com.illusionware.npsbrowser.ui.screens.SettingsScreen
import com.illusionware.npsbrowser.ui.theme.ColorAccent
import com.illusionware.npsbrowser.ui.theme.ColorOnPrimaryLight
import com.illusionware.npsbrowser.ui.theme.NPSBrowserTheme
import com.illusionware.npsbrowser.ui.theme.Typography
import com.illusionware.npsbrowser.util.isNavigationBarNeedsScrim
import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
import com.illusionware.npsbrowser.viewmodels.PackageDetailsViewModel
import com.illusionware.npsbrowser.viewmodels.PackageListViewModel
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import kotlinx.coroutines.runBlocking
@@ -116,6 +122,7 @@ class MainActivity : ComponentActivity() {
val onboardingViewModel: OnboardingViewModel = viewModel(
factory = OnboardingViewModel.Factory
)
val packageDetailsViewModel: PackageDetailsViewModel = viewModel()
val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
val appTheme = runBlocking { settingsViewModel.getAppTheme() }
@@ -135,29 +142,39 @@ class MainActivity : ComponentActivity() {
) {
NavHost(
navController = navController,
startDestination = if (onboardingPrefs.seenOnboarding) "Home" else "Onboarding",
startDestination = if (onboardingPrefs.seenOnboarding) Routes.Home.route else Routes.Onboarding.route,
) {
composable(route = "Home") {
composable(Routes.Home.route) {
HomePage(
navigateToSettings = { navController.navigate("Settings") },
navigateToSettings = { navController.navigate(Routes.Settings.route) },
settingsPrefs = settingsPrefs,
settingsViewModel = settingsViewModel,
navigateToDetailsScreen = { item ->
packageDetailsViewModel.setSelectedPackage(item)
navController.navigate(Routes.PackageDetails.route)
},
)
}
composable(route = "Settings") {
composable(Routes.Settings.route) {
SettingsScreen(
navigationGoBack = { navController.popBackStack() },
)
}
composable(route = "Onboarding") {
composable(Routes.Onboarding.route) {
OnBoarding(
navigateToHome = {
navController.popBackStack()
navController.navigate("Home")
navController.navigate(Routes.Home.route)
},
darkTheme = darkTheme,
)
}
composable(Routes.PackageDetails.route) {
PackageDetails(
navigationGoBack = { navController.popBackStack() },
packageDetailsViewModel = packageDetailsViewModel,
)
}
}
}
}
@@ -168,7 +185,8 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomePage(
navigateToSettings: () -> Unit = {},
navigateToSettings: () -> Unit,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
settingsPrefs: SettingsPreferences,
settingsViewModel: SettingsViewModel,
) {
@@ -176,9 +194,22 @@ fun HomePage(
var searchQuery by rememberSaveable { mutableStateOf("")}
val searchBar = remember { FocusRequester() }
var layoutDialogOpen by remember { mutableStateOf(false) }
val context = LocalContext.current
val systemUiController = rememberSystemUiController()
val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
val tsvs = settingsViewModel.availableTsvs()
var selectedBarItem by remember { mutableStateOf(ConsoleType.PSVITA) }
var selectedBarItem by rememberSaveable { mutableStateOf(ConsoleType.PSVITA) }
LaunchedEffect(systemUiController) {
systemUiController.setNavigationBarColor(
color = if (context.isNavigationBarNeedsScrim()) {
navbarScrimColor.copy(alpha = 0.7f)
} else {
Color.Transparent
}
)
}
Scaffold(
topBar = {
@@ -272,7 +303,11 @@ fun HomePage(
if (tsvs.isEmpty()) {
NoTsvs(navigateToSettings = navigateToSettings)
} else {
PackageList(selectedBarItem, settingsPrefs = settingsPrefs)
PackageList(
navigateToDetailsScreen = navigateToDetailsScreen,
type = selectedBarItem,
settingsPrefs = settingsPrefs
)
}
}
@@ -375,6 +410,7 @@ fun NoTsvs(padding: PaddingValues = PaddingValues(0.dp), navigateToSettings: ()
@Composable
fun PackageList(
type: ConsoleType = ConsoleType.PSVITA,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
viewModel: PackageListViewModel = viewModel(
factory = PackageListViewModel.Factory
),
@@ -429,7 +465,7 @@ fun PackageList(
if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
LazyColumn {
items(packages) {
PackageListItem(it)
PackageListItem(it, navigateToDetailsScreen)
}
}
} else {
@@ -438,7 +474,7 @@ fun PackageList(
contentPadding = PaddingValues(8.dp),
) {
items(packages) {
PackageGridItem(it)
PackageGridItem(it, navigateToDetailsScreen)
}
}
}
@@ -448,11 +484,15 @@ fun PackageList(
}
@Composable
fun PackageListItem(item: PackageItem) {
fun PackageListItem(
item: PackageItem,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
) {
Box(
Modifier
.fillMaxWidth()
.clickable { /* TODO: */ }) {
.clickable { navigateToDetailsScreen(item) }
) {
Row(
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
@@ -500,8 +540,8 @@ fun PackageListItem(item: PackageItem) {
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
Text(text = item.region)
if (item.minFW.isNotEmpty()) {
Text(text = String.format("%.2f", item.minFW.toFloat()))
if (!item.minFW.isNullOrEmpty()) {
Text(text = item.minFW)
}
if (item.pkgSize.isNotEmpty()) {
Text(text = item.pkgSize)
@@ -512,14 +552,16 @@ fun PackageListItem(item: PackageItem) {
}
}
Column {
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
if (item.pkgUrl.isNullOrEmpty()) {
Icon(
painter = painterResource(id = R.drawable.no_url_icon),
contentDescription = "No PKG URL",
tint = Color(0xFFCB5A5A)
)
}
if (item.consoleType != ConsoleType.PSX && (item.zRif.isEmpty() && item.rap.isEmpty())) {
if (
item.consoleType != ConsoleType.PSX &&
(item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty())) {
Icon(
painter = painterResource(id = R.drawable.no_key_icon),
contentDescription = "No License Key",
@@ -553,17 +595,20 @@ fun Tag(title: String, small: Boolean = false) {
}
@Composable
fun PackageGridItem(item: PackageItem) {
fun PackageGridItem(
item: PackageItem,
navigateToDetailsScreen: (item: PackageItem) -> Unit,
) {
Box(
Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(8.dp))
.clickable { /* TODO: */ }
.clickable { navigateToDetailsScreen(item) },
) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.Start,
modifier = Modifier.padding(8.dp)
modifier = Modifier.padding(8.dp),
) {
Layout(
content = {
@@ -607,8 +652,8 @@ fun PackageGridItem(item: PackageItem) {
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
Text(text = item.region)
if (item.minFW.isNotEmpty()) {
Text(text = String.format("%.2f", item.minFW.toFloat()))
if (!item.minFW.isNullOrEmpty()) {
Text(text = item.minFW)
}
if (item.pkgSize.isNotEmpty()) {
Text(text = item.pkgSize)
@@ -616,14 +661,14 @@ fun PackageGridItem(item: PackageItem) {
}
}
Row {
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
if (item.pkgUrl.isNullOrEmpty()) {
Icon(
painter = painterResource(id = R.drawable.no_url_icon),
contentDescription = "No PKG URL",
tint = Color(0xFFCB5A5A)
)
}
if (item.zRif.isEmpty() && item.rap.isEmpty()) {
if (item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty()) {
Icon(
painter = painterResource(id = R.drawable.no_key_icon),
contentDescription = "No License Key",
@@ -4,14 +4,14 @@ data class PackageItem(
val titleId: String,
val region: String,
val name: String,
val pkgUrl: String,
val contentId: String,
val modificationDate: String,
val pkgUrl: String?,
val contentId: String?,
val modificationDate: String?,
val pkgSize: String,
val sha256: String,
val zRif: String,
val rap: String,
val minFW: String,
val sha256: String?,
val zRif: String?,
val rap: String?,
val minFW: String?,
val consoleType: ConsoleType,
val dataType: PackageItemType,
@@ -0,0 +1,95 @@
package com.illusionware.npsbrowser.ui.components
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Eco
import androidx.compose.material3.FloatingActionButtonDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.onClick
import androidx.compose.ui.semantics.onLongClick
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun NPSFloatingActionButton(
modifier: Modifier = Modifier,
enabled: Boolean = true,
onClick: () -> Unit,
onLongClick: (() -> Unit)? = null,
shape: Shape = FloatingActionButtonDefaults.shape,
containerColor: Color = FloatingActionButtonDefaults.containerColor,
contentColor: Color = MaterialTheme.colorScheme.primary,
elevation: Dp = 6.dp,
content: @Composable () -> Unit,
) {
val haptic = LocalHapticFeedback.current
Surface(
modifier = modifier
.alpha(if (enabled) 1f else 0.5f)
.then(if (enabled) Modifier.shadow(6.dp, shape) else Modifier.clip(shape))
.then(if (enabled) Modifier.combinedClickable(
onClick = { onClick() },
onLongClick = {
if (onLongClick != null) {
onLongClick()
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
}
) else Modifier)
.semantics(
mergeDescendants = true,
properties = {
if (!enabled) disabled()
onClick { onClick(); true }
if (onLongClick != null) {
onLongClick { onLongClick(); true }
}
role = Role.Button
},
),
color = containerColor,
tonalElevation = 3.dp,
shadowElevation = elevation,
contentColor = contentColor,
) {
Box(
modifier = Modifier.padding(16.dp)
) {
Row {
content()
}
}
}
}
@Composable
@Preview
fun NPSFABPreview() {
NPSFloatingActionButton(
onClick = {},
) {
Icon(imageVector = Icons.Outlined.Eco, contentDescription = "Eco")
}
}
@@ -0,0 +1,456 @@
package com.illusionware.npsbrowser.ui.screens
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.DownloadDone
import androidx.compose.material.icons.filled.Public
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SheetState
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.ColorPainter
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.activities.PlaceholderColor
import com.illusionware.npsbrowser.data.ItemLayout
import com.illusionware.npsbrowser.data.SettingsPreferences
import com.illusionware.npsbrowser.data.Theme
import com.illusionware.npsbrowser.ui.components.NPSAlertDialog
import com.illusionware.npsbrowser.ui.components.NPSFloatingActionButton
import com.illusionware.npsbrowser.ui.components.NPSIconButton
import com.illusionware.npsbrowser.ui.theme.ColorSecondaryLight
import com.illusionware.npsbrowser.ui.theme.Typography
import com.illusionware.npsbrowser.viewmodels.PackageDetailsViewModel
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PackageDetails(
navigationGoBack: () -> Unit = {},
settingsViewModel: SettingsViewModel = viewModel(
factory = SettingsViewModel.Factory
),
packageDetailsViewModel: PackageDetailsViewModel,
) {
val clipboard = LocalClipboardManager.current
val sheetState = rememberModalBottomSheetState(
skipPartiallyExpanded = true
)
val snackbarHostState = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
val prefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val item = packageDetailsViewModel.uiState.collectAsStateWithLifecycle().value.item!!
var showBottomSheet by remember { mutableStateOf(false) }
Scaffold(
snackbarHost = { SnackbarHost(snackbarHostState)},
topBar = {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.statusBarsPadding()
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
NPSIconButton(tooltip = "Go Back", onClick = navigationGoBack) {
Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "Go Back" )
}
}
},
floatingActionButton = {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
NPSFloatingActionButton(
onClick = {
if (!item.zRif.isNullOrEmpty()) {
clipboard.setText(AnnotatedString(item.zRif))
} else if (!item.rap.isNullOrEmpty()) {
clipboard.setText(AnnotatedString(item.rap))
}
scope.launch {
snackbarHostState.showSnackbar("License key copied to clipboard")
}
},
enabled = !item.zRif.isNullOrEmpty() || !item.rap.isNullOrEmpty()
) {
Icon(
painter = painterResource(id = R.drawable.key_copy_fill),
contentDescription = "Copy License Key"
)
}
NPSFloatingActionButton(
onClick = { /* TODO: */ },
onLongClick = { showBottomSheet = true },
enabled = !item.pkgUrl.isNullOrEmpty()
) {
Icon(imageVector = Icons.Filled.Download, contentDescription = "Download")
}
}
},
) { padding ->
Column(Modifier.padding(padding)) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
.data("TODO")
.crossfade(true)
.error(R.drawable.default_game_icon)
.build(),
placeholder = ColorPainter(PlaceholderColor),
contentDescription = "Package Icon",
modifier = Modifier
.aspectRatio(1f / 1f)
.fillMaxWidth(),
contentScale = ContentScale.Crop,
)
Column(Modifier.padding(16.dp)) {
Text(
text = "[${item.titleId}] ${item.name}",
style = Typography.headlineSmall.copy(fontSize = 20.sp),
fontWeight = FontWeight.Light,
)
if (!item.contentId.isNullOrEmpty()) {
Spacer(modifier = Modifier.padding(4.dp))
Text(
text = item.contentId,
style = Typography.bodyMedium,
color = MaterialTheme.colorScheme.outline
)
}
Spacer(modifier = Modifier.padding(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
if (item.region.isNotEmpty()) {
Info(
label = item.region,
iconDescription = "Region",
icon = Icons.Filled.Public
)
}
if (item.pkgSize.isNotEmpty()) {
Info(
label = item.pkgSize,
iconDescription = "Package",
icon = painterResource(id = R.drawable.package_ic),
)
}
if (!item.minFW.isNullOrEmpty()) {
Info(
label = item.minFW,
iconDescription = "Minimum Firmware",
icon = painterResource(id = R.drawable.gear),
)
}
}
}
}
}
if (showBottomSheet) {
OneTimeConfigBottomSheet(
onDismiss = {
scope.launch {
sheetState.hide()
showBottomSheet = false
}
},
sheetState = sheetState,
prefs = prefs,
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OneTimeConfigBottomSheet(
sheetState: SheetState,
onDismiss: () -> Unit,
prefs: SettingsPreferences
) {
ModalBottomSheet(
onDismissRequest = onDismiss,
sheetState = sheetState,
windowInsets = WindowInsets(top = 0.dp),
) {
OneTimeConfigSheetContent(prefs)
}
}
@Composable
fun Info(label: String, iconDescription: String, icon: Painter) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painter = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Text(
text = label,
style = Typography.bodyMedium,
)
}
}
@Composable
fun Info(label: String, iconDescription: String, icon: ImageVector) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = iconDescription,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
)
Text(
text = label,
style = Typography.bodyMedium,
)
}
}
@Composable
fun Option(
title: String,
value: String,
onClick: () -> Unit,
) {
Box(
Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 16.dp, horizontal = 24.dp)
) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight, fontSize = 14.sp)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OneTimeConfigSheetContent(prefs: SettingsPreferences) {
var autoDecrypt by remember { mutableStateOf(prefs.pkg2zipAutoDecrypt) }
var unpackInPlace by remember { mutableStateOf(prefs.unpackInDownload) }
var deleteAfter by remember { mutableStateOf(prefs.deleteAfterUnpack) }
var downloadDir by remember { mutableStateOf(Uri.decode(prefs.downloadDir)) }
var decParamsDialog by remember { mutableStateOf(false) }
var unpackDir by remember { mutableStateOf(Uri.decode(prefs.unpackDir)) }
var pkg2zipParams by remember { mutableStateOf(prefs.pkg2zipParams) }
var displayPkg2zipParams by remember { mutableStateOf(prefs.pkg2zipParams) }
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
downloadDir = dirUri.toString()
}
}
val unpackDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
unpackDir = dirUri.toString()
}
}
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.navigationBarsPadding()
) {
Text(
text = "One-time Configuration",
textAlign = TextAlign.Center,
style = Typography.titleLarge
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.horizontalScroll(rememberScrollState())
.padding(horizontal = 24.dp)
) {
FilterChip(
label = { Text(text = "Decrypt automatically") },
selected = autoDecrypt,
onClick = { autoDecrypt = !autoDecrypt }
)
FilterChip(
label = { Text(text = "Unpack in place") },
selected = unpackInPlace,
onClick = { unpackInPlace = !unpackInPlace },
)
FilterChip(
label = { Text(text = "Delete after") },
selected = deleteAfter,
onClick = { deleteAfter = !deleteAfter },
)
}
Column(
modifier = Modifier.padding(bottom = 16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
horizontalAlignment = Alignment.End,
) {
Column {
Option(
title = "Download Directory",
value = Uri.decode(downloadDir).ifEmpty { "None" },
onClick = {
downloadDirPicker.launch(Uri.EMPTY)
}
)
AnimatedVisibility(
visible = !unpackInPlace
) {
Option(
title = "Unpack Directory",
value = Uri.decode(unpackDir).ifEmpty { "None" },
onClick = {
unpackDirPicker.launch(Uri.EMPTY)
}
)
}
AnimatedVisibility(
visible = autoDecrypt
) {
Option(
title = "Decryption Parameters",
value = pkg2zipParams,
onClick = { decParamsDialog = true }
)
}
}
Button(
onClick = { /*TODO: do the download*/ },
modifier = Modifier.padding(horizontal = 24.dp),
) {
Icon(
imageVector = Icons.Filled.DownloadDone,
contentDescription = "Download",
Modifier.size(20.dp)
)
Spacer(modifier = Modifier.size(8.dp))
Text("Download")
}
}
}
if (decParamsDialog) {
NPSAlertDialog(
onDismiss = {
decParamsDialog = false
displayPkg2zipParams = pkg2zipParams
},
title = "Decryption Parameters",
buttons = {
TextButton(onClick = {
pkg2zipParams = displayPkg2zipParams
decParamsDialog = false
}) {
Text(text = "OK")
}
}
) {
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
value = displayPkg2zipParams,
label = { Text(text = "Decryption Parameters") },
onValueChange = {
displayPkg2zipParams = it
},
)
}
}
}
@Composable
@Preview(showBackground = true, backgroundColor = 0xFFFFFFFF)
fun BottomSheetPreview() {
val prefs = SettingsPreferences(
Theme.SYSTEM.ordinal,
ItemLayout.LIST.ordinal,
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
false,
false,
"",
true,
)
Column {
OneTimeConfigSheetContent(prefs = prefs)
}
}
@@ -13,7 +13,9 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
@@ -98,12 +101,18 @@ fun SettingsScreen(
Text(text = stringResource(id = R.string.title_activity_settings), style = Typography.titleLarge)
}
},
contentWindowInsets = WindowInsets(bottom = 0.dp),
) { padding ->
Column(modifier = Modifier
.padding(padding)
.verticalScroll(rememberScrollState())
) {
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
modifier = Modifier
.padding(vertical = 16.dp)
.navigationBarsPadding()
) {
Appearance(viewModel, prefs)
TsvFiles(viewModel, prefs)
Updates(viewModel, prefs)
@@ -366,6 +375,7 @@ fun Downloads(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
title = "Unpack Directory",
value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
icon = Icons.Outlined.Unarchive,
enabled = !prefs.unpackInDownload,
onClick = {
unpackDirPicker.launch(Uri.EMPTY)
}
@@ -514,12 +524,14 @@ fun DialogSetting(
value: String,
icon: ImageVector?,
onClick: () -> Unit,
singleLine : Boolean = false
singleLine : Boolean = false,
enabled: Boolean = true
) {
Box(
Modifier
.fillMaxWidth()
.clickable { onClick() }
.alpha(if (enabled) 1f else 0.5f)
.then(if (enabled) Modifier.clickable { onClick() } else Modifier)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -98,12 +98,12 @@ fun NPSBrowserTheme(
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = Color.Transparent.toArgb()
window.navigationBarColor = Color.Transparent.toArgb()
// allows pages to extend to the status bar
WindowCompat.setDecorFitsSystemWindows(window, false)
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
@@ -0,0 +1,10 @@
package com.illusionware.npsbrowser.util
import android.content.Context
import android.os.Build
// Borrowed from https://github.com/tachiyomiorg/tachiyomi/tree/master
fun Context.isNavigationBarNeedsScrim(): Boolean {
return Build.VERSION.SDK_INT < Build.VERSION_CODES.Q ||
InternalResourceHelper.getBoolean(this, "config_navBarNeedsScrim", true)
}
@@ -0,0 +1,28 @@
package com.illusionware.npsbrowser.util
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
// Borrowed from https://github.com/tachiyomiorg/tachiyomi/tree/master
object InternalResourceHelper {
fun getBoolean(context: Context, resName: String, defaultValue: Boolean): Boolean {
val id = getResourceId(resName, "bool")
return if (id != 0) {
context.createPackageContext("android", 0).resources.getBoolean(id)
} else {
defaultValue
}
}
/**
* Get resource id from system resources
* @param resName resource name to get
* @param type resource type of [resName] to get
* @return 0 if not available
*/
@SuppressLint("DiscouragedApi")
private fun getResourceId(resName: String, type: String): Int {
return Resources.getSystem().getIdentifier(resName, type, "android")
}
}
@@ -0,0 +1,20 @@
package com.illusionware.npsbrowser.viewmodels
import androidx.lifecycle.ViewModel
import com.illusionware.npsbrowser.model.PackageItem
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
data class SelectedPackageUiState(
val item: PackageItem?,
)
class PackageDetailsViewModel: ViewModel() {
private val _uiState = MutableStateFlow(SelectedPackageUiState(null))
val uiState: StateFlow<SelectedPackageUiState> = _uiState.asStateFlow()
fun setSelectedPackage(item: PackageItem) {
_uiState.value = SelectedPackageUiState(item)
}
}
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import okhttp3.internal.toLongOrDefault
import java.io.InputStream
data class PackageListUiState(
@@ -91,24 +92,64 @@ class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesReposit
val tsv = tsvReader.readAllWithHeader(file!!)
tsv.forEach { row ->
var size = ""
val tempSize = row["File Size"]
if (!tempSize.isNullOrEmpty()) {
size = bytesIntoHumanReadable(tempSize.toLong())
if (row["Name"].isNullOrEmpty()) {
return@forEach
}
var size = ""
var minFW: String? = null
var pkgUrl: String? = null
var zRif: String? = null
var rap: String? = null
var contentId: String? = null
var sha256: String? = null
var modificationDate: String? = null
val tempSize = row["File Size"]
val tempFW = row["Required FW"]
val tempUrl = row["PKG direct link"]
val tempZRif = row["zRIF"]
val tempRap = row["RAP"]
val tempContentId = row["Content ID"]
val tempsha256 = row["SHA256"]
val tempDate = row["Last Modification Date"]
if (!tempSize.isNullOrEmpty()) {
size = bytesIntoHumanReadable(tempSize.toLongOrDefault(0))
}
if (!tempFW.isNullOrEmpty()) {
minFW = String.format("%.2f", tempFW.toFloat())
}
if (!tempUrl.isNullOrEmpty() && tempUrl != "MISSING") {
pkgUrl = tempUrl
}
if (!tempZRif.isNullOrEmpty() && tempZRif != "MISSING") {
zRif = tempZRif
}
if (!tempRap.isNullOrEmpty() && tempRap != "MISSING") {
rap = tempRap
}
if (!tempContentId.isNullOrEmpty()) {
contentId = tempContentId
}
if (!tempsha256.isNullOrEmpty()) {
sha256 = tempsha256
}
if (!tempDate.isNullOrEmpty()) {
modificationDate = tempDate
}
list.add(
PackageItem(
row["Title ID"] ?: "",
row["Region"] ?: "",
row["Name"] ?: "",
row["PKG direct link"] ?: "",
row["Content ID"] ?: "",
row["Last Modification Date"] ?: "",
row["Name"]?.trim() ?: "",
pkgUrl,
contentId,
modificationDate,
size,
row["SHA256"] ?: "",
row["zRIF"] ?: "",
row["RAP"] ?: "",
row["Required FW"] ?: "",
sha256,
zRif,
rap,
minFW,
console,
type,
)
File diff suppressed because one or more lines are too long
@@ -0,0 +1,7 @@
<vector android:height="24dp" android:viewportHeight="256"
android:viewportWidth="256" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000"
android:pathData="m139.8,13.09a72.78,72.78 0,0 0,-69.17 95.25l-52.33,52.31a7.27,7.27 0,0 0,-2.13 5.16v36.36a7.27,7.27 0,0 0,7.27 7.27h12.95v-24.08a7.27,7.27 0,0 1,2.14 -5.14l52.31,-52.33A72.78,72.78 0,0 1,160.02 32.66,72.72 72.72,0 0,1 194.86,43 72.72,72.72 0,0 0,139.8 13.09ZM178.52,73.52a14.54,14.54 0,0 0,-9.95 3.66,14.54 14.54,0 0,1 -0.27,0.64 14.54,14.54 0,0 0,5.89 22,15.7 15.7,0 0,1 -0.98,-4.92 15.7,15.7 0,0 1,15.7 -15.69,15.7 15.7,0 0,1 0.16,0.02 14.54,14.54 0,0 0,-10.55 -5.7z" android:strokeWidth="0.908993"/>
<path android:fillColor="#000000"
android:pathData="M169.69,40.2A72.78,72.78 0,0 0,100.52 135.45l-52.33,52.32a7.27,7.27 0,0 0,-2.13 5.14v36.36a7.27,7.27 0,0 0,7.27 7.27h36.36a7.27,7.27 0,0 0,7.27 -7.27v-14.54h14.54a7.27,7.27 0,0 0,7.27 -7.27v-14.54h14.54a7.27,7.27 0,0 0,5.14 -2.13l8.69,-8.7A72.72,72.72 0,1 0,169.69 40.2ZM187.87,109.29a14.54,14.54 0,1 1,14.54 -14.54,14.54 14.54,0 0,1 -14.54,14.54z" android:strokeWidth="0.908993"/>
</vector>
+4
View File
@@ -0,0 +1,4 @@
<vector android:height="24dp" android:viewportHeight="256"
android:viewportWidth="256" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#000000" android:pathData="M223.68,66.15 L135.68,18a15.88,15.88 0,0 0,-15.36 0l-88,48.17a16,16 0,0 0,-8.32 14v95.64a16,16 0,0 0,8.32 14l88,48.17a15.88,15.88 0,0 0,15.36 0l88,-48.17a16,16 0,0 0,8.32 -14L232,80.18A16,16 0,0 0,223.68 66.15ZM128,32l80.35,44L178.57,92.29l-80.35,-44ZM128,120L47.65,76 81.56,57.43l80.35,44ZM216,175.85h0l-80,43.79L136,133.83l32,-17.51L168,152a8,8 0,0 0,16 0L184,107.56l32,-17.51v85.76Z"/>
</vector>