preference saving

This commit is contained in:
2023-06-28 04:49:41 +03:00
parent 5c7c1d9036
commit 4d6fb0419e
3 changed files with 615 additions and 126 deletions
@@ -1,18 +1,51 @@
package com.illusionware.npsbrowser package com.illusionware.npsbrowser
import android.content.Context import android.content.Context
import android.net.Uri
import android.os.Environment
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
enum class ConsoleType {
PSVITA,
PS3,
PSP,
PSX,
PSM,
}
enum class DataType {
GAMES,
DLC,
THEMES,
}
class Preferences(private val context: Context) { class Preferences(private val context: Context) {
companion object { companion object {
private val Context.dataStore by preferencesDataStore(name = "settings") private val Context.dataStore by preferencesDataStore("settings")
private val APP_THEME = intPreferencesKey("app_theme") private val APP_THEME = intPreferencesKey("app_theme")
private val DATA_LAYOUT = intPreferencesKey("layout") private val DATA_LAYOUT = intPreferencesKey("layout")
private val SEEN_ONBOARDING = booleanPreferencesKey("seen_onboarding") 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 -> val theme = context.dataStore.data.map { prefs ->
@@ -27,6 +60,70 @@ class Preferences(private val context: Context) {
prefs[SEEN_ONBOARDING] ?: false 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) { suspend fun changeTheme(theme: Int) {
context.dataStore.edit { prefs -> context.dataStore.edit { prefs ->
prefs[APP_THEME] = theme prefs[APP_THEME] = theme
@@ -44,4 +141,87 @@ class Preferences(private val context: Context) {
prefs[SEEN_ONBOARDING] = true 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 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,7 +3,7 @@ package com.illusionware.npsbrowser.ui.pages
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.util.Log import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
@@ -68,15 +68,21 @@ import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.layoutId import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import com.illusionware.npsbrowser.ConsoleType
import com.illusionware.npsbrowser.DataType
import com.illusionware.npsbrowser.Preferences
import com.illusionware.npsbrowser.ui.components.NPSIconButton import com.illusionware.npsbrowser.ui.components.NPSIconButton
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -229,6 +235,9 @@ fun OnBoardingSetupTSVs(
incrementPage: () -> Unit = {}, incrementPage: () -> Unit = {},
skipToEnd: () -> Unit = {} skipToEnd: () -> Unit = {}
) { ) {
val prefs = Preferences(LocalContext.current)
val scope = rememberCoroutineScope()
Scaffold { padding -> Scaffold { padding ->
Column( Column(
Modifier Modifier
@@ -277,25 +286,52 @@ fun OnBoardingSetupTSVs(
verticalArrangement = Arrangement.spacedBy(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp),
) { ) {
TSVGroup(title = "PS Vita") { TSVGroup(title = "PS Vita") {
TSVButton("Games") TSVButton(
TSVButton("Themes") "Games",
TSVButton("DLC") onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.GAMES, it) }}
)
TSVButton(
"Themes",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.THEMES, it) }}
)
TSVButton(
"DLC",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSVITA, DataType.DLC, it) }}
)
} }
TSVGroup(title = "PSP") { TSVGroup(title = "PSP") {
TSVButton("Games") TSVButton(
TSVButton("DLC") "Games",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSP, DataType.GAMES, it) }}
)
TSVButton(
"DLC",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSP, DataType.DLC, it) }}
)
Box(modifier = Modifier.width(110.dp)) Box(modifier = Modifier.width(110.dp))
} }
TSVGroup(title = "PS3") { TSVGroup(title = "PS3") {
TSVButton("Games") TSVButton(
TSVButton("DLC") "Games",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PS3, DataType.GAMES, it) }}
)
TSVButton(
"DLC",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PS3, DataType.DLC, it) }}
)
Box(modifier = Modifier.width(110.dp)) Box(modifier = Modifier.width(110.dp))
} }
TSVGroup(title = "PSX") { TSVGroup(title = "PSX") {
TSVButton("Games") TSVButton(
"Games",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSX, DataType.GAMES, it) }}
)
} }
TSVGroup(title = "PSM") { TSVGroup(title = "PSM") {
TSVButton("Games") TSVButton(
"Games",
onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSM, DataType.GAMES, it) }}
)
} }
} }
} }
@@ -360,15 +396,14 @@ fun TSVGroup(title: String, content: @Composable RowScope.() -> Unit) {
} }
@Composable @Composable
fun TSVButton(title: String) { fun TSVButton(title: String, onFinish: (Uri) -> Unit) {
var hasSelected by rememberSaveable { mutableStateOf(false) } var hasSelected by rememberSaveable { mutableStateOf(false) }
val filePicker = rememberLauncherForActivityResult( val filePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(), contract = ActivityResultContracts.GetContent(),
) { fileUri -> ) { fileUri ->
if (fileUri != null) { if (fileUri != null) {
// TODO: store file URI in preferences onFinish(fileUri)
Log.d("TSVItem", "fileUri: $fileUri")
hasSelected = true hasSelected = true
} }
} }
@@ -442,8 +477,44 @@ fun OnBoardingSetupSettings(
navigateToHome: () -> Unit = {}, navigateToHome: () -> Unit = {},
) { ) {
val clipboard = LocalClipboardManager.current val clipboard = LocalClipboardManager.current
var hmacKey by rememberSaveable { mutableStateOf("") } val prefs = Preferences(LocalContext.current)
var pkg2zipArgs by rememberSaveable { mutableStateOf("") } 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())
}
}
}
val unpackDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
scope.launch {
prefs.setUnpackDir(dirUri.toString())
}
}
}
fun setHmacKey(key: String) {
if (key.isNotEmpty()) {
scope.launch {
prefs.setHMACKey(key)
}
}
}
Scaffold { padding -> Scaffold { padding ->
Column( Column(
@@ -485,14 +556,21 @@ fun OnBoardingSetupSettings(
) )
OutlinedTextField( OutlinedTextField(
value = hmacKey, value = hmacKey,
onValueChange = { hmacKey = it }, onValueChange = {
hmacKey = it
setHmacKey(it.text)
},
label = { Text(text = "HMAC Key") }, label = { Text(text = "HMAC Key") },
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
singleLine = true, singleLine = true,
trailingIcon = { trailingIcon = {
NPSIconButton( NPSIconButton(
tooltip = "Paste", tooltip = "Paste",
onClick = { hmacKey = clipboard.getText()?.text ?: "" } onClick = {
val text = clipboard.getText()?.text ?: ""
hmacKey = TextFieldValue(text, selection = TextRange(text.length))
setHmacKey(text)
}
) { ) {
Icon( Icon(
imageVector = Icons.Filled.ContentPaste, imageVector = Icons.Filled.ContentPaste,
@@ -508,36 +586,54 @@ fun OnBoardingSetupSettings(
text = "Choose your desired directory to download and unpack packages", text = "Choose your desired directory to download and unpack packages",
style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline), style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline),
) )
Toggleable( Column {
title = "Unpack in download directory", Toggleable(
) title = "Unpack in download directory",
Toggleable( checked = unpackInDownload,
title = "Delete package after unpacking", onCheckedChange = {
) scope.launch {
Box( prefs.setUnpackInDownload(it)
Modifier.fillMaxWidth().clickable { /*TODO*/ } }
) { },
Column(Modifier.padding(horizontal = 16.dp)) { )
Text( Toggleable(
text = "Download Directory", title = "Delete package after unpacking",
) checked = deleteAfterUnpack,
Text( onCheckedChange = {
text = "sdcard/Downloads", scope.launch {
color = MaterialTheme.colorScheme.outline, prefs.setDeleteAfterUnpack(it)
) }
},
)
Box(
Modifier
.fillMaxWidth()
.clickable { downloadDirPicker.launch(Uri.EMPTY) }
) {
Column(Modifier.padding(16.dp)) {
Text(
text = "Download Directory",
)
Text(
text = Uri.decode(downloadDir).ifEmpty { "None" },
color = MaterialTheme.colorScheme.outline,
)
}
} }
} Box(
Box( Modifier
Modifier.fillMaxWidth().clickable { /*TODO*/ } .fillMaxWidth()
) { .clickable { unpackDirPicker.launch(Uri.EMPTY) }
Column(Modifier.padding(horizontal = 16.dp)) { ) {
Text( Column(Modifier.padding(16.dp)) {
text = "Unpack Directory", Text(
) text = "Unpack Directory",
Text( )
text = "sdcard/Unpack", Text(
color = MaterialTheme.colorScheme.outline, text = Uri.decode(unpackDir).ifEmpty { "None" },
) color = MaterialTheme.colorScheme.outline,
)
}
} }
} }
} }
@@ -549,11 +645,24 @@ fun OnBoardingSetupSettings(
) )
Toggleable( Toggleable(
title = "Automatically decrypt downloaded content", title = "Automatically decrypt downloaded content",
checked = autoDecrypt,
onCheckedChange = {
scope.launch {
prefs.setPkg2zipAutoDecrypt(it)
}
},
) )
OutlinedTextField( OutlinedTextField(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
value = pkg2zipArgs, value = pkg2zipArgs,
onValueChange = { pkg2zipArgs = it }, // TODO: validate args are legal ?? onValueChange = {
pkg2zipArgs = it
scope.launch {
prefs.setPkg2zipParams(it)
}
}, // TODO: validate args are legal ??
label = { Text(text = "Pkg2zip args") }, label = { Text(text = "Pkg2zip args") },
placeholder = { Text(text = "-x {pkgFile} \"{zRifKey}\"") }, placeholder = { Text(text = "-x {pkgFile} \"{zRifKey}\"") },
) )
@@ -576,9 +685,13 @@ fun OnBoardingSetupSettings(
} }
@Composable @Composable
fun Toggleable(title: String) { fun Toggleable(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
Box( Box(
modifier = Modifier.clickable { /* TODO: */ }, modifier = Modifier
.clickable onCheckedChange@{
onCheckedChange(!checked)
}
.padding(vertical = 16.dp),
) { ) {
Row( Row(
modifier = Modifier.padding(horizontal = 16.dp), modifier = Modifier.padding(horizontal = 16.dp),
@@ -586,7 +699,7 @@ fun Toggleable(title: String) {
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
) { ) {
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f)) Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Switch(checked = true, onCheckedChange = { /* TODO: */ }) Switch(checked = checked, onCheckedChange = null)
} }
} }
} }
@@ -599,20 +712,25 @@ fun OnBoardingBottomButtons(
skipToEnd: () -> Unit = {}, skipToEnd: () -> Unit = {},
navigateToHome: () -> Unit = {}, navigateToHome: () -> Unit = {},
) { ) {
val prefs = Preferences(LocalContext.current)
val scope = rememberCoroutineScope()
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.fillMaxHeight(0.1f), .fillMaxHeight(0.1f),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = if (page != 2) Arrangement.SpaceBetween else Arrangement.End,
) { ) {
FilledTonalButton( if (page == 1) {
onClick = skipToEnd, FilledTonalButton(
colors = ButtonDefaults.filledTonalButtonColors( onClick = skipToEnd,
contentColor = MaterialTheme.colorScheme.primary colors = ButtonDefaults.filledTonalButtonColors(
), contentColor = MaterialTheme.colorScheme.primary
contentPadding = PaddingValues(horizontal = 18.dp) ),
) { contentPadding = PaddingValues(horizontal = 18.dp)
Text(text = "Skip") ) {
Text(text = "Skip")
}
} }
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
if (page != 1) { if (page != 1) {
@@ -621,7 +739,15 @@ fun OnBoardingBottomButtons(
} }
} }
if (page == 2) { if (page == 2) {
Button(onClick = navigateToHome, contentPadding = PaddingValues(horizontal = 18.dp)) { Button(
onClick = {
navigateToHome()
scope.launch {
prefs.finishOnboarding()
}
},
contentPadding = PaddingValues(horizontal = 18.dp)
) {
Text(text = "Finish") Text(text = "Finish")
} }
} else { } else {
@@ -1,5 +1,8 @@
package com.illusionware.npsbrowser.ui.pages package com.illusionware.npsbrowser.ui.pages
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@@ -15,34 +18,36 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.ContentPaste
import androidx.compose.material.icons.outlined.FileDownload import androidx.compose.material.icons.outlined.FileDownload
import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Key
import androidx.compose.material.icons.outlined.Palette import androidx.compose.material.icons.outlined.Palette
import androidx.compose.material.icons.outlined.Unarchive import androidx.compose.material.icons.outlined.Unarchive
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.Fragment
import com.illusionware.npsbrowser.Preferences import com.illusionware.npsbrowser.Preferences
import com.illusionware.npsbrowser.R import com.illusionware.npsbrowser.R
import com.illusionware.npsbrowser.ui.components.NPSAlertDialog import com.illusionware.npsbrowser.ui.components.NPSAlertDialog
@@ -54,6 +59,12 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
val Themes = hashMapOf(
0 to "Light",
1 to "Dark",
2 to "Use System Theme"
)
@Composable @Composable
@Preview @Preview
fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) -> Unit = {}) { fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) -> Unit = {}) {
@@ -79,46 +90,18 @@ fun SettingsScreen(navigationGoBack: () -> Unit = {}, onThemeChange: (v: Int) ->
) { ) {
Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) { Column(verticalArrangement = Arrangement.spacedBy(20.dp), modifier = Modifier.padding(vertical = 16.dp)) {
Appearance(onThemeChange) Appearance(onThemeChange)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { Updates()
Header("Updates") Downloads()
Spacer(modifier = Modifier.size(2.dp)) Pkg2Zip()
DialogSetting(title = "HMAC Key", value = "None", icon = Icons.Outlined.Key)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header("Downloads")
Spacer(modifier = Modifier.size(2.dp))
ToggleSetting(title = "Unpack in Download Directory")
DialogSetting(title = "Download Directory", value = "sdcard/Downloads", icon = Icons.Outlined.FileDownload)
DialogSetting(title = "Unpack Directory", value = "sdcard/unpack", icon = Icons.Outlined.Unarchive)
}
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header("Pkg2zip")
Spacer(modifier = Modifier.size(2.dp))
ToggleSetting(title = "Automatically decrypt downloaded content")
DialogSetting(title = "Decryption params", value = "-x {pkgFile} \"{zRifKey}\"", icon = painterResource(
id = R.drawable.lock_open
))
}
} }
} }
} }
} }
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header(title)
Spacer(modifier = Modifier.size(2.dp))
content()
}
}
val Themes = hashMapOf(
0 to "Light",
1 to "Dark",
2 to "Use System Theme"
)
@Composable @Composable
fun Appearance(onThemeChange: (v: Int) -> Unit) { fun Appearance(onThemeChange: (v: Int) -> Unit) {
@@ -143,6 +126,197 @@ fun Appearance(onThemeChange: (v: Int) -> Unit) {
} }
} }
@Composable
fun Updates() {
val prefs = Preferences(LocalContext.current)
val scope = rememberCoroutineScope()
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
}
SettingGroup(title = "Updates") {
DialogSetting(
title = "HMAC Key",
value = hmacKey.ifEmpty { "None" },
icon = Icons.Outlined.Key,
onClick = { hmacOpenDialog = true }
)
}
if (hmacOpenDialog) {
NPSAlertDialog(
onDismiss = { hmacOpenDialog = false },
title = "HMAC Key",
buttons = {
TextButton(onClick = { hmacOpenDialog = false }) {
Text(text = "Cancel")
}
TextButton(onClick = {
scope.launch {
prefs.setHMACKey(dialogHmacKey)
}
hmacOpenDialog = false
}) {
Text(text = "Ok")
}
}
) {
OutlinedTextField(
value = dialogHmacKey,
onValueChange = { dialogHmacKey = it },
label = { Text(text = "HMAC Key") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
trailingIcon = {
NPSIconButton(
tooltip = "Paste",
onClick = { dialogHmacKey = clipboard.getText()?.text ?: "" }
) {
Icon(
imageVector = Icons.Filled.ContentPaste,
contentDescription = "Paste",
)
}
}
)
}
}
}
@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
val downloadDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
scope.launch {
prefs.setDownloadDir(dirUri.toString())
}
}
}
val unpackDirPicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocumentTree(),
) { dirUri ->
if (dirUri != null) {
scope.launch {
prefs.setUnpackDir(dirUri.toString())
}
}
}
SettingGroup(title = "Downloads") {
ToggleSetting(
title = "Unpack in Download Directory",
checked = unpackInDownload,
onClick = {
scope.launch {
prefs.setUnpackInDownload(it)
}
}
)
ToggleSetting(
title = "Delete package after unpacking",
checked = deleteAfterUnpack,
onClick = {
scope.launch {
prefs.setDeleteAfterUnpack(it)
}
}
)
DialogSetting(
title = "Download Directory",
value = downloadDir,
icon = Icons.Outlined.FileDownload,
onClick = {
downloadDirPicker.launch(Uri.EMPTY)
}
)
DialogSetting(
title = "Unpack Directory",
value = unpackDir,
icon = Icons.Outlined.Unarchive,
onClick = {
unpackDirPicker.launch(Uri.EMPTY)
}
)
}
}
@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
var pkg2zipOpenDialog by remember { mutableStateOf(false) }
var dialogPkg2zipParams by remember { mutableStateOf("") }
SideEffect {
dialogPkg2zipParams = pkg2zipParams
}
SettingGroup(title = "Pkg2Zip") {
ToggleSetting(
title = "Automatically decrypt downloaded content",
checked = autoDecrypt,
onClick = {
scope.launch {
prefs.setPkg2zipAutoDecrypt(it)
}
}
)
DialogSetting(
title = "Decryption params",
value = pkg2zipParams.ifEmpty { "None" },
icon = painterResource(
id = R.drawable.lock_open
),
onClick = { pkg2zipOpenDialog = true }
)
}
if (pkg2zipOpenDialog) {
NPSAlertDialog(
onDismiss = { pkg2zipOpenDialog = false },
title = "PKG2ZIP Args",
buttons = {
TextButton(onClick = { pkg2zipOpenDialog = false }) {
Text(text = "Cancel")
}
TextButton(onClick = {
scope.launch {
prefs.setPkg2zipParams(dialogPkg2zipParams)
}
pkg2zipOpenDialog = false
}) {
Text(text = "Ok")
}
}
) {
OutlinedTextField(
value = dialogPkg2zipParams,
onValueChange = { dialogPkg2zipParams = it },
label = { Text(text = "PKG2ZIP Args") },
modifier = Modifier.fillMaxWidth(),
singleLine = true,
)
}
}
}
@Composable @Composable
fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) { fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) {
val prefs = Preferences(LocalContext.current) val prefs = Preferences(LocalContext.current)
@@ -187,6 +361,15 @@ fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) {
} }
} }
@Composable
fun SettingGroup(title: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
Header(title)
Spacer(modifier = Modifier.size(2.dp))
content()
}
}
@Composable @Composable
fun Header(title: String) { fun Header(title: String) {
Row(Modifier.padding(start = 56.dp)) { Row(Modifier.padding(start = 56.dp)) {
@@ -195,29 +378,7 @@ fun Header(title: String) {
} }
@Composable @Composable
// TODO: remove the default value for onClick fun DialogSetting(title: String, value: String, icon: Painter, onClick: () -> Unit) {
fun DialogSetting(title: String, value: String, icon: Painter, onClick: () -> Unit = {}) {
Box(
Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(painter = icon, contentDescription = "TODO:", modifier = Modifier.width(56.dp))
Column {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight)
}
}
}
}
@Composable
// TODO: remove the default value for onClick
fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -> Unit = {}) {
Box( Box(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@@ -227,7 +388,7 @@ fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp) modifier = Modifier.padding(vertical = 8.dp)
) { ) {
Icon(imageVector = icon, contentDescription = "TODO:", modifier = Modifier.width(56.dp)) Icon(painter = icon, contentDescription = null, modifier = Modifier.width(56.dp))
Column { Column {
Text(text = title, color = MaterialTheme.colorScheme.onBackground) Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight) Text(text = value, color = ColorSecondaryLight)
@@ -237,9 +398,31 @@ fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -
} }
@Composable @Composable
fun ToggleSetting(title: String, icon: ImageVector? = null) { fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -> Unit) {
Box( Box(
modifier = Modifier.clickable { /* TODO: */ }, Modifier
.fillMaxWidth()
.clickable { onClick() }
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(vertical = 8.dp)
) {
Icon(imageVector = icon, contentDescription = null, modifier = Modifier.width(56.dp))
Column {
Text(text = title, color = MaterialTheme.colorScheme.onBackground)
Text(text = value, color = ColorSecondaryLight)
}
}
}
}
@Composable
fun ToggleSetting(title: String, icon: ImageVector? = null, checked: Boolean, onClick: (Boolean) -> Unit) {
Box(
modifier = Modifier.clickable onClick@{
onClick(!checked)
},
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -247,12 +430,12 @@ fun ToggleSetting(title: String, icon: ImageVector? = null) {
modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp), modifier = Modifier.padding(top = 8.dp, end = 24.dp, bottom = 8.dp),
) { ) {
if (icon != null) { if (icon != null) {
Icon(imageVector = icon, contentDescription = "TODO:", Modifier.width(56.dp)) Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp))
} else { } else {
Box(modifier = Modifier.width(56.dp)) Box(modifier = Modifier.width(56.dp))
} }
Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f)) Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f))
Switch(checked = true, onCheckedChange = { /* TODO: */ }) Switch(checked = checked, onCheckedChange = null)
} }
} }
} }