mirror of
https://github.com/IllusionMan1212/NPS-Browser.git
synced 2026-10-24 02:12:37 +00:00
use viewmodels and parse tsv files and display them
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
|
||||
<application
|
||||
android:name=".NPSApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.illusionware.npsbrowser
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.Preferences
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import com.illusionware.npsbrowser.data.OnboardingPreferencesRepository
|
||||
import com.illusionware.npsbrowser.data.SettingsPreferencesRepository
|
||||
|
||||
private const val PREFERENCES_NAME = "preferences"
|
||||
private val Context.dataStore: DataStore<Preferences> 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)
|
||||
}
|
||||
}
|
||||
@@ -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<Pair<ConsoleType, DataType>> {
|
||||
val list = mutableListOf<Pair<ConsoleType, DataType>>()
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Package>()
|
||||
// list.add(Package("test"))
|
||||
// val gs = prefs.psvGames.collectAsState(initial = emptyList<Package>())
|
||||
// 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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Preferences>,
|
||||
) {
|
||||
companion object {
|
||||
private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding")
|
||||
}
|
||||
|
||||
fun getPreferences(): Flow<OnboardingPreferences> =
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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<Preferences>
|
||||
) {
|
||||
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<Triple<String, ConsoleType, PackageItemType>>()
|
||||
|
||||
fun getPreferences(): Flow<SettingsPreferences> =
|
||||
dataStore.data.map { mapSettingsPreferences(it) }
|
||||
|
||||
suspend fun availableTsvs(): MutableList<Triple<String, ConsoleType, PackageItemType>> {
|
||||
val list = mutableListOf<Triple<String, ConsoleType, PackageItemType>>()
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.illusionware.npsbrowser.model
|
||||
|
||||
enum class ConsoleType {
|
||||
PSVITA,
|
||||
PS3,
|
||||
PSP,
|
||||
PSX,
|
||||
PSM,
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.illusionware.npsbrowser.model
|
||||
|
||||
enum class PackageItemType {
|
||||
GAME,
|
||||
DLC,
|
||||
THEME,
|
||||
}
|
||||
@@ -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)
|
||||
) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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<OnboardingPreferences> =
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Package> = emptyList(),
|
||||
val packages: ArrayList<PackageItem> = 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<PackageListUiState> = _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<PackageItem>()
|
||||
|
||||
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<String, ConsoleType, PackageItemType>, app: NPSApp
|
||||
): ArrayList<PackageItem> {
|
||||
val list = ArrayList<PackageItem>()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SettingsPreferences> =
|
||||
settingsPrefsRepo.getPreferences().stateIn(viewModelScope, SharingStarted.Lazily, SettingsPreferences(
|
||||
Theme.SYSTEM.ordinal,
|
||||
ItemLayout.LIST.ordinal,
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
true,
|
||||
))
|
||||
|
||||
fun availableTsvs(): MutableList<Pair<ConsoleType, PackageItemType>> {
|
||||
val list = mutableListOf<Pair<ConsoleType, PackageItemType>>()
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<vector android:height="256dp" android:viewportHeight="100"
|
||||
android:viewportWidth="100" android:width="256dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ee3e54" android:pathData="M13,27A2,2 0,1 0,13 31A2,2 0,1 0,13 27Z"/>
|
||||
<path android:fillColor="#f1bc19" android:pathData="M77,12A1,1 0,1 0,77 14A1,1 0,1 0,77 12Z"/>
|
||||
<path android:fillColor="#fce0a2" android:pathData="M50,13A37,37 0,1 0,50 87A37,37 0,1 0,50 13Z"/>
|
||||
<path android:fillColor="#f1bc19" android:pathData="M83,11A4,4 0,1 0,83 19A4,4 0,1 0,83 11Z"/>
|
||||
<path android:fillColor="#ee3e54" android:pathData="M87,22A2,2 0,1 0,87 26A2,2 0,1 0,87 22Z"/>
|
||||
<path android:fillColor="#fbcd59" android:pathData="M81,74A2,2 0,1 0,81 78,2 2,0 1,0 81,74zM15,59A4,4 0,1 0,15 67,4 4,0 1,0 15,59z"/>
|
||||
<path android:fillColor="#ee3e54" android:pathData="M25,85A2,2 0,1 0,25 89A2,2 0,1 0,25 85Z"/>
|
||||
<path android:fillColor="#fff" android:pathData="M18.5,51A2.5,2.5 0,1 0,18.5 56A2.5,2.5 0,1 0,18.5 51Z"/>
|
||||
<path android:fillColor="#f1bc19" android:pathData="M21,66A1,1 0,1 0,21 68A1,1 0,1 0,21 66Z"/>
|
||||
<path android:fillColor="#fff" android:pathData="M80,33A1,1 0,1 0,80 35A1,1 0,1 0,80 33Z"/>
|
||||
<path android:fillColor="#cdcbbd" android:pathData="M29.51,69.3c-0.5,0 -1.13,-0.14 -1.48,-1.27c-0.27,-0.87 -0.27,-1.95 -0.27,-2.65L27.7,37.8v-3.82c0,-1.69 1.32,-3.07 2.94,-3.07h17.65c1.66,0 3.01,1.46 3.01,3.24v1.46c0,1.24 0.97,2.26 2.16,2.26h16.09c0.42,0 0.76,0.38 0.76,0.86v23.57c0,0.33 0.01,0.75 0.03,1.21c0.05,1.7 0.13,4.46 -0.44,5.8H29.51z"/>
|
||||
<path android:fillColor="#472b29" android:pathData="M48.29,31.6c1.27,0 2.31,1.14 2.31,2.54v1.46c0,1.63 1.28,2.96 2.86,2.96l16.08,-0c0.01,0 0.07,0.05 0.07,0.16v23.57c0,0.34 0.01,0.76 0.03,1.23c0.04,1.29 0.11,3.74 -0.24,5.08h-39.88c-0.27,0 -0.57,0 -0.81,-0.79c-0.24,-0.77 -0.24,-1.78 -0.24,-2.44l0,-0.09L28.4,37.8v-3.82c0,-1.31 1.01,-2.37 2.24,-2.37H48.29M48.29,30.2H30.65c-2.01,0 -3.64,1.7 -3.64,3.77v3.82l0.05,27.48C27.05,66.7 27,70 29.51,70h40.8C71.37,68.36 71,64.1 71,62.28V38.72c0,-0.86 -0.65,-1.56 -1.46,-1.56H53.46c-0.81,0 -1.46,-0.7 -1.46,-1.56v-1.46C52,31.98 50.33,30.2 48.29,30.2L48.29,30.2z"/>
|
||||
<path android:fillColor="#472b29" android:pathData="M41.5,35h-11c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5h11c0.28,0 0.5,0.22 0.5,0.5S41.78,35 41.5,35z"/>
|
||||
<path android:fillColor="#472b29" android:pathData="M48.5,35h-4c-0.28,0 -0.5,-0.22 -0.5,-0.5s0.22,-0.5 0.5,-0.5h4c0.28,0 0.5,0.22 0.5,0.5S48.78,35 48.5,35z"/>
|
||||
<path android:fillColor="#fdfcee" android:pathData="M29.48,69.3c-0.61,0 -1.11,-0.52 -1.14,-1.18L34.03,42.7h41.92l-5.64,25.37c-0.01,0.05 -0.02,0.1 -0.02,0.15c0,0.29 -0.11,0.57 -0.3,0.77c-0.18,0.19 -0.43,0.3 -0.68,0.3H29.48z"/>
|
||||
<path android:fillColor="#472b29" android:pathData="M75.07,43.4l-5.45,24.52c-0.02,0.1 -0.03,0.2 -0.03,0.31c0,0.12 -0.04,0.23 -0.12,0.3c-0.05,0.05 -0.11,0.08 -0.17,0.08H29.48c-0.21,0 -0.39,-0.19 -0.43,-0.44L34.59,43.4H75.07M76.82,42H33.47l-5.83,26.05c0,1.08 0.82,1.95 1.84,1.95h39.83c0.93,0 1.68,-0.8 1.68,-1.78L76.82,42L76.82,42z"/>
|
||||
<path android:fillColor="#472b29" android:pathData="M50.5,55h-14c-0.15,0 -0.3,-0.07 -0.39,-0.19s-0.13,-0.28 -0.09,-0.43l1.5,-6C37.57,48.16 37.77,48 38,48h14c0.15,0 0.3,0.07 0.39,0.19s0.13,0.28 0.09,0.43l-1.5,6C50.93,54.84 50.73,55 50.5,55zM37.14,54h12.97l1.25,-5H38.39L37.14,54z"/>
|
||||
</vector>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2
-1
@@ -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" )
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user