diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cea2e01..dd9cc31 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -61,7 +61,7 @@ android {
}
composeOptions {
- kotlinCompilerExtensionVersion = "1.4.4"
+ kotlinCompilerExtensionVersion = "1.4.8"
}
buildToolsVersion = "33.0.1"
kotlinOptions {
@@ -76,10 +76,12 @@ repositories {
dependencies {
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
+ implementation("androidx.lifecycle:lifecycle-runtime-compose:2.6.1")
implementation("androidx.activity:activity-compose:1.7.2")
implementation(platform("androidx.compose:compose-bom:2022.10.00"))
implementation("androidx.compose.ui:ui")
implementation("androidx.compose.ui:ui-graphics")
+ implementation("androidx.core:core-ktx:1.10.1")
androidTestImplementation(platform("androidx.compose:compose-bom:2022.10.00"))
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
// Jetpack Compose
@@ -88,7 +90,7 @@ dependencies {
androidTestImplementation(composeBom)
// Material 3
- implementation("androidx.compose.material3:material3:1.1.0")
+ implementation("androidx.compose.material3:material3:1.1.1")
implementation("androidx.compose.material:material-icons-extended:1.4.3")
// Android Studio Preview support
implementation("androidx.compose.ui:ui-tooling-preview")
@@ -100,7 +102,7 @@ dependencies {
// Compose extras library for controlling system ui and etc.
implementation("com.google.accompanist:accompanist-systemuicontroller:0.31.2-alpha")
- implementation("com.github.doyaaaaaken:kotlin-csv-jvm:1.9.1")
+ implementation("com.github.IllusionMan1212:kotlin-csv:cedb7c5ab2")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2dc2646..e4b0650 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
by preferencesDataStore(
+ name = PREFERENCES_NAME
+)
+
+class NPSApp: Application() {
+ lateinit var settingsPreferencesRepository: SettingsPreferencesRepository
+ lateinit var onboardingPreferencesRepository: OnboardingPreferencesRepository
+
+ override fun onCreate() {
+ super.onCreate()
+ settingsPreferencesRepository = SettingsPreferencesRepository(dataStore)
+ onboardingPreferencesRepository = OnboardingPreferencesRepository(dataStore)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt b/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt
deleted file mode 100644
index 1dc03b3..0000000
--- a/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt
+++ /dev/null
@@ -1,246 +0,0 @@
-package com.illusionware.npsbrowser
-
-import android.content.Context
-import android.net.Uri
-import android.os.Environment
-import androidx.datastore.preferences.core.booleanPreferencesKey
-import androidx.datastore.preferences.core.edit
-import androidx.datastore.preferences.core.intPreferencesKey
-import androidx.datastore.preferences.core.stringPreferencesKey
-import androidx.datastore.preferences.preferencesDataStore
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
-
-enum class ConsoleType {
- PSVITA,
- PS3,
- PSP,
- PSX,
- PSM,
-}
-
-enum class DataType {
- GAMES,
- DLC,
- THEMES,
-}
-
-enum class Layout {
- LIST,
- GRID,
-}
-
-class Preferences(private val context: Context) {
- companion object {
- private val Context.dataStore by preferencesDataStore("settings")
- private val APP_THEME = intPreferencesKey("app_theme")
- private val DATA_LAYOUT = intPreferencesKey("layout")
- private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding")
- private val PSV_GAMES = stringPreferencesKey("psv_games")
- private val PS3_GAMES = stringPreferencesKey("ps3_games")
- private val PSP_GAMES = stringPreferencesKey("psp_games")
- private val PSX_GAMES = stringPreferencesKey("psx_games")
- private val PSM_GAMES = stringPreferencesKey("psm_games")
- private val PSV_DLC = stringPreferencesKey("psv_dlc")
- private val PS3_DLC = stringPreferencesKey("ps3_dlc")
- private val PSP_DLC = stringPreferencesKey("psp_dlc")
- private val PSV_THEMES = stringPreferencesKey("psv_themes")
- private val HMAC_KEY = stringPreferencesKey("hmac_key")
- private val DOWNLOAD_DIR = stringPreferencesKey("download_dir")
- private val UNPACK_DIR = stringPreferencesKey("unpack_dir")
- private val UNPACK_IN_DOWNLOAD = booleanPreferencesKey("unpack_in_download")
- private val DELETE_AFTER_UNPACK = booleanPreferencesKey("delete_after_unpack")
- private val PKG2ZIP_PARAMS = stringPreferencesKey("pkg2zip_params")
- private val PKG2ZIP_AUTO_DECRYPT = booleanPreferencesKey("pkg2zip_auto_decrypt")
- }
-
- val theme = context.dataStore.data.map { prefs ->
- prefs[APP_THEME] ?: 2
- }
-
- val layout = context.dataStore.data.map{ prefs ->
- prefs[DATA_LAYOUT] ?: Layout.LIST.ordinal
- }
-
- val seenOnboarding = context.dataStore.data.map{ prefs ->
- prefs[SEEN_ONBOARDING] ?: false
- }
-
- val psvGames = context.dataStore.data.map{ prefs ->
- prefs[PSV_GAMES] ?: ""
- }
-
- val ps3Games = context.dataStore.data.map{ prefs ->
- prefs[PS3_GAMES] ?: ""
- }
-
- val pspGames = context.dataStore.data.map{ prefs ->
- prefs[PSP_GAMES] ?: ""
- }
-
- val psxGames = context.dataStore.data.map{ prefs ->
- prefs[PSX_GAMES] ?: ""
- }
-
- val psmGames = context.dataStore.data.map{ prefs ->
- prefs[PSM_GAMES] ?: ""
- }
-
- val psvDLC = context.dataStore.data.map{ prefs ->
- prefs[PSV_DLC] ?: ""
- }
-
- val ps3DLC = context.dataStore.data.map{ prefs ->
- prefs[PS3_DLC] ?: ""
- }
-
- val pspDLC = context.dataStore.data.map{ prefs ->
- prefs[PSP_DLC] ?: ""
- }
-
- val psvThemes = context.dataStore.data.map{ prefs ->
- prefs[PSV_THEMES] ?: ""
- }
-
- val hmacKey = context.dataStore.data.map{ prefs ->
- prefs[HMAC_KEY] ?: ""
- }
-
- val downloadDir = context.dataStore.data.map{ prefs ->
- prefs[DOWNLOAD_DIR] ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.path ?: ""
- }
-
- val unpackDir = context.dataStore.data.map{ prefs ->
- prefs[UNPACK_DIR] ?: Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.path ?: ""
- }
-
- val unpackInDownload = context.dataStore.data.map{ prefs ->
- prefs[UNPACK_IN_DOWNLOAD] ?: false
- }
-
- val deleteAfterUnpack = context.dataStore.data.map{ prefs ->
- prefs[DELETE_AFTER_UNPACK] ?: false
- }
-
- val pkg2zipParams = context.dataStore.data.map{ prefs ->
- prefs[PKG2ZIP_PARAMS] ?: "-x {pkgFile} \"{zRifKey}\""
- }
-
- val pkg2zipAutoDecrypt = context.dataStore.data.map{ prefs ->
- prefs[PKG2ZIP_AUTO_DECRYPT] ?: true
- }
-
- suspend fun changeTheme(theme: Int) {
- context.dataStore.edit { prefs ->
- prefs[APP_THEME] = theme
- }
- }
-
- suspend fun changeLayout(layout: Int) {
- context.dataStore.edit { prefs ->
- prefs[DATA_LAYOUT] = layout
- }
- }
-
- suspend fun finishOnboarding() {
- context.dataStore.edit { prefs ->
- prefs[SEEN_ONBOARDING] = true
- }
- }
-
- suspend fun setTSVFile(console: ConsoleType, contentType: DataType, file: Uri) {
- context.dataStore.edit { prefs ->
- when (console) {
- ConsoleType.PSVITA -> {
- when (contentType) {
- DataType.GAMES -> prefs[PSV_GAMES] = file.toString()
- DataType.DLC -> prefs[PSV_DLC] = file.toString()
- DataType.THEMES -> prefs[PSV_THEMES] = file.toString()
- }
- }
- ConsoleType.PS3 -> {
- when (contentType) {
- DataType.GAMES -> prefs[PS3_GAMES] = file.toString()
- DataType.DLC -> prefs[PS3_DLC] = file.toString()
- else -> {}
- }
- }
- ConsoleType.PSP -> {
- when (contentType) {
- DataType.GAMES -> prefs[PSP_GAMES] = file.toString()
- DataType.DLC -> prefs[PSP_DLC] = file.toString()
- else -> {}
- }
- }
- ConsoleType.PSX -> {
- when (contentType) {
- DataType.GAMES -> prefs[PSX_GAMES] = file.toString()
- else -> {}
- }
- }
- ConsoleType.PSM -> {
- when (contentType) {
- DataType.GAMES -> prefs[PSM_GAMES] = file.toString()
- else -> {}
- }
- }
- }
- }
- }
-
- suspend fun getAvailableTsvs(): List> {
- val list = mutableListOf>()
- if (psvGames.first().isNotEmpty()) list.add(Pair(ConsoleType.PSVITA, DataType.GAMES))
- if (psvDLC.first().isNotEmpty()) list.add(Pair(ConsoleType.PSVITA, DataType.DLC))
- if (psvThemes.first().isNotEmpty()) list.add(Pair(ConsoleType.PSVITA, DataType.THEMES))
- if (ps3Games.first().isNotEmpty()) list.add(Pair(ConsoleType.PS3, DataType.GAMES))
- if (ps3DLC.first().isNotEmpty()) list.add(Pair(ConsoleType.PS3, DataType.DLC))
- if (pspGames.first().isNotEmpty()) list.add(Pair(ConsoleType.PSP, DataType.GAMES))
- if (pspDLC.first().isNotEmpty()) list.add(Pair(ConsoleType.PSP, DataType.DLC))
- if (psxGames.first().isNotEmpty()) list.add(Pair(ConsoleType.PSX, DataType.GAMES))
- if (psmGames.first().isNotEmpty()) list.add(Pair(ConsoleType.PSM, DataType.GAMES))
- return list
- }
-
- suspend fun setHMACKey(key: String) {
- context.dataStore.edit { prefs ->
- prefs[HMAC_KEY] = key
- }
- }
-
- suspend fun setDownloadDir(dir: String) {
- context.dataStore.edit { prefs ->
- prefs[DOWNLOAD_DIR] = dir
- }
- }
-
- suspend fun setUnpackDir(dir: String) {
- context.dataStore.edit { prefs ->
- prefs[UNPACK_DIR] = dir
- }
- }
-
- suspend fun setUnpackInDownload(unpackInDownload: Boolean) {
- context.dataStore.edit { prefs ->
- prefs[UNPACK_IN_DOWNLOAD] = unpackInDownload
- }
- }
-
- suspend fun setDeleteAfterUnpack(deleteAfterUnpack: Boolean) {
- context.dataStore.edit { prefs ->
- prefs[DELETE_AFTER_UNPACK] = deleteAfterUnpack
- }
- }
-
- suspend fun setPkg2zipParams(params: String) {
- context.dataStore.edit { prefs ->
- prefs[PKG2ZIP_PARAMS] = params
- }
- }
-
- suspend fun setPkg2zipAutoDecrypt(autoDecrypt: Boolean) {
- context.dataStore.edit { prefs ->
- prefs[PKG2ZIP_AUTO_DECRYPT] = autoDecrypt
- }
- }
-}
diff --git a/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt b/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt
index e4762da..51205a4 100644
--- a/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt
+++ b/app/src/main/java/com/illusionware/npsbrowser/activities/MainActivity.kt
@@ -3,9 +3,7 @@ package com.illusionware.npsbrowser.activities
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
-import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.clickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
@@ -16,7 +14,6 @@ import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
-import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -25,6 +22,8 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
+import androidx.compose.foundation.lazy.grid.items
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
@@ -32,6 +31,8 @@ import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.GridView
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.Settings
+import androidx.compose.material.icons.filled.ViewList
+import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -47,7 +48,6 @@ import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -69,31 +69,34 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import coil.compose.AsyncImage
import coil.request.ImageRequest
-import com.illusionware.npsbrowser.ConsoleType
-import com.illusionware.npsbrowser.Layout
-import com.illusionware.npsbrowser.Preferences
import com.illusionware.npsbrowser.R
+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.pages.OnBoarding
+import com.illusionware.npsbrowser.ui.pages.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.ui.components.*
-import com.illusionware.npsbrowser.ui.pages.SettingsScreen
-import com.illusionware.npsbrowser.ui.pages.OnBoarding
-import com.illusionware.npsbrowser.ui.theme.ColorOnPrimaryLight
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
+import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
+import com.illusionware.npsbrowser.viewmodels.PackageListViewModel
+import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import kotlinx.coroutines.runBlocking
val PlaceholderColor = Color(0x1F888888)
@@ -102,59 +105,60 @@ val redHatDisplayFamily = FontFamily(
)
class MainActivity : ComponentActivity() {
-
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
setContent {
- Router()
- }
- }
-}
+ val settingsViewModel: SettingsViewModel = viewModel(
+ factory = SettingsViewModel.Factory
+ )
+ val onboardingViewModel: OnboardingViewModel = viewModel(
+ factory = OnboardingViewModel.Factory
+ )
-@Composable
-fun Router() {
- val prefs = Preferences(LocalContext.current)
- var theme by remember { runBlocking { mutableStateOf(prefs.theme.first()) } }
- var seenOnboarding by remember { runBlocking { mutableStateOf(prefs.seenOnboarding.first()) } }
- val navController = rememberNavController()
+ val onboardingPrefs = onboardingViewModel.uiState.collectAsStateWithLifecycle().value
+ val appTheme = runBlocking { settingsViewModel.getAppTheme() }
+ val settingsPrefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
+ val navController = rememberNavController()
- val darkTheme = when (theme) {
- 0 -> false
- 1 -> true
- else -> isSystemInDarkTheme()
- }
+ val darkTheme = when (appTheme) {
+ 0 -> false
+ 1 -> true
+ else -> isSystemInDarkTheme()
+ }
- NPSBrowserTheme(darkTheme = darkTheme) {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = if (seenOnboarding) MaterialTheme.colorScheme.background else ColorAccent
- ) {
- NavHost(
- navController = navController,
- startDestination = if (seenOnboarding) "Home" else "Onboarding",
- ) {
- composable(route = "Home") {
- HomePage(
- navigateToSettings = { navController.navigate("Settings") }
- )
- }
- composable(route = "Settings") {
- SettingsScreen(
- navigationGoBack = { navController.popBackStack() },
- onThemeChange = { v -> theme = v },
- )
- }
- composable(route = "Onboarding") {
- OnBoarding(
- navigateToHome = {
- navController.popBackStack()
- seenOnboarding = true
- navController.navigate("Home")
- },
- darkTheme = darkTheme
- )
+ NPSBrowserTheme(darkTheme = darkTheme) {
+ Surface(
+ modifier = Modifier.fillMaxSize(),
+ color = if (onboardingPrefs.seenOnboarding) MaterialTheme.colorScheme.background else ColorAccent
+ ) {
+ NavHost(
+ navController = navController,
+ startDestination = if (onboardingPrefs.seenOnboarding) "Home" else "Onboarding",
+ ) {
+ composable(route = "Home") {
+ HomePage(
+ navigateToSettings = { navController.navigate("Settings") },
+ settingsPrefs = settingsPrefs,
+ settingsViewModel = settingsViewModel,
+ )
+ }
+ composable(route = "Settings") {
+ SettingsScreen(
+ navigationGoBack = { navController.popBackStack() },
+ )
+ }
+ composable(route = "Onboarding") {
+ OnBoarding(
+ navigateToHome = {
+ navController.popBackStack()
+ navController.navigate("Home")
+ },
+ darkTheme = darkTheme,
+ )
+ }
+ }
}
}
}
@@ -163,17 +167,18 @@ fun Router() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-@Preview
-fun HomePage(navigateToSettings: () -> Unit = {}) {
- val prefs = Preferences(LocalContext.current)
+fun HomePage(
+ navigateToSettings: () -> Unit = {},
+ settingsPrefs: SettingsPreferences,
+ settingsViewModel: SettingsViewModel,
+) {
var isSearchOpen by rememberSaveable { mutableStateOf(false)}
var searchQuery by rememberSaveable { mutableStateOf("")}
val searchBar = remember { FocusRequester() }
var layoutDialogOpen by remember { mutableStateOf(false) }
- val tsvs by remember { runBlocking { mutableStateOf(prefs.getAvailableTsvs()) }}
+ val tsvs = settingsViewModel.availableTsvs()
var selectedBarItem by remember { mutableStateOf(ConsoleType.PSVITA) }
-// val noTsvs by remember { mutableStateOf(false) }
Scaffold(
topBar = {
@@ -239,7 +244,7 @@ fun HomePage(navigateToSettings: () -> Unit = {}) {
tooltip = stringResource(id = R.string.layout),
onClick = { layoutDialogOpen = true }
) {
- Icon(imageVector = Icons.Filled.GridView, contentDescription = stringResource(
+ Icon(imageVector = if (settingsPrefs.layout == ItemLayout.GRID.ordinal) Icons.Filled.GridView else Icons.Filled.ViewList, contentDescription = stringResource(
id = R.string.layout
))
}
@@ -267,16 +272,16 @@ fun HomePage(navigateToSettings: () -> Unit = {}) {
if (tsvs.isEmpty()) {
NoTsvs(navigateToSettings = navigateToSettings)
} else {
-// when (selectedBarItem) {
-// ConsoleType.PSVITA -> VitaPage(searchQuery = searchQuery)
-// ConsoleType.PS3 -> PS3Page(searchQuery = searchQuery)
-// }
- PackageList(selectedBarItem)
+ PackageList(selectedBarItem, settingsPrefs = settingsPrefs)
}
}
if (layoutDialogOpen) {
- LayoutDialog(onDismiss = { layoutDialogOpen = false })
+ LayoutDialog(
+ onDismiss = { layoutDialogOpen = false },
+ prefs = settingsPrefs,
+ viewModel = settingsViewModel,
+ )
}
}
}
@@ -368,54 +373,82 @@ fun NoTsvs(padding: PaddingValues = PaddingValues(0.dp), navigateToSettings: ()
}
@Composable
-@Preview
-fun PackageList(type: ConsoleType = ConsoleType.PSVITA) {
- val prefs = Preferences(LocalContext.current)
- val layout = prefs.layout.collectAsState(initial = Layout.LIST.ordinal).value
-// val thing = when (type) {
-// ConsoleType.PSVITA -> {
-// var list = ArrayList()
-// list.add(Package("test"))
-// val gs = prefs.psvGames.collectAsState(initial = emptyList())
-// list.add(gs.value)
-// }
-// ConsoleType.PS3 -> {
-// prefs.ps3Games.collectAsState(initial = emptyList())
-// }
-// ConsoleType.PSP -> {
-// prefs.pspGames.collectAsState(initial = emptyList())
-// }
-// ConsoleType.PSX -> {
-// prefs.psxGames.collectAsState(initial = emptyList())
-// }
-// ConsoleType.PSM -> {
-// prefs.psmGames.collectAsState(initial = emptyList())
-// }
-// }
+fun PackageList(
+ type: ConsoleType = ConsoleType.PSVITA,
+ viewModel: PackageListViewModel = viewModel(
+ factory = PackageListViewModel.Factory
+ ),
+ settingsPrefs: SettingsPreferences,
+) {
+ val uiState = viewModel.uiState.collectAsStateWithLifecycle().value
+
+ val packages = when (type) {
+ ConsoleType.PSVITA -> {
+ uiState.packages.filter { it.consoleType == ConsoleType.PSVITA }
+ }
+ ConsoleType.PS3 -> {
+ uiState.packages.filter { it.consoleType == ConsoleType.PS3 }
+ }
+ ConsoleType.PSP -> {
+ uiState.packages.filter { it.consoleType == ConsoleType.PSP }
+ }
+ ConsoleType.PSX -> {
+ uiState.packages.filter { it.consoleType == ConsoleType.PSX }
+ }
+ ConsoleType.PSM -> {
+ uiState.packages.filter { it.consoleType == ConsoleType.PSM }
+ }
+ }
Column(Modifier.fillMaxSize()) {
- if (layout == Layout.LIST.ordinal) {
- LazyColumn {
- items(15) {
- PackageListItem()
- }
+ if (uiState.isLoading) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ CircularProgressIndicator()
}
} else {
- LazyVerticalGrid(
- columns = GridCells.Adaptive(140.dp),
- contentPadding = PaddingValues(8.dp),
- ) {
- items(15) {
- PackageGridItem()
+ if (packages.isEmpty()) {
+ Box(
+ modifier = Modifier
+ .fillMaxSize()
+ .padding(16.dp),
+ contentAlignment = Alignment.Center
+ ) {
+ Text(
+ "You haven't added any TSVs for this console yet",
+ style = Typography.bodyLarge,
+ textAlign = TextAlign.Center,
+ color = MaterialTheme.colorScheme.outline
+ )
+ }
+ } else {
+ if (settingsPrefs.layout == ItemLayout.LIST.ordinal) {
+ LazyColumn {
+ items(packages) {
+ PackageListItem(it)
+ }
+ }
+ } else {
+ LazyVerticalGrid(
+ columns = GridCells.Adaptive(140.dp),
+ contentPadding = PaddingValues(8.dp),
+ ) {
+ items(packages) {
+ PackageGridItem(it)
+ }
+ }
}
}
}
}
}
-@OptIn(ExperimentalFoundationApi::class)
@Composable
-fun PackageListItem() {
+fun PackageListItem(item: PackageItem) {
Box(
Modifier
.fillMaxWidth()
@@ -434,7 +467,7 @@ fun PackageListItem() {
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
- .data("https://picsum.photos/200")
+ .data("TODO:")
.crossfade(true)
.error(R.drawable.default_game_icon)
.build(),
@@ -451,7 +484,7 @@ fun PackageListItem() {
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
- text = "10 Second Ninja X",
+ text = item.name,
color = MaterialTheme.colorScheme.onBackground,
style = Typography.bodyLarge,
overflow = TextOverflow.Ellipsis,
@@ -466,25 +499,33 @@ fun PackageListItem() {
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
- Text(text = "US")
- Text(text = "3.69")
- Text(text = "223.8MB")
- Tag(title = "GAME", small = true)
+ Text(text = item.region)
+ if (item.minFW.isNotEmpty()) {
+ Text(text = String.format("%.2f", item.minFW.toFloat()))
+ }
+ if (item.pkgSize.isNotEmpty()) {
+ Text(text = item.pkgSize)
+ }
+ Tag(title = item.dataType.name, small = true)
}
}
}
}
Column {
- Icon(
- painter = painterResource(id = R.drawable.no_url_icon),
- contentDescription = "No PKG URL",
- tint = Color(0xFFCB5A5A)
- )
- Icon(
- painter = painterResource(id = R.drawable.no_key_icon),
- contentDescription = "No Zrif Key",
- tint = Color(0xFFCB5A5A)
- )
+ if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
+ 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())) {
+ Icon(
+ painter = painterResource(id = R.drawable.no_key_icon),
+ contentDescription = "No License Key",
+ tint = Color(0xFFCB5A5A)
+ )
+ }
}
}
}
@@ -512,8 +553,7 @@ fun Tag(title: String, small: Boolean = false) {
}
@Composable
-@Preview
-fun PackageGridItem() {
+fun PackageGridItem(item: PackageItem) {
Box(
Modifier
.fillMaxWidth()
@@ -529,7 +569,7 @@ fun PackageGridItem() {
content = {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current)
- .data("https://picsum.photos/200")
+ .data("TODO:")
.crossfade(true)
.error(R.drawable.default_game_icon)
.build(),
@@ -540,7 +580,7 @@ fun PackageGridItem() {
.fillMaxWidth(),
contentScale = ContentScale.Crop,
)
- Tag(title = "GAME")
+ Tag(title = item.dataType.name)
}
) { measurables, constraints ->
val imagePlaceable = measurables[0].measure(constraints)
@@ -555,7 +595,7 @@ fun PackageGridItem() {
}
}
Text(
- text = "10 Second Ninja X",
+ text = item.name,
color = MaterialTheme.colorScheme.onBackground,
style = Typography.bodyLarge,
overflow = TextOverflow.Ellipsis,
@@ -566,32 +606,41 @@ fun PackageGridItem() {
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
ProvideTextStyle(value = Typography.bodySmall.copy(color = MaterialTheme.colorScheme.outline)) {
- Text(text = "US")
- Text(text = "3.69")
- Text(text = "223.8MB")
+ Text(text = item.region)
+ if (item.minFW.isNotEmpty()) {
+ Text(text = String.format("%.2f", item.minFW.toFloat()))
+ }
+ if (item.pkgSize.isNotEmpty()) {
+ Text(text = item.pkgSize)
+ }
}
}
Row {
- Icon(
- painter = painterResource(id = R.drawable.no_url_icon),
- contentDescription = "No PKG URL",
- tint = Color(0xFFCB5A5A)
- )
- Icon(
- painter = painterResource(id = R.drawable.no_key_icon),
- contentDescription = "No Zrif Key",
- tint = Color(0xFFCB5A5A)
- )
+ if (item.pkgUrl.isEmpty() || item.pkgUrl == "MISSING") {
+ Icon(
+ painter = painterResource(id = R.drawable.no_url_icon),
+ contentDescription = "No PKG URL",
+ tint = Color(0xFFCB5A5A)
+ )
+ }
+ if (item.zRif.isEmpty() && item.rap.isEmpty()) {
+ Icon(
+ painter = painterResource(id = R.drawable.no_key_icon),
+ contentDescription = "No License Key",
+ tint = Color(0xFFCB5A5A)
+ )
+ }
}
}
}
}
@Composable
-fun LayoutDialog(onDismiss: () -> Unit) {
- val prefs = Preferences(LocalContext.current)
- val layout = prefs.layout.collectAsState(initial = Layout.LIST.ordinal).value
-
+fun LayoutDialog(
+ onDismiss: () -> Unit,
+ prefs: SettingsPreferences,
+ viewModel: SettingsViewModel
+) {
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.layout),
@@ -604,13 +653,13 @@ fun LayoutDialog(onDismiss: () -> Unit) {
Column {
NPSRadioButton(
text = "List",
- selected = layout == Layout.LIST.ordinal,
- onClick = { CoroutineScope(Dispatchers.IO).launch { prefs.changeLayout(Layout.LIST.ordinal) } },
+ selected = prefs.layout == ItemLayout.LIST.ordinal,
+ onClick = { viewModel.setLayout(ItemLayout.LIST) },
)
NPSRadioButton(
text = "Grid",
- selected = layout == Layout.GRID.ordinal,
- onClick = { CoroutineScope(Dispatchers.IO).launch { prefs.changeLayout(Layout.GRID.ordinal) } },
+ selected = prefs.layout == ItemLayout.GRID.ordinal,
+ onClick = { viewModel.setLayout(ItemLayout.GRID) },
)
}
}
diff --git a/app/src/main/java/com/illusionware/npsbrowser/data/OnboardingPreferencesRepository.kt b/app/src/main/java/com/illusionware/npsbrowser/data/OnboardingPreferencesRepository.kt
new file mode 100644
index 0000000..0d53288
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/data/OnboardingPreferencesRepository.kt
@@ -0,0 +1,36 @@
+package com.illusionware.npsbrowser.data
+
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+
+data class OnboardingPreferences(
+ val seenOnboarding: Boolean,
+)
+
+class OnboardingPreferencesRepository(
+ private val dataStore: DataStore,
+) {
+ companion object {
+ private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding")
+ }
+
+ fun getPreferences(): Flow =
+ runBlocking { dataStore.data.map { mapOnboardingSettings(it) } }
+
+ suspend fun setSeenOnboarding(seen: Boolean) {
+ dataStore.edit { prefs ->
+ prefs[SEEN_ONBOARDING] = seen
+ }
+ }
+
+ private fun mapOnboardingSettings(prefs: Preferences): OnboardingPreferences {
+ return OnboardingPreferences(
+ prefs[SEEN_ONBOARDING] ?: false,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/data/SettingsPreferencesRepository.kt b/app/src/main/java/com/illusionware/npsbrowser/data/SettingsPreferencesRepository.kt
new file mode 100644
index 0000000..82d5b66
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/data/SettingsPreferencesRepository.kt
@@ -0,0 +1,219 @@
+package com.illusionware.npsbrowser.data
+
+import android.net.Uri
+import android.os.Environment
+import androidx.datastore.core.DataStore
+import androidx.datastore.preferences.core.Preferences
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.intPreferencesKey
+import androidx.datastore.preferences.core.stringPreferencesKey
+import com.illusionware.npsbrowser.model.ConsoleType
+import com.illusionware.npsbrowser.model.PackageItemType
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+val defaultDownloadDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/NPSBrowser"
+
+enum class ItemLayout {
+ LIST,
+ GRID,
+}
+
+enum class Theme {
+ LIGHT,
+ DARK,
+ SYSTEM,
+}
+
+data class SettingsPreferences(
+ val appTheme: Int,
+ val layout: Int,
+ val psvGames: String,
+ val ps3Games: String,
+ val pspGames: String,
+ val psxGames: String,
+ val psmGames: String,
+ val psvDlc: String,
+ val ps3Dlc: String,
+ val pspDlc: String,
+ val psvThemes: String,
+ val hmacKey: String,
+ val downloadDir: String,
+ val unpackDir: String,
+ val unpackInDownload: Boolean,
+ val deleteAfterUnpack: Boolean,
+ val pkg2zipParams: String,
+ val pkg2zipAutoDecrypt: Boolean,
+)
+
+class SettingsPreferencesRepository(
+ private val dataStore: DataStore
+) {
+ companion object {
+ private val APP_THEME = intPreferencesKey("app_theme")
+ private val LAYOUT = intPreferencesKey("layout")
+ private val PSV_GAMES = stringPreferencesKey("psv_games")
+ private val PS3_GAMES = stringPreferencesKey("ps3_games")
+ private val PSP_GAMES = stringPreferencesKey("psp_games")
+ private val PSX_GAMES = stringPreferencesKey("psx_games")
+ private val PSM_GAMES = stringPreferencesKey("psm_games")
+ private val PSV_DLC = stringPreferencesKey("psv_dlc")
+ private val PS3_DLC = stringPreferencesKey("ps3_dlc")
+ private val PSP_DLC = stringPreferencesKey("psp_dlc")
+ private val PSV_THEMES = stringPreferencesKey("psv_themes")
+ private val HMAC_KEY = stringPreferencesKey("hmac_key")
+ private val DOWNLOAD_DIR = stringPreferencesKey("download_dir")
+ private val UNPACK_DIR = stringPreferencesKey("unpack_dir")
+ private val UNPACK_IN_DOWNLOAD = booleanPreferencesKey("unpack_in_download")
+ private val DELETE_AFTER_UNPACK = booleanPreferencesKey("delete_after_unpack")
+ private val PKG2ZIP_PARAMS = stringPreferencesKey("pkg2zip_params")
+ private val PKG2ZIP_AUTO_DECRYPT = booleanPreferencesKey("pkg2zip_auto_decrypt")
+ }
+
+ private val tsvFiles = MutableSharedFlow>()
+
+ fun getPreferences(): Flow =
+ dataStore.data.map { mapSettingsPreferences(it) }
+
+ suspend fun availableTsvs(): MutableList> {
+ val list = mutableListOf>()
+
+ return dataStore.data.map {
+ if (it[PSV_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSV_GAMES] ?: "", ConsoleType.PSVITA, PackageItemType.GAME))
+ if (it[PSV_DLC]?.isNotEmpty() == true) list.add(Triple(it[PSV_DLC] ?: "", ConsoleType.PSVITA, PackageItemType.DLC))
+ if (it[PSV_THEMES]?.isNotEmpty() == true) list.add(Triple(it[PSV_THEMES] ?: "", ConsoleType.PSVITA, PackageItemType.THEME))
+ if (it[PS3_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PS3_GAMES] ?: "", ConsoleType.PS3, PackageItemType.GAME))
+ if (it[PS3_DLC]?.isNotEmpty() == true) list.add(Triple(it[PS3_DLC] ?: "", ConsoleType.PS3, PackageItemType.DLC))
+ if (it[PSP_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSP_GAMES] ?: "", ConsoleType.PSP, PackageItemType.GAME))
+ if (it[PSP_DLC]?.isNotEmpty() == true) list.add(Triple(it[PSP_DLC] ?: "", ConsoleType.PSP, PackageItemType.DLC))
+ if (it[PSX_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSX_GAMES] ?: "", ConsoleType.PSX, PackageItemType.GAME))
+ if (it[PSM_GAMES]?.isNotEmpty() == true) list.add(Triple(it[PSM_GAMES] ?: "", ConsoleType.PSM, PackageItemType.GAME))
+
+ list
+ }.first()
+ }
+
+ suspend fun setTheme(theme: Theme) {
+ dataStore.edit { prefs ->
+ prefs[APP_THEME] = theme.ordinal
+ }
+ }
+
+ suspend fun setLayout(layout: ItemLayout) {
+ dataStore.edit { prefs ->
+ prefs[LAYOUT] = layout.ordinal
+ }
+ }
+
+ suspend fun setHMACKey(key: String) {
+ dataStore.edit { prefs ->
+ prefs[HMAC_KEY] = key
+ }
+ }
+
+ suspend fun setDownloadDir(dir: String) {
+ dataStore.edit { prefs ->
+ prefs[DOWNLOAD_DIR] = dir
+ }
+ }
+
+ suspend fun setUnpackDir(dir: String) {
+ dataStore.edit { prefs ->
+ prefs[UNPACK_DIR] = dir
+ }
+ }
+
+ suspend fun setUnpackInDownload(unpackInDownload: Boolean) {
+ dataStore.edit { prefs ->
+ prefs[UNPACK_IN_DOWNLOAD] = unpackInDownload
+ }
+ }
+
+ suspend fun setDeleteAfterUnpack(deleteAfterUnpack: Boolean) {
+ dataStore.edit { prefs ->
+ prefs[DELETE_AFTER_UNPACK] = deleteAfterUnpack
+ }
+ }
+
+ suspend fun setPkg2zipParams(params: String) {
+ dataStore.edit { prefs ->
+ prefs[PKG2ZIP_PARAMS] = params
+ }
+ }
+
+ suspend fun setPkg2zipAutoDecrypt(autoDecrypt: Boolean) {
+ dataStore.edit { prefs ->
+ prefs[PKG2ZIP_AUTO_DECRYPT] = autoDecrypt
+ }
+ }
+
+ fun tsvFiles() = tsvFiles
+
+ suspend fun setTSVFile(console: ConsoleType, contentType: PackageItemType, file: Uri) {
+ tsvFiles.emit(Triple(file.toString(), console, contentType))
+
+ dataStore.edit { prefs ->
+ when (console) {
+ ConsoleType.PSVITA -> {
+ when (contentType) {
+ PackageItemType.GAME -> prefs[PSV_GAMES] = file.toString()
+ PackageItemType.DLC -> prefs[PSV_DLC] = file.toString()
+ PackageItemType.THEME -> prefs[PSV_THEMES] = file.toString()
+ }
+ }
+ ConsoleType.PS3 -> {
+ when (contentType) {
+ PackageItemType.GAME -> prefs[PS3_GAMES] = file.toString()
+ PackageItemType.DLC -> prefs[PS3_DLC] = file.toString()
+ else -> {}
+ }
+ }
+ ConsoleType.PSP -> {
+ when (contentType) {
+ PackageItemType.GAME -> prefs[PSP_GAMES] = file.toString()
+ PackageItemType.DLC -> prefs[PSP_DLC] = file.toString()
+ else -> {}
+ }
+ }
+ ConsoleType.PSX -> {
+ when (contentType) {
+ PackageItemType.GAME -> prefs[PSX_GAMES] = file.toString()
+ else -> {}
+ }
+ }
+ ConsoleType.PSM -> {
+ when (contentType) {
+ PackageItemType.GAME -> prefs[PSM_GAMES] = file.toString()
+ else -> {}
+ }
+ }
+ }
+ }
+ }
+
+ private fun mapSettingsPreferences(prefs: Preferences): SettingsPreferences {
+ return SettingsPreferences(
+ prefs[APP_THEME] ?: Theme.SYSTEM.ordinal,
+ prefs[LAYOUT] ?: ItemLayout.LIST.ordinal,
+ prefs[PSV_GAMES] ?: "",
+ prefs[PS3_GAMES] ?: "",
+ prefs[PSP_GAMES] ?: "",
+ prefs[PSX_GAMES] ?: "",
+ prefs[PSM_GAMES] ?: "",
+ prefs[PSV_DLC] ?: "",
+ prefs[PS3_DLC] ?: "",
+ prefs[PSP_DLC] ?: "",
+ prefs[PSV_THEMES] ?: "",
+ prefs[HMAC_KEY] ?: "",
+ prefs[DOWNLOAD_DIR] ?: defaultDownloadDir,
+ prefs[UNPACK_DIR] ?: defaultDownloadDir,
+ prefs[UNPACK_IN_DOWNLOAD] ?: false,
+ prefs[DELETE_AFTER_UNPACK] ?: false,
+ prefs[PKG2ZIP_PARAMS] ?: "-x {pkgFile} \"{zRifKey}\"",
+ prefs[PKG2ZIP_AUTO_DECRYPT] ?: true,
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/model/ConsoleType.kt b/app/src/main/java/com/illusionware/npsbrowser/model/ConsoleType.kt
new file mode 100644
index 0000000..6b3085a
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/model/ConsoleType.kt
@@ -0,0 +1,9 @@
+package com.illusionware.npsbrowser.model
+
+enum class ConsoleType {
+ PSVITA,
+ PS3,
+ PSP,
+ PSX,
+ PSM,
+}
diff --git a/app/src/main/java/com/illusionware/npsbrowser/model/PackageItem.kt b/app/src/main/java/com/illusionware/npsbrowser/model/PackageItem.kt
new file mode 100644
index 0000000..1910ad2
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/model/PackageItem.kt
@@ -0,0 +1,18 @@
+package com.illusionware.npsbrowser.model
+
+data class PackageItem(
+ val titleId: String,
+ val region: String,
+ val name: 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 consoleType: ConsoleType,
+ val dataType: PackageItemType,
+)
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/model/PackageItemType.kt b/app/src/main/java/com/illusionware/npsbrowser/model/PackageItemType.kt
new file mode 100644
index 0000000..20b7f8b
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/model/PackageItemType.kt
@@ -0,0 +1,7 @@
+package com.illusionware.npsbrowser.model
+
+enum class PackageItemType {
+ GAME,
+ DLC,
+ THEME,
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt
index 3097b80..20d7062 100644
--- a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt
+++ b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt
@@ -2,6 +2,7 @@ package com.illusionware.npsbrowser.ui.pages
import android.annotation.SuppressLint
import android.app.Activity
+import android.content.Intent
import android.content.pm.ActivityInfo
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
@@ -68,7 +69,6 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
-import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.graphicsLayer
@@ -77,21 +77,45 @@ import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle
+import androidx.compose.ui.text.font.Font
+import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.sp
-import com.illusionware.npsbrowser.ConsoleType
-import com.illusionware.npsbrowser.DataType
-import com.illusionware.npsbrowser.Preferences
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.illusionware.npsbrowser.data.SettingsPreferences
+import com.illusionware.npsbrowser.model.ConsoleType
+import com.illusionware.npsbrowser.model.PackageItemType
import com.illusionware.npsbrowser.ui.components.NPSIconButton
+import com.illusionware.npsbrowser.viewmodels.OnboardingViewModel
+import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
import kotlinx.coroutines.launch
const val PAGE_COUNT = 3
+val readexProFamily = FontFamily(
+ Font(R.font.readex_pro_extra_light, FontWeight.ExtraLight),
+ Font(R.font.readex_pro_light, FontWeight.Light),
+ Font(R.font.readex_pro_regular, FontWeight.Normal),
+ Font(R.font.readex_pro_medium, FontWeight.Medium),
+ Font(R.font.readex_pro_semi_bold, FontWeight.SemiBold),
+ Font(R.font.readex_pro_bold, FontWeight.Bold),
+)
+
@OptIn(ExperimentalFoundationApi::class)
@SuppressLint("SourceLockedOrientationActivity")
@Composable
-fun OnBoarding(navigateToHome: () -> Unit = {}, darkTheme: Boolean = false) {
+fun OnBoarding(
+ navigateToHome: () -> Unit = {},
+ darkTheme: Boolean = false,
+ onboardingViewModel: OnboardingViewModel = viewModel(
+ factory = OnboardingViewModel.Factory
+ ),
+ settingsViewModel: SettingsViewModel = viewModel(
+ factory = SettingsViewModel.Factory
+ ),
+) {
val systemUiController = rememberSystemUiController()
val pagerState = rememberPagerState(
initialPage = 0,
@@ -99,13 +123,14 @@ fun OnBoarding(navigateToHome: () -> Unit = {}, darkTheme: Boolean = false) {
)
val scope = rememberCoroutineScope()
val context = LocalContext.current
+ val settingsPrefs = settingsViewModel.uiState.collectAsStateWithLifecycle().value
val darkIcons = when (pagerState.currentPage) {
0 -> false
else -> true
}
- DisposableEffect(systemUiController, darkIcons) {
+ DisposableEffect(systemUiController) {
systemUiController.setStatusBarColor(
color = Color.Transparent,
darkIcons = darkIcons && !darkTheme
@@ -123,18 +148,48 @@ fun OnBoarding(navigateToHome: () -> Unit = {}, darkTheme: Boolean = false) {
HorizontalPager(pageCount = PAGE_COUNT, state = pagerState) { page ->
when (page) {
- 0 -> OnBoardingInitial(incrementPage = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } })
+ 0 -> OnBoardingInitial(
+ incrementPage = {
+ scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
+ },
+ )
1 -> OnBoardingSetupTSVs(
page = page,
- incrementPage = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } },
- skipToEnd = { scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) } }
+ incrementPage = {
+ scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
+ },
+ skipToEnd = {
+ scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
+ },
+ settingsViewModel,
)
+// 2 -> OnBoardingStoragePermission(
+// page = page,
+// incrementPage = {
+// scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
+// },
+// decrementPage = {
+// scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
+// },
+// skipToEnd = {
+// scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
+// },
+// )
2 -> OnBoardingSetupSettings(
page = page,
- incrementPage = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } },
- decrementPage = { scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) } },
- skipToEnd = { scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) } },
- navigateToHome = navigateToHome
+ incrementPage = {
+ scope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) }
+ },
+ decrementPage = {
+ scope.launch { pagerState.animateScrollToPage(pagerState.currentPage - 1) }
+ },
+ skipToEnd = {
+ scope.launch { pagerState.animateScrollToPage(PAGE_COUNT - 1) }
+ },
+ navigateToHome = navigateToHome,
+ settingsViewModel,
+ onboardingViewModel,
+ settingsPrefs,
)
}
}
@@ -232,15 +287,12 @@ fun OnBoardingInitial(incrementPage: () -> Unit = {}) {
}
@Composable
-@Preview
fun OnBoardingSetupTSVs(
page: Int = 1,
incrementPage: () -> Unit = {},
- skipToEnd: () -> Unit = {}
+ skipToEnd: () -> Unit = {},
+ settingsViewModel: SettingsViewModel,
) {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
-
Scaffold { padding ->
Column(
Modifier
@@ -291,49 +343,49 @@ fun OnBoardingSetupTSVs(
TSVGroup(title = "PS Vita") {
TSVButton(
"Games",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.GAMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.GAME, it) }
)
TSVButton(
"Themes",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.THEMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.THEME, it) }
)
TSVButton(
"DLC",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.DLC, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.DLC, it) }
)
}
TSVGroup(title = "PSP") {
TSVButton(
"Games",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSP, DataType.GAMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSP, PackageItemType.GAME, it) }
)
TSVButton(
"DLC",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSP, DataType.DLC, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSP, PackageItemType.DLC, it) }
)
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PS3") {
TSVButton(
"Games",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PS3, DataType.GAMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PS3, PackageItemType.GAME, it) }
)
TSVButton(
"DLC",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PS3, DataType.DLC, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PS3, PackageItemType.DLC, it) }
)
Box(modifier = Modifier.width(110.dp))
}
TSVGroup(title = "PSX") {
TSVButton(
"Games",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSX, DataType.GAMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSX, PackageItemType.GAME, it) }
)
}
TSVGroup(title = "PSM") {
TSVButton(
"Games",
- onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSM, DataType.GAMES, it) }}
+ onFinish = { settingsViewModel.setTSVFile(ConsoleType.PSM, PackageItemType.GAME, it) }
)
}
}
@@ -400,19 +452,24 @@ fun TSVGroup(title: String, content: @Composable RowScope.() -> Unit) {
@Composable
fun TSVButton(title: String, onFinish: (Uri) -> Unit) {
+ val context = LocalContext.current
var hasSelected by rememberSaveable { mutableStateOf(false) }
val filePicker = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
+ contract = ActivityResultContracts.OpenDocument(),
) { fileUri ->
if (fileUri != null) {
+ context.contentResolver.takePersistableUriPermission(
+ fileUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
onFinish(fileUri)
hasSelected = true
}
}
FilledTonalButton(
onClick = {
- filePicker.launch("text/tab-separated-values")
+ filePicker.launch(arrayOf("text/tab-separated-values"))
},
modifier = Modifier
.width(110.dp)
@@ -470,34 +527,114 @@ fun TSVButton(title: String, onFinish: (Uri) -> Unit) {
}
}
+//@Composable
+//fun OnBoardingStoragePermission(
+// page: Int = 2,
+// incrementPage: () -> Unit = {},
+// decrementPage: () -> Unit = {},
+// skipToEnd: () -> Unit = {},
+//) {
+// val hasPermission = when (PackageManager.PERMISSION_GRANTED) {
+// ContextCompat.checkSelfPermission(
+// LocalContext.current,
+// Manifest.permission.WRITE_EXTERNAL_STORAGE
+// ) -> true
+// else -> false
+// }
+//
+// val permissionLegacy = rememberLauncherForActivityResult(
+// contract = ActivityResultContracts.RequestPermission(),
+// ) {}
+//
+// Scaffold { padding ->
+// Column(
+// Modifier
+// .padding(padding)
+// .padding(16.dp)
+// .fillMaxSize(),
+// verticalArrangement = Arrangement.SpaceBetween
+// ) {
+// Column(
+// Modifier
+// .padding(vertical = 32.dp)
+// .weight(1.0f)
+// .fillMaxHeight(),
+// horizontalAlignment = Alignment.CenterHorizontally,
+// ) {
+// Image(
+// painter = painterResource(id = R.drawable.folder_bubbles),
+// contentDescription = "Folder Image",
+// contentScale = ContentScale.Crop,
+// modifier = Modifier.fillMaxWidth()
+// )
+// Text(
+// text = "Storage".uppercase(),
+// style = TextStyle.Default.copy(
+// fontWeight = FontWeight.Bold,
+// fontSize = 28.sp,
+// fontFamily = readexProFamily
+// ),
+// )
+// Spacer(modifier = Modifier.height(24.dp))
+// Text(
+// "Granting storage permission allows us to bypass newer Android limitations and choose any directory as your download directory",
+// style = TextStyle.Default.copy(
+// fontSize = 20.sp,
+// fontFamily = readexProFamily
+// ),
+// textAlign = TextAlign.Center,
+// )
+// Spacer(modifier = Modifier
+// .fillMaxHeight()
+// .weight(1.0f))
+// Button(
+// onClick = {
+// permissionLegacy.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
+// },
+// modifier = Modifier.fillMaxWidth(),
+// enabled = !hasPermission,
+// ) {
+// Text(
+// text = if (hasPermission) "Granted" else "Grant",
+// style = TextStyle.Default.copy(
+// fontSize = 18.sp,
+// fontFamily = readexProFamily
+// ),
+// )
+// }
+// }
+// Column(Modifier.fillMaxWidth()) {
+// OnBoardingBottomButtons(
+// page = page,
+// incrementPage = incrementPage,
+// decrementPage = decrementPage,
+// skipToEnd = skipToEnd
+// )
+// }
+// }
+// }
+//}
+
@Composable
-@Preview
fun OnBoardingSetupSettings(
page: Int = 2,
incrementPage: () -> Unit = {},
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
+ settingsViewModel: SettingsViewModel,
+ onboardingViewModel: OnboardingViewModel,
+ settingsPrefs: SettingsPreferences,
) {
val clipboard = LocalClipboardManager.current
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
var hmacKey by rememberSaveable(stateSaver = TextFieldValue.Saver) { mutableStateOf(TextFieldValue("")) }
- var pkg2zipArgs = prefs.pkg2zipParams.collectAsState(initial = "").value
- val downloadDir = prefs.downloadDir.collectAsState(initial = "").value
- val unpackDir = prefs.unpackDir.collectAsState(initial = "").value
- val unpackInDownload = prefs.unpackInDownload.collectAsState(initial = false).value
- val deleteAfterUnpack = prefs.deleteAfterUnpack.collectAsState(initial = false).value
- val autoDecrypt = prefs.pkg2zipAutoDecrypt.collectAsState(initial = true).value
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
- scope.launch {
- prefs.setDownloadDir(dirUri.toString())
- }
+ settingsViewModel.setDownloadDir(dirUri.toString())
}
}
@@ -505,17 +642,13 @@ fun OnBoardingSetupSettings(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
- scope.launch {
- prefs.setUnpackDir(dirUri.toString())
- }
+ settingsViewModel.setUnpackDir(dirUri.toString())
}
}
fun setHmacKey(key: String) {
if (key.isNotEmpty()) {
- scope.launch {
- prefs.setHMACKey(key)
- }
+ settingsViewModel.setHMACKey(key)
}
}
@@ -592,20 +725,16 @@ fun OnBoardingSetupSettings(
Column {
Toggleable(
title = "Unpack in download directory",
- checked = unpackInDownload,
+ checked = settingsPrefs.unpackInDownload,
onCheckedChange = {
- scope.launch {
- prefs.setUnpackInDownload(it)
- }
+ settingsViewModel.setUnpackInDownload(it)
},
)
Toggleable(
title = "Delete package after unpacking",
- checked = deleteAfterUnpack,
+ checked = settingsPrefs.deleteAfterUnpack,
onCheckedChange = {
- scope.launch {
- prefs.setDeleteAfterUnpack(it)
- }
+ settingsViewModel.setDeleteAfterUnpack(it)
},
)
Box(
@@ -618,7 +747,7 @@ fun OnBoardingSetupSettings(
text = "Download Directory",
)
Text(
- text = Uri.decode(downloadDir).ifEmpty { "None" },
+ text = Uri.decode(settingsPrefs.downloadDir).ifEmpty { "None" },
color = MaterialTheme.colorScheme.outline,
)
}
@@ -633,7 +762,7 @@ fun OnBoardingSetupSettings(
text = "Unpack Directory",
)
Text(
- text = Uri.decode(unpackDir).ifEmpty { "None" },
+ text = Uri.decode(settingsPrefs.unpackDir).ifEmpty { "None" },
color = MaterialTheme.colorScheme.outline,
)
}
@@ -648,23 +777,18 @@ fun OnBoardingSetupSettings(
)
Toggleable(
title = "Automatically decrypt downloaded content",
- checked = autoDecrypt,
+ checked = settingsPrefs.pkg2zipAutoDecrypt,
onCheckedChange = {
- scope.launch {
- prefs.setPkg2zipAutoDecrypt(it)
- }
+ settingsViewModel.setPkg2zipAutoDecrypt(it)
},
)
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
- value = pkg2zipArgs,
+ value = settingsPrefs.pkg2zipParams,
onValueChange = {
- pkg2zipArgs = it
- scope.launch {
- prefs.setPkg2zipParams(it)
- }
+ settingsViewModel.setPkg2zipParams(it)
}, // TODO: validate args are legal ??
label = { Text(text = "Pkg2zip args") },
placeholder = { Text(text = "-x {pkgFile} \"{zRifKey}\"") },
@@ -679,7 +803,9 @@ fun OnBoardingSetupSettings(
incrementPage = incrementPage,
decrementPage = decrementPage,
skipToEnd = skipToEnd,
- navigateToHome = navigateToHome
+ navigateToHome = navigateToHome,
+ settingsViewModel = settingsViewModel,
+ onboardingViewModel = onboardingViewModel,
)
}
}
@@ -714,17 +840,18 @@ fun OnBoardingBottomButtons(
decrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {},
+ settingsViewModel: SettingsViewModel? = null,
+ onboardingViewModel: OnboardingViewModel? = null
) {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
+ val finalPage = PAGE_COUNT - 1
Row(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.1f),
- horizontalArrangement = if (page != 2) Arrangement.SpaceBetween else Arrangement.End,
+ horizontalArrangement = if (page != finalPage) Arrangement.SpaceBetween else Arrangement.End,
) {
- if (page == 1) {
+ if (page != finalPage) {
FilledTonalButton(
onClick = skipToEnd,
colors = ButtonDefaults.filledTonalButtonColors(
@@ -741,13 +868,11 @@ fun OnBoardingBottomButtons(
Text(text = "Prev")
}
}
- if (page == 2) {
+ if (page == finalPage) {
Button(
onClick = {
navigateToHome()
- scope.launch {
- prefs.finishOnboarding()
- }
+ onboardingViewModel?.finishOnboarding(settingsViewModel!!.shouldCreateDefaultDir())
},
contentPadding = PaddingValues(horizontal = 18.dp)
) {
diff --git a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt
index 88e685d..cec5d6f 100644
--- a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt
+++ b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt
@@ -1,5 +1,6 @@
package com.illusionware.npsbrowser.ui.pages
+import android.content.Intent
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -22,16 +23,10 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentPaste
-import androidx.compose.material.icons.outlined.ArrowCircleDown
-import androidx.compose.material.icons.outlined.ArrowDownward
import androidx.compose.material.icons.outlined.ArrowDropDown
-import androidx.compose.material.icons.outlined.ArrowOutward
-import androidx.compose.material.icons.outlined.ArrowUpward
-import androidx.compose.material.icons.outlined.CompareArrows
import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.Palette
-import androidx.compose.material.icons.outlined.SubdirectoryArrowRight
import androidx.compose.material.icons.outlined.Unarchive
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -42,11 +37,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
-import androidx.compose.runtime.collectAsState
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
@@ -59,28 +52,37 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
-import com.illusionware.npsbrowser.ConsoleType
-import com.illusionware.npsbrowser.DataType
-import com.illusionware.npsbrowser.Preferences
+import androidx.compose.ui.unit.sp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.ui.components.NPSAlertDialog
import com.illusionware.npsbrowser.ui.components.NPSIconButton
import com.illusionware.npsbrowser.ui.components.NPSRadioButton
import com.illusionware.npsbrowser.ui.theme.ColorSecondaryLight
import com.illusionware.npsbrowser.ui.theme.Typography
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.launch
+import com.illusionware.npsbrowser.viewmodels.SettingsViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.illusionware.npsbrowser.data.SettingsPreferences
+import com.illusionware.npsbrowser.data.Theme
+import com.illusionware.npsbrowser.model.ConsoleType
+import com.illusionware.npsbrowser.model.PackageItemType
-val Themes = hashMapOf(
- 0 to "Light",
- 1 to "Dark",
- 2 to "Use System Theme"
+private val Themes = hashMapOf(
+ Theme.LIGHT.ordinal to "Light",
+ Theme.DARK.ordinal to "Dark",
+ Theme.SYSTEM.ordinal to "Use System Theme"
)
@Composable
@Preview
-fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) -> Unit = {}) {
+fun SettingsScreen(
+ navigationGoBack: () -> Unit = {},
+ viewModel: SettingsViewModel = viewModel(
+ factory = SettingsViewModel.Factory
+ )
+) {
+ val prefs = viewModel.uiState.collectAsStateWithLifecycle().value
+
Scaffold (
topBar = {
Row(
@@ -102,11 +104,11 @@ fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) ->
.verticalScroll(rememberScrollState())
) {
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) {
- Appearance(onThemeChange)
- TsvFiles()
- Updates()
- Downloads()
- Pkg2Zip()
+ Appearance(viewModel, prefs)
+ TsvFiles(viewModel, prefs)
+ Updates(viewModel, prefs)
+ Downloads(viewModel, prefs)
+ Pkg2Zip(viewModel, prefs)
}
}
@@ -114,83 +116,63 @@ fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) ->
}
@Composable
-fun Appearance(onThemeChange: (v: Int) -> Unit) {
- val prefs = Preferences(LocalContext.current)
- val theme = prefs.theme.collectAsState(initial = 0)
-
+fun Appearance(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var themeOpenDialog by remember { mutableStateOf(false) }
SettingGroup(title = stringResource(id = R.string.appearance)) {
DialogSetting(
title = stringResource(id = R.string.theme),
- value = Themes[theme.value] ?: "None",
+ value = Themes[prefs.appTheme] ?: "None",
icon = Icons.Outlined.Palette,
onClick = { themeOpenDialog = true }
)
}
if (themeOpenDialog) {
ThemeDialog(
+ prefs = prefs,
onDismiss = { themeOpenDialog = false },
- onThemeChange = onThemeChange
+ onThemeChange = {
+ viewModel.setTheme(it)
+ }
)
}
}
@Composable
-fun TsvFiles() {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
-
+fun TsvFiles(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var expanded by remember { mutableStateOf(false) }
- val psvGames = prefs.psvGames.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val psvDlc = prefs.psvDLC.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val psvThemes = prefs.psvThemes.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val ps3Games = prefs.ps3Games.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val ps3Dlc = prefs.ps3DLC.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val pspGames = prefs.pspGames.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val pspDlc = prefs.pspDLC.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val psxGames = prefs.psxGames.collectAsState(initial = "").value.ifEmpty { "Browse" }
- val psmGames = prefs.psmGames.collectAsState(initial = "").value.ifEmpty { "Browse" }
SettingGroup(title = "TSV Files") {
TSVPicker(
title = "PSV Games",
- value = Uri.decode(psvGames),
+ value = Uri.decode(prefs.psvGames.ifEmpty { "Browse" }),
icon = painterResource(id = R.drawable.vita_icon),
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSVITA, DataType.GAMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSV DLC",
- value = Uri.decode(psvDlc),
+ value = Uri.decode(prefs.psvDlc.ifEmpty { "Browse" }),
icon = null,
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSVITA, DataType.DLC, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSV Themes",
- value = Uri.decode(psvThemes),
+ value = Uri.decode(prefs.psvThemes.ifEmpty { "Browse" }),
icon = null,
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSVITA, DataType.THEMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSVITA, PackageItemType.THEME, it)
},
)
TSVPicker(
title = "PS3 Games",
- value = Uri.decode(ps3Games),
+ value = Uri.decode(prefs.ps3Games).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.ps3_icon),
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PS3, DataType.GAMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PS3, PackageItemType.GAME, it)
}
)
if (!expanded) {
@@ -209,52 +191,42 @@ fun TsvFiles() {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
TSVPicker(
title = "PS3 DLC",
- value = Uri.decode(ps3Dlc),
+ value = Uri.decode(prefs.ps3Dlc).ifEmpty { "Browse" },
icon = null,
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PS3, DataType.DLC, it)
- }
+ viewModel.setTSVFile(ConsoleType.PS3, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSP Games",
- value = Uri.decode(pspGames),
+ value = Uri.decode(prefs.pspGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psp_icon),
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSP, DataType.GAMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSP, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSP DLC",
- value = Uri.decode(pspDlc),
+ value = Uri.decode(prefs.pspDlc).ifEmpty { "Browse" },
icon = null,
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSP, DataType.DLC, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSP, PackageItemType.DLC, it)
}
)
TSVPicker(
title = "PSX Games",
- value = Uri.decode(psxGames),
+ value = Uri.decode(prefs.psxGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psx_icon),
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSX, DataType.GAMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSX, PackageItemType.GAME, it)
}
)
TSVPicker(
title = "PSM Games",
- value = Uri.decode(psmGames),
+ value = Uri.decode(prefs.psmGames).ifEmpty { "Browse" },
icon = painterResource(id = R.drawable.psm_icon),
onFilePick = {
- scope.launch(Dispatchers.IO) {
- prefs.setTSVFile(ConsoleType.PSM, DataType.GAMES, it)
- }
+ viewModel.setTSVFile(ConsoleType.PSM, PackageItemType.GAME, it)
}
)
}
@@ -269,10 +241,16 @@ fun TSVPicker(
icon: Painter? = null,
onFilePick: (Uri) -> Unit,
) {
+ val context = LocalContext.current
+
val filePicker = rememberLauncherForActivityResult(
- contract = ActivityResultContracts.GetContent(),
+ contract = ActivityResultContracts.OpenDocument(),
) { fileUri ->
if (fileUri != null) {
+ context.contentResolver.takePersistableUriPermission(
+ fileUri,
+ Intent.FLAG_GRANT_READ_URI_PERMISSION
+ )
onFilePick(fileUri)
}
}
@@ -282,28 +260,25 @@ fun TSVPicker(
value = value,
icon = icon,
onClick = {
- filePicker.launch("text/tab-separated-values")
+ filePicker.launch(arrayOf("text/tab-separated-values"))
}
)}
@Composable
-fun Updates() {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
+fun Updates(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
val clipboard = LocalClipboardManager.current
- val hmacKey = prefs.hmacKey.collectAsState(initial = "").value
var hmacOpenDialog by remember { mutableStateOf(false) }
var dialogHmacKey by remember { mutableStateOf("") }
SideEffect {
- dialogHmacKey = hmacKey
+ dialogHmacKey = prefs.hmacKey
}
SettingGroup(title = "Updates") {
DialogSetting(
title = "HMAC Key",
- value = hmacKey.ifEmpty { "None" },
+ value = prefs.hmacKey.ifEmpty { "None" },
icon = Icons.Outlined.Key,
onClick = { hmacOpenDialog = true }
)
@@ -317,9 +292,7 @@ fun Updates() {
Text(text = "Cancel")
}
TextButton(onClick = {
- scope.launch {
- prefs.setHMACKey(dialogHmacKey)
- }
+ viewModel.setHMACKey(dialogHmacKey)
hmacOpenDialog = false
}) {
Text(text = "OK")
@@ -349,21 +322,12 @@ fun Updates() {
}
@Composable
-fun Downloads() {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
- val downloadDir = prefs.downloadDir.collectAsState(initial = "").value
- val unpackDir = prefs.unpackDir.collectAsState(initial = "").value
- val unpackInDownload = prefs.unpackInDownload.collectAsState(initial = false).value
- val deleteAfterUnpack = prefs.deleteAfterUnpack.collectAsState(initial = false).value
-
+fun Downloads(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
- scope.launch {
- prefs.setDownloadDir(dirUri.toString())
- }
+ viewModel.setDownloadDir(dirUri.toString())
}
}
@@ -371,34 +335,28 @@ fun Downloads() {
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
- scope.launch {
- prefs.setUnpackDir(dirUri.toString())
- }
+ viewModel.setUnpackDir(dirUri.toString())
}
}
SettingGroup(title = "Downloads") {
ToggleSetting(
title = "Unpack in Download Directory",
- checked = unpackInDownload,
+ checked = prefs.unpackInDownload,
onClick = {
- scope.launch {
- prefs.setUnpackInDownload(it)
- }
+ viewModel.setUnpackInDownload(it)
}
)
ToggleSetting(
title = "Delete package after unpacking",
- checked = deleteAfterUnpack,
+ checked = prefs.deleteAfterUnpack,
onClick = {
- scope.launch {
- prefs.setDeleteAfterUnpack(it)
- }
+ viewModel.setDeleteAfterUnpack(it)
}
)
DialogSetting(
title = "Download Directory",
- value = Uri.decode(downloadDir),
+ value = Uri.decode(prefs.downloadDir).ifEmpty { "None" },
icon = Icons.Outlined.FileDownload,
onClick = {
downloadDirPicker.launch(Uri.EMPTY)
@@ -406,7 +364,7 @@ fun Downloads() {
)
DialogSetting(
title = "Unpack Directory",
- value = Uri.decode(unpackDir),
+ value = Uri.decode(prefs.unpackDir).ifEmpty { "None" },
icon = Icons.Outlined.Unarchive,
onClick = {
unpackDirPicker.launch(Uri.EMPTY)
@@ -416,32 +374,25 @@ fun Downloads() {
}
@Composable
-fun Pkg2Zip() {
- val prefs = Preferences(LocalContext.current)
- val scope = rememberCoroutineScope()
-
- val autoDecrypt = prefs.pkg2zipAutoDecrypt.collectAsState(initial = true).value
- val pkg2zipParams = prefs.pkg2zipParams.collectAsState(initial = "").value
+fun Pkg2Zip(viewModel: SettingsViewModel, prefs: SettingsPreferences) {
var pkg2zipOpenDialog by remember { mutableStateOf(false) }
var dialogPkg2zipParams by remember { mutableStateOf("") }
SideEffect {
- dialogPkg2zipParams = pkg2zipParams
+ dialogPkg2zipParams = prefs.pkg2zipParams
}
SettingGroup(title = "Pkg2Zip") {
ToggleSetting(
title = "Automatically decrypt downloaded content",
- checked = autoDecrypt,
+ checked = prefs.pkg2zipAutoDecrypt,
onClick = {
- scope.launch {
- prefs.setPkg2zipAutoDecrypt(it)
- }
+ viewModel.setPkg2zipAutoDecrypt(it)
}
)
DialogSetting(
title = "Decryption params",
- value = pkg2zipParams.ifEmpty { "None" },
+ value = prefs.pkg2zipParams.ifEmpty { "None" },
icon = painterResource(
id = R.drawable.lock_open
),
@@ -457,9 +408,7 @@ fun Pkg2Zip() {
Text(text = "Cancel")
}
TextButton(onClick = {
- scope.launch {
- prefs.setPkg2zipParams(dialogPkg2zipParams)
- }
+ viewModel.setPkg2zipParams(dialogPkg2zipParams)
pkg2zipOpenDialog = false
}) {
Text(text = "OK")
@@ -478,10 +427,11 @@ fun Pkg2Zip() {
}
@Composable
-fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) {
- val prefs = Preferences(LocalContext.current)
- val theme = prefs.theme.collectAsState(initial = "")
-
+fun ThemeDialog(
+ prefs: SettingsPreferences,
+ onDismiss: () -> Unit,
+ onThemeChange: (v: Theme) -> Unit
+) {
NPSAlertDialog(
onDismiss = onDismiss,
title = stringResource(id = R.string.theme),
@@ -495,27 +445,24 @@ fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) {
Column {
NPSRadioButton(
text = "Light",
- onClick = { CoroutineScope(Dispatchers.IO).launch {
- onThemeChange(0)
- prefs.changeTheme(0) }
+ onClick = {
+ onThemeChange(Theme.LIGHT)
},
- selected = theme.value == 0,
+ selected = prefs.appTheme == Theme.LIGHT.ordinal,
)
NPSRadioButton(
text = "Dark",
- onClick = { CoroutineScope(Dispatchers.IO).launch {
- onThemeChange(1)
- prefs.changeTheme(1) }
+ onClick = {
+ onThemeChange(Theme.DARK)
},
- selected = theme.value == 1,
+ selected = prefs.appTheme == Theme.DARK.ordinal,
)
NPSRadioButton(
text = "Use System Theme",
- onClick = { CoroutineScope(Dispatchers.IO).launch {
- onThemeChange(2)
- prefs.changeTheme(2) }
+ onClick = {
+ onThemeChange(Theme.SYSTEM)
},
- selected = theme.value == 2,
+ selected = prefs.appTheme == Theme.SYSTEM.ordinal,
)
}
}
@@ -553,9 +500,9 @@ fun DialogSetting(title: String, value: String, icon: Painter?, onClick: () -> U
} else {
Box(modifier = Modifier.width(56.dp))
}
- Column {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
- Text(text = value, color = ColorSecondaryLight)
+ Text(text = value, color = ColorSecondaryLight, fontSize = 14.sp)
}
}
}
@@ -583,11 +530,12 @@ fun DialogSetting(
} else {
Box(modifier = Modifier.width(56.dp))
}
- Column {
+ Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(
text = value,
color = ColorSecondaryLight,
+ fontSize = 14.sp,
maxLines = if (singleLine) 1 else Int.MAX_VALUE,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.padding(end = 24.dp)
diff --git a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/OnboardingViewModel.kt b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/OnboardingViewModel.kt
new file mode 100644
index 0000000..468d20c
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/OnboardingViewModel.kt
@@ -0,0 +1,52 @@
+package com.illusionware.npsbrowser.viewmodels
+
+import android.os.Environment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.illusionware.npsbrowser.NPSApp
+import com.illusionware.npsbrowser.data.OnboardingPreferences
+import com.illusionware.npsbrowser.data.OnboardingPreferencesRepository
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+import java.io.File
+
+class OnboardingViewModel(
+ private val onboardingPreferencesRepository: OnboardingPreferencesRepository
+): ViewModel() {
+ val uiState: StateFlow =
+ onboardingPreferencesRepository.getPreferences().stateIn(
+ viewModelScope,
+ SharingStarted.WhileSubscribed(2_000),
+ OnboardingPreferences(
+ false,
+ )
+ )
+
+ fun finishOnboarding(shouldCreateDefaultDir: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ if (shouldCreateDefaultDir) {
+ val dir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "NPSBrowser")
+ if (!dir.exists()) {
+ dir.mkdir()
+ }
+ }
+ onboardingPreferencesRepository.setSeenOnboarding(true)
+ }
+ }
+
+ companion object {
+ val Factory: ViewModelProvider.Factory = viewModelFactory {
+ initializer {
+ val application = (this[APPLICATION_KEY] as NPSApp)
+ OnboardingViewModel(application.onboardingPreferencesRepository)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt
index 09885dc..c68db0c 100644
--- a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt
+++ b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/PackageListData.kt
@@ -1,45 +1,152 @@
package com.illusionware.npsbrowser.viewmodels
+import android.net.Uri
import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
import com.github.doyaaaaaken.kotlincsv.dsl.context.ExcessFieldsRowBehaviour
import com.github.doyaaaaaken.kotlincsv.dsl.context.InsufficientFieldsRowBehaviour
import com.github.doyaaaaaken.kotlincsv.dsl.csvReader
-import com.illusionware.npsbrowser.ConsoleType
-import com.illusionware.npsbrowser.DataType
+import com.illusionware.npsbrowser.NPSApp
+import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
+import com.illusionware.npsbrowser.model.ConsoleType
+import com.illusionware.npsbrowser.model.PackageItem
+import com.illusionware.npsbrowser.model.PackageItemType
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
-
-data class Package(
- val id: String,
- val title: String,
- val region: String,
- val minFW: String,
- val pkgUrl: String,
- val pkgSize: Int,
- val consoleType: ConsoleType,
- val dataType: DataType,
-)
+import kotlinx.coroutines.launch
+import java.io.InputStream
data class PackageListUiState(
- val packages: List = emptyList(),
+ val packages: ArrayList = ArrayList(),
val error: String = "",
+ val isLoading: Boolean = true,
)
-class PackageListViewModel(): ViewModel() {
- // TODO: actually properly think about and implement this
+class PackageListViewModel(app: NPSApp, settingsRepo: SettingsPreferencesRepository): ViewModel() {
private val _uiState = MutableStateFlow(PackageListUiState())
val uiState: StateFlow = _uiState.asStateFlow()
+
private val tsvReader = csvReader {
- charset = "ISO_8859_1"
+ charset = "UTF-8"
quoteChar = '"'
delimiter = '\t'
excessFieldsRowBehaviour = ExcessFieldsRowBehaviour.TRIM
insufficientFieldsRowBehaviour = InsufficientFieldsRowBehaviour.EMPTY_STRING
}
- fun loadPackagesFromTsv() {
- val file = "psv_games.tsv"
- val rows = tsvReader.readAll(file)
+ init {
+ viewModelScope.launch(Dispatchers.IO) {
+ val list = ArrayList()
+
+ settingsRepo.availableTsvs().forEach {
+ list += parsePackageFromTSV(it, app)
+ }
+
+ list.sortBy { it.name }
+
+ _uiState.emit(PackageListUiState(isLoading = false, packages = list))
+
+ settingsRepo.tsvFiles().collect { tsv ->
+ _uiState.emit(PackageListUiState())
+
+ val alreadyExists = list.find { item ->
+ item.consoleType == tsv.second && item.dataType == tsv.third
+ }
+
+ if (alreadyExists != null) {
+ list.removeAll { item ->
+ item.consoleType == tsv.second && item.dataType == tsv.third
+ }
+ }
+
+ list.addAll(parsePackageFromTSV(tsv, app))
+ list.sortBy { it.name }
+
+ _uiState.emit(PackageListUiState(isLoading = false, packages = list))
+ }
+ }
+ }
+
+ private fun parsePackageFromTSV(
+ it: Triple, app: NPSApp
+ ): ArrayList {
+ val list = ArrayList()
+
+ val fileUri = it.first
+ val console = it.second
+ val type = it.third
+
+ val file: InputStream?
+ try {
+ file = app.contentResolver.openInputStream(Uri.parse(fileUri))
+ } catch (e: Exception) {
+ throw e
+ }
+ val tsv = tsvReader.readAllWithHeader(file!!)
+
+ tsv.forEach { row ->
+ var size = ""
+ val tempSize = row["File Size"]
+ if (!tempSize.isNullOrEmpty()) {
+ size = bytesIntoHumanReadable(tempSize.toLong())
+ }
+ list.add(
+ PackageItem(
+ row["Title ID"] ?: "",
+ row["Region"] ?: "",
+ row["Name"] ?: "",
+ row["PKG direct link"] ?: "",
+ row["Content ID"] ?: "",
+ row["Last Modification Date"] ?: "",
+ size,
+ row["SHA256"] ?: "",
+ row["zRIF"] ?: "",
+ row["RAP"] ?: "",
+ row["Required FW"] ?: "",
+ console,
+ type,
+ )
+ )
+ }
+
+ file.close()
+
+ return list
+ }
+
+ private fun bytesIntoHumanReadable(bytes: Long): String {
+ val kilobyte: Long = 1024
+ val megabyte = kilobyte * 1024
+ val gigabyte = megabyte * 1024
+ val terabyte = gigabyte * 1024
+
+ return if (bytes in 0 until kilobyte) {
+ "$bytes" + "B"
+ } else if (bytes in kilobyte until megabyte) {
+ String.format("%.2f", (bytes.toDouble() / kilobyte.toDouble())) + "KB"
+ } else if (bytes in megabyte until gigabyte) {
+ String.format("%.2f", (bytes.toDouble() / megabyte.toDouble())) + "MB"
+ } else if (bytes in gigabyte until terabyte) {
+ String.format("%.2f", (bytes.toDouble() / gigabyte.toDouble())) + "GB"
+ } else if (bytes >= terabyte) {
+ String.format("%.2f", (bytes.toDouble() / terabyte.toDouble())) + "TB"
+ } else {
+ "$bytes" + "B"
+ }
+ }
+
+ companion object {
+ val Factory: ViewModelProvider.Factory = viewModelFactory {
+ initializer {
+ val application = (this[APPLICATION_KEY] as NPSApp)
+ PackageListViewModel(application, application.settingsPreferencesRepository)
+ }
+ }
}
}
diff --git a/app/src/main/java/com/illusionware/npsbrowser/viewmodels/SettingsViewModel.kt b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/SettingsViewModel.kt
new file mode 100644
index 0000000..59d43dd
--- /dev/null
+++ b/app/src/main/java/com/illusionware/npsbrowser/viewmodels/SettingsViewModel.kt
@@ -0,0 +1,138 @@
+package com.illusionware.npsbrowser.viewmodels
+
+import android.net.Uri
+import android.os.Environment
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
+import androidx.lifecycle.viewModelScope
+import androidx.lifecycle.viewmodel.initializer
+import androidx.lifecycle.viewmodel.viewModelFactory
+import com.illusionware.npsbrowser.NPSApp
+import com.illusionware.npsbrowser.data.ItemLayout
+import com.illusionware.npsbrowser.data.SettingsPreferences
+import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
+import com.illusionware.npsbrowser.data.Theme
+import com.illusionware.npsbrowser.model.ConsoleType
+import com.illusionware.npsbrowser.model.PackageItemType
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.launch
+
+class SettingsViewModel(private val settingsPrefsRepo: SettingsPreferencesRepository): ViewModel() {
+ val uiState: StateFlow =
+ settingsPrefsRepo.getPreferences().stateIn(viewModelScope, SharingStarted.Lazily, SettingsPreferences(
+ Theme.SYSTEM.ordinal,
+ ItemLayout.LIST.ordinal,
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ "",
+ false,
+ false,
+ "",
+ true,
+ ))
+
+ fun availableTsvs(): MutableList> {
+ val list = mutableListOf>()
+ if (uiState.value.psvGames.isNotEmpty()) list.add(Pair(ConsoleType.PSVITA, PackageItemType.GAME))
+ if (uiState.value.ps3Games.isNotEmpty()) list.add(Pair(ConsoleType.PS3, PackageItemType.GAME))
+ if (uiState.value.pspGames.isNotEmpty()) list.add(Pair(ConsoleType.PSP, PackageItemType.GAME))
+ if (uiState.value.psxGames.isNotEmpty()) list.add(Pair(ConsoleType.PSX, PackageItemType.GAME))
+ if (uiState.value.psmGames.isNotEmpty()) list.add(Pair(ConsoleType.PSM, PackageItemType.GAME))
+
+ return list
+ }
+
+ suspend fun getAppTheme(): Int {
+ return settingsPrefsRepo.getPreferences().first().appTheme
+ }
+
+ fun shouldCreateDefaultDir(): Boolean {
+ val defaultDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS).absolutePath + "/NPSBrowser"
+
+ return uiState.value.downloadDir == defaultDir
+ || uiState.value.unpackDir == defaultDir
+ }
+
+ fun setTSVFile(console: ConsoleType, itemType: PackageItemType, file: Uri) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setTSVFile(console, itemType, file)
+ }
+ }
+
+ fun setTheme(theme: Theme) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setTheme(theme)
+ }
+ }
+
+ fun setLayout(layout: ItemLayout) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setLayout(layout)
+ }
+ }
+
+ fun setHMACKey(hmacKey: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setHMACKey(hmacKey)
+ }
+ }
+
+ fun setDownloadDir(downloadDir: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setDownloadDir(downloadDir)
+ }
+ }
+
+ fun setUnpackDir(unpackDir: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setUnpackDir(unpackDir)
+ }
+ }
+
+ fun setUnpackInDownload(unpackInDownload: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setUnpackInDownload(unpackInDownload)
+ }
+ }
+
+ fun setDeleteAfterUnpack(deleteAfterUnpack: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setDeleteAfterUnpack(deleteAfterUnpack)
+ }
+ }
+
+ fun setPkg2zipParams(pkg2zipParams: String) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setPkg2zipParams(pkg2zipParams)
+ }
+ }
+
+ fun setPkg2zipAutoDecrypt(pkg2zipAutoDecrypt: Boolean) {
+ viewModelScope.launch(Dispatchers.IO) {
+ settingsPrefsRepo.setPkg2zipAutoDecrypt(pkg2zipAutoDecrypt)
+ }
+ }
+
+ companion object {
+ val Factory: ViewModelProvider.Factory = viewModelFactory {
+ initializer {
+ val application = (this[APPLICATION_KEY] as NPSApp)
+ SettingsViewModel(application.settingsPreferencesRepository)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/folder_bubbles.xml b/app/src/main/res/drawable/folder_bubbles.xml
new file mode 100644
index 0000000..590ac0d
--- /dev/null
+++ b/app/src/main/res/drawable/folder_bubbles.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/font/readex_pro_bold.ttf b/app/src/main/res/font/readex_pro_bold.ttf
new file mode 100644
index 0000000..7f56cba
Binary files /dev/null and b/app/src/main/res/font/readex_pro_bold.ttf differ
diff --git a/app/src/main/res/font/readex_pro_extra_light.ttf b/app/src/main/res/font/readex_pro_extra_light.ttf
new file mode 100644
index 0000000..f008ca6
Binary files /dev/null and b/app/src/main/res/font/readex_pro_extra_light.ttf differ
diff --git a/app/src/main/res/font/readex_pro_light.ttf b/app/src/main/res/font/readex_pro_light.ttf
new file mode 100644
index 0000000..bfbe921
Binary files /dev/null and b/app/src/main/res/font/readex_pro_light.ttf differ
diff --git a/app/src/main/res/font/readex_pro_medium.ttf b/app/src/main/res/font/readex_pro_medium.ttf
new file mode 100644
index 0000000..abd3cf9
Binary files /dev/null and b/app/src/main/res/font/readex_pro_medium.ttf differ
diff --git a/app/src/main/res/font/readex_pro_regular.ttf b/app/src/main/res/font/readex_pro_regular.ttf
new file mode 100644
index 0000000..93fc892
Binary files /dev/null and b/app/src/main/res/font/readex_pro_regular.ttf differ
diff --git a/app/src/main/res/font/readex_pro_semi_bold.ttf b/app/src/main/res/font/readex_pro_semi_bold.ttf
new file mode 100644
index 0000000..5ca7312
Binary files /dev/null and b/app/src/main/res/font/readex_pro_semi_bold.ttf differ
diff --git a/build.gradle.kts b/build.gradle.kts
index 4c8fd4a..8462ad7 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- val kotlin_version = "1.8.10"
+ val kotlin_version = "1.8.22"
repositories {
google()
@@ -20,6 +20,7 @@ allprojects {
repositories {
google()
mavenCentral()
+ maven( url = "https://jitpack.io" )
}
}