mirror of
https://github.com/IllusionMan1212/NPS-Browser.git
synced 2026-10-24 02:12:37 +00:00
package details screen and other various fixes and improvements
This commit is contained in:
@@ -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.TextField
|
||||||
import androidx.compose.material3.TextFieldDefaults
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -82,19 +83,24 @@ import androidx.navigation.compose.composable
|
|||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import coil.request.ImageRequest
|
import coil.request.ImageRequest
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import com.illusionware.npsbrowser.R
|
import com.illusionware.npsbrowser.R
|
||||||
|
import com.illusionware.npsbrowser.Routes
|
||||||
import com.illusionware.npsbrowser.data.ItemLayout
|
import com.illusionware.npsbrowser.data.ItemLayout
|
||||||
import com.illusionware.npsbrowser.data.SettingsPreferences
|
import com.illusionware.npsbrowser.data.SettingsPreferences
|
||||||
import com.illusionware.npsbrowser.model.ConsoleType
|
import com.illusionware.npsbrowser.model.ConsoleType
|
||||||
import com.illusionware.npsbrowser.model.PackageItem
|
import com.illusionware.npsbrowser.model.PackageItem
|
||||||
import com.illusionware.npsbrowser.ui.components.*
|
import com.illusionware.npsbrowser.ui.components.*
|
||||||
import com.illusionware.npsbrowser.ui.screens.OnBoarding
|
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.screens.SettingsScreen
|
||||||
import com.illusionware.npsbrowser.ui.theme.ColorAccent
|
import com.illusionware.npsbrowser.ui.theme.ColorAccent
|
||||||
import com.illusionware.npsbrowser.ui.theme.ColorOnPrimaryLight
|
import com.illusionware.npsbrowser.ui.theme.ColorOnPrimaryLight
|
||||||
import com.illusionware.npsbrowser.ui.theme.NPSBrowserTheme
|
import com.illusionware.npsbrowser.ui.theme.NPSBrowserTheme
|
||||||
import com.illusionware.npsbrowser.ui.theme.Typography
|
import com.illusionware.npsbrowser.ui.theme.Typography
|
||||||
|
import com.illusionware.npsbrowser.util.isNavigationBarNeedsScrim
|
||||||
import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
|
import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
|
||||||
|
import com.illusionware.npsbrowser.viewmodels.PackageDetailsViewModel
|
||||||
import com.illusionware.npsbrowser.viewmodels.PackageListViewModel
|
import com.illusionware.npsbrowser.viewmodels.PackageListViewModel
|
||||||
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
|
import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
@@ -116,6 +122,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
val onboardingViewModel: OnboardingViewModel = viewModel(
|
val onboardingViewModel: OnboardingViewModel = viewModel(
|
||||||
factory = OnboardingViewModel.Factory
|
factory = OnboardingViewModel.Factory
|
||||||
)
|
)
|
||||||
|
val packageDetailsViewModel: PackageDetailsViewModel = viewModel()
|
||||||
|
|
||||||
val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
|
val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
|
||||||
val appTheme = runBlocking { settingsViewModel.getAppTheme() }
|
val appTheme = runBlocking { settingsViewModel.getAppTheme() }
|
||||||
@@ -135,29 +142,39 @@ class MainActivity : ComponentActivity() {
|
|||||||
) {
|
) {
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
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(
|
HomePage(
|
||||||
navigateToSettings = { navController.navigate("Settings") },
|
navigateToSettings = { navController.navigate(Routes.Settings.route) },
|
||||||
settingsPrefs = settingsPrefs,
|
settingsPrefs = settingsPrefs,
|
||||||
settingsViewModel = settingsViewModel,
|
settingsViewModel = settingsViewModel,
|
||||||
|
navigateToDetailsScreen = { item ->
|
||||||
|
packageDetailsViewModel.setSelectedPackage(item)
|
||||||
|
navController.navigate(Routes.PackageDetails.route)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(route = "Settings") {
|
composable(Routes.Settings.route) {
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
navigationGoBack = { navController.popBackStack() },
|
navigationGoBack = { navController.popBackStack() },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(route = "Onboarding") {
|
composable(Routes.Onboarding.route) {
|
||||||
OnBoarding(
|
OnBoarding(
|
||||||
navigateToHome = {
|
navigateToHome = {
|
||||||
navController.popBackStack()
|
navController.popBackStack()
|
||||||
navController.navigate("Home")
|
navController.navigate(Routes.Home.route)
|
||||||
},
|
},
|
||||||
darkTheme = darkTheme,
|
darkTheme = darkTheme,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
composable(Routes.PackageDetails.route) {
|
||||||
|
PackageDetails(
|
||||||
|
navigationGoBack = { navController.popBackStack() },
|
||||||
|
packageDetailsViewModel = packageDetailsViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,7 +185,8 @@ class MainActivity : ComponentActivity() {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomePage(
|
fun HomePage(
|
||||||
navigateToSettings: () -> Unit = {},
|
navigateToSettings: () -> Unit,
|
||||||
|
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||||
settingsPrefs: SettingsPreferences,
|
settingsPrefs: SettingsPreferences,
|
||||||
settingsViewModel: SettingsViewModel,
|
settingsViewModel: SettingsViewModel,
|
||||||
) {
|
) {
|
||||||
@@ -176,9 +194,22 @@ fun HomePage(
|
|||||||
var searchQuery by rememberSaveable { mutableStateOf("")}
|
var searchQuery by rememberSaveable { mutableStateOf("")}
|
||||||
val searchBar = remember { FocusRequester() }
|
val searchBar = remember { FocusRequester() }
|
||||||
var layoutDialogOpen by remember { mutableStateOf(false) }
|
var layoutDialogOpen by remember { mutableStateOf(false) }
|
||||||
|
val context = LocalContext.current
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
val navbarScrimColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp)
|
||||||
|
|
||||||
val tsvs = settingsViewModel.availableTsvs()
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
@@ -272,7 +303,11 @@ fun HomePage(
|
|||||||
if (tsvs.isEmpty()) {
|
if (tsvs.isEmpty()) {
|
||||||
NoTsvs(navigateToSettings = navigateToSettings)
|
NoTsvs(navigateToSettings = navigateToSettings)
|
||||||
} else {
|
} 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
|
@Composable
|
||||||
fun PackageList(
|
fun PackageList(
|
||||||
type: ConsoleType = ConsoleType.PSVITA,
|
type: ConsoleType = ConsoleType.PSVITA,
|
||||||
|
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||||
viewModel: PackageListViewModel = viewModel(
|
viewModel: PackageListViewModel = viewModel(
|
||||||
factory = PackageListViewModel.Factory
|
factory = PackageListViewModel.Factory
|
||||||
),
|
),
|
||||||
@@ -429,7 +465,7 @@ fun PackageList(
|
|||||||
if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
|
if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
|
||||||
LazyColumn {
|
LazyColumn {
|
||||||
items(packages) {
|
items(packages) {
|
||||||
PackageListItem(it)
|
PackageListItem(it, navigateToDetailsScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -438,7 +474,7 @@ fun PackageList(
|
|||||||
contentPadding = PaddingValues(8.dp),
|
contentPadding = PaddingValues(8.dp),
|
||||||
) {
|
) {
|
||||||
items(packages) {
|
items(packages) {
|
||||||
PackageGridItem(it)
|
PackageGridItem(it, navigateToDetailsScreen)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,11 +484,15 @@ fun PackageList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PackageListItem(item: PackageItem) {
|
fun PackageListItem(
|
||||||
|
item: PackageItem,
|
||||||
|
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { /* TODO: */ }) {
|
.clickable { navigateToDetailsScreen(item) }
|
||||||
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 16.dp, vertical = 8.dp)
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
@@ -500,8 +540,8 @@ fun PackageListItem(item: PackageItem) {
|
|||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
||||||
Text(text = item.region)
|
Text(text = item.region)
|
||||||
if (item.minFW.isNotEmpty()) {
|
if (!item.minFW.isNullOrEmpty()) {
|
||||||
Text(text = String.format("%.2f", item.minFW.toFloat()))
|
Text(text = item.minFW)
|
||||||
}
|
}
|
||||||
if (item.pkgSize.isNotEmpty()) {
|
if (item.pkgSize.isNotEmpty()) {
|
||||||
Text(text = item.pkgSize)
|
Text(text = item.pkgSize)
|
||||||
@@ -512,14 +552,16 @@ fun PackageListItem(item: PackageItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Column {
|
Column {
|
||||||
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
|
if (item.pkgUrl.isNullOrEmpty()) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.no_url_icon),
|
painter = painterResource(id = R.drawable.no_url_icon),
|
||||||
contentDescription = "No PKG URL",
|
contentDescription = "No PKG URL",
|
||||||
tint = Color(0xFFCB5A5A)
|
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(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.no_key_icon),
|
painter = painterResource(id = R.drawable.no_key_icon),
|
||||||
contentDescription = "No License Key",
|
contentDescription = "No License Key",
|
||||||
@@ -553,17 +595,20 @@ fun Tag(title: String, small: Boolean = false) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PackageGridItem(item: PackageItem) {
|
fun PackageGridItem(
|
||||||
|
item: PackageItem,
|
||||||
|
navigateToDetailsScreen: (item: PackageItem) -> Unit,
|
||||||
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable { /* TODO: */ }
|
.clickable { navigateToDetailsScreen(item) },
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalAlignment = Alignment.Start,
|
horizontalAlignment = Alignment.Start,
|
||||||
modifier = Modifier.padding(8.dp)
|
modifier = Modifier.padding(8.dp),
|
||||||
) {
|
) {
|
||||||
Layout(
|
Layout(
|
||||||
content = {
|
content = {
|
||||||
@@ -607,8 +652,8 @@ fun PackageGridItem(item: PackageItem) {
|
|||||||
) {
|
) {
|
||||||
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
|
||||||
Text(text = item.region)
|
Text(text = item.region)
|
||||||
if (item.minFW.isNotEmpty()) {
|
if (!item.minFW.isNullOrEmpty()) {
|
||||||
Text(text = String.format("%.2f", item.minFW.toFloat()))
|
Text(text = item.minFW)
|
||||||
}
|
}
|
||||||
if (item.pkgSize.isNotEmpty()) {
|
if (item.pkgSize.isNotEmpty()) {
|
||||||
Text(text = item.pkgSize)
|
Text(text = item.pkgSize)
|
||||||
@@ -616,14 +661,14 @@ fun PackageGridItem(item: PackageItem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row {
|
Row {
|
||||||
if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
|
if (item.pkgUrl.isNullOrEmpty()) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.no_url_icon),
|
painter = painterResource(id = R.drawable.no_url_icon),
|
||||||
contentDescription = "No PKG URL",
|
contentDescription = "No PKG URL",
|
||||||
tint = Color(0xFFCB5A5A)
|
tint = Color(0xFFCB5A5A)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (item.zRif.isEmpty() && item.rap.isEmpty()) {
|
if (item.zRif.isNullOrEmpty() && item.rap.isNullOrEmpty()) {
|
||||||
Icon(
|
Icon(
|
||||||
painter = painterResource(id = R.drawable.no_key_icon),
|
painter = painterResource(id = R.drawable.no_key_icon),
|
||||||
contentDescription = "No License Key",
|
contentDescription = "No License Key",
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ data class PackageItem(
|
|||||||
val titleId: String,
|
val titleId: String,
|
||||||
val region: String,
|
val region: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val pkgUrl: String,
|
val pkgUrl: String?,
|
||||||
val contentId: String,
|
val contentId: String?,
|
||||||
val modificationDate: String,
|
val modificationDate: String?,
|
||||||
val pkgSize: String,
|
val pkgSize: String,
|
||||||
val sha256: String,
|
val sha256: String?,
|
||||||
val zRif: String,
|
val zRif: String?,
|
||||||
val rap: String,
|
val rap: String?,
|
||||||
val minFW: String,
|
val minFW: String?,
|
||||||
|
|
||||||
val consoleType: ConsoleType,
|
val consoleType: ConsoleType,
|
||||||
val dataType: PackageItemType,
|
val dataType: PackageItemType,
|
||||||
|
|||||||
+95
@@ -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.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.statusBarsPadding
|
import androidx.compose.foundation.layout.statusBarsPadding
|
||||||
@@ -43,6 +45,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalClipboardManager
|
import androidx.compose.ui.platform.LocalClipboardManager
|
||||||
@@ -98,12 +101,18 @@ fun SettingsScreen(
|
|||||||
Text(text = stringResource(id = R.string.title_activity_settings), style = Typography.titleLarge)
|
Text(text = stringResource(id = R.string.title_activity_settings), style = Typography.titleLarge)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
contentWindowInsets = WindowInsets(bottom = 0.dp),
|
||||||
) { padding ->
|
) { padding ->
|
||||||
Column(modifier = Modifier
|
Column(modifier = Modifier
|
||||||
.padding(padding)
|
.padding(padding)
|
||||||
.verticalScroll(rememberScrollState())
|
.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)
|
Appearance(viewModel, prefs)
|
||||||
TsvFiles(viewModel, prefs)
|
TsvFiles(viewModel, prefs)
|
||||||
Updates(viewModel, prefs)
|
Updates(viewModel, prefs)
|
||||||
@@ -366,6 +375,7 @@ fun Downloads(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
|
|||||||
title = "Unpack Directory",
|
title = "Unpack Directory",
|
||||||
value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
|
value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
|
||||||
icon = Icons.Outlined.Unarchive,
|
icon = Icons.Outlined.Unarchive,
|
||||||
|
enabled = !prefs.unpackInDownload,
|
||||||
onClick = {
|
onClick = {
|
||||||
unpackDirPicker.launch(Uri.EMPTY)
|
unpackDirPicker.launch(Uri.EMPTY)
|
||||||
}
|
}
|
||||||
@@ -514,12 +524,14 @@ fun DialogSetting(
|
|||||||
value: String,
|
value: String,
|
||||||
icon: ImageVector?,
|
icon: ImageVector?,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
singleLine : Boolean = false
|
singleLine : Boolean = false,
|
||||||
|
enabled: Boolean = true
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clickable { onClick() }
|
.alpha(if (enabled) 1f else 0.5f)
|
||||||
|
.then(if (enabled) Modifier.clickable { onClick() } else Modifier)
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
|||||||
@@ -98,12 +98,12 @@ fun NPSBrowserTheme(
|
|||||||
darkTheme -> DarkColorScheme
|
darkTheme -> DarkColorScheme
|
||||||
else -> LightColorScheme
|
else -> LightColorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
val view = LocalView.current
|
val view = LocalView.current
|
||||||
if (!view.isInEditMode) {
|
if (!view.isInEditMode) {
|
||||||
SideEffect {
|
SideEffect {
|
||||||
val window = (view.context as Activity).window
|
val window = (view.context as Activity).window
|
||||||
window.statusBarColor = Color.Transparent.toArgb()
|
window.statusBarColor = Color.Transparent.toArgb()
|
||||||
|
window.navigationBarColor = Color.Transparent.toArgb()
|
||||||
// allows pages to extend to the status bar
|
// allows pages to extend to the status bar
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
|
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.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import okhttp3.internal.toLongOrDefault
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
data class PackageListUiState(
|
data class PackageListUiState(
|
||||||
@@ -91,24 +92,64 @@ class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesReposit
|
|||||||
val tsv = tsvReader.readAllWithHeader(file!!)
|
val tsv = tsvReader.readAllWithHeader(file!!)
|
||||||
|
|
||||||
tsv.forEach { row ->
|
tsv.forEach { row ->
|
||||||
var size = ""
|
if (row["Name"].isNullOrEmpty()) {
|
||||||
val tempSize = row["File Size"]
|
return@forEach
|
||||||
if (!tempSize.isNullOrEmpty()) {
|
|
||||||
size = bytesIntoHumanReadable(tempSize.toLong())
|
|
||||||
}
|
}
|
||||||
|
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(
|
list.add(
|
||||||
PackageItem(
|
PackageItem(
|
||||||
row["Title ID"] ?: "",
|
row["Title ID"] ?: "",
|
||||||
row["Region"] ?: "",
|
row["Region"] ?: "",
|
||||||
row["Name"] ?: "",
|
row["Name"]?.trim() ?: "",
|
||||||
row["PKG direct link"] ?: "",
|
pkgUrl,
|
||||||
row["Content ID"] ?: "",
|
contentId,
|
||||||
row["Last Modification Date"] ?: "",
|
modificationDate,
|
||||||
size,
|
size,
|
||||||
row["SHA256"] ?: "",
|
sha256,
|
||||||
row["zRIF"] ?: "",
|
zRif,
|
||||||
row["RAP"] ?: "",
|
rap,
|
||||||
row["Required FW"] ?: "",
|
minFW,
|
||||||
console,
|
console,
|
||||||
type,
|
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>
|
||||||
@@ -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>
|
||||||
Reference in New Issue
Block a user