From 4d6fb0419e45b61bc8e0062b996f42fc7ddb1d4c Mon Sep 17 00:00:00 2001 From: IllusionMan1212 Date: Wed, 28 Jun 2023 04:49:41 +0300 Subject: [PATCH] preference saving --- .../illusionware/npsbrowser/Preferences.kt | 182 +++++++++- .../npsbrowser/ui/pages/Onboarding.kt | 248 ++++++++++---- .../npsbrowser/ui/pages/Settings.kt | 311 ++++++++++++++---- 3 files changed, 615 insertions(+), 126 deletions(-) diff --git a/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt b/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt index c4b93da..1d6107e 100644 --- a/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt +++ b/app/src/main/java/com/illusionware/npsbrowser/Preferences.kt @@ -1,18 +1,51 @@ 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.map +enum class ConsoleType { + PSVITA, + PS3, + PSP, + PSX, + PSM, +} + +enum class DataType { + GAMES, + DLC, + THEMES, +} + class Preferences(private val context: Context) { 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 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 -> @@ -27,6 +60,70 @@ class Preferences(private val context: Context) { 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 @@ -44,4 +141,87 @@ class Preferences(private val context: Context) { 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 + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt index c883253..60db7e5 100644 --- a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt +++ b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Onboarding.kt @@ -3,7 +3,7 @@ package com.illusionware.npsbrowser.ui.pages import android.annotation.SuppressLint import android.app.Activity import android.content.pm.ActivityInfo -import android.util.Log +import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility @@ -68,15 +68,21 @@ 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 import androidx.compose.ui.layout.Layout 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.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 com.illusionware.npsbrowser.ui.components.NPSIconButton import kotlinx.coroutines.launch @@ -229,6 +235,9 @@ fun OnBoardingSetupTSVs( incrementPage: () -> Unit = {}, skipToEnd: () -> Unit = {} ) { + val prefs = Preferences(LocalContext.current) + val scope = rememberCoroutineScope() + Scaffold { padding -> Column( Modifier @@ -277,25 +286,52 @@ fun OnBoardingSetupTSVs( verticalArrangement = Arrangement.spacedBy(16.dp), ) { TSVGroup(title = "PS Vita") { - TSVButton("Games") - TSVButton("Themes") - TSVButton("DLC") + TSVButton( + "Games", + 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") { - TSVButton("Games") - TSVButton("DLC") + TSVButton( + "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)) } TSVGroup(title = "PS3") { - TSVButton("Games") - TSVButton("DLC") + TSVButton( + "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)) } TSVGroup(title = "PSX") { - TSVButton("Games") + TSVButton( + "Games", + onFinish = { scope.launch { prefs.setTSVFile(ConsoleType.PSX, DataType.GAMES, it) }} + ) } 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 -fun TSVButton(title: String) { +fun TSVButton(title: String, onFinish: (Uri) -> Unit) { var hasSelected by rememberSaveable { mutableStateOf(false) } val filePicker = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent(), ) { fileUri -> if (fileUri != null) { - // TODO: store file URI in preferences - Log.d("TSVItem", "fileUri: $fileUri") + onFinish(fileUri) hasSelected = true } } @@ -442,8 +477,44 @@ fun OnBoardingSetupSettings( navigateToHome: () -> Unit = {}, ) { val clipboard = LocalClipboardManager.current - var hmacKey by rememberSaveable { mutableStateOf("") } - var pkg2zipArgs by rememberSaveable { mutableStateOf("") } + 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()) + } + } + } + + 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 -> Column( @@ -485,14 +556,21 @@ fun OnBoardingSetupSettings( ) OutlinedTextField( value = hmacKey, - onValueChange = { hmacKey = it }, + onValueChange = { + hmacKey = it + setHmacKey(it.text) + }, label = { Text(text = "HMAC Key") }, modifier = Modifier.fillMaxWidth(), singleLine = true, trailingIcon = { NPSIconButton( tooltip = "Paste", - onClick = { hmacKey = clipboard.getText()?.text ?: "" } + onClick = { + val text = clipboard.getText()?.text ?: "" + hmacKey = TextFieldValue(text, selection = TextRange(text.length)) + setHmacKey(text) + } ) { Icon( imageVector = Icons.Filled.ContentPaste, @@ -508,36 +586,54 @@ fun OnBoardingSetupSettings( text = "Choose your desired directory to download and unpack packages", style = Typography.bodyMedium.copy(color = MaterialTheme.colorScheme.outline), ) - Toggleable( - title = "Unpack in download directory", - ) - Toggleable( - title = "Delete package after unpacking", - ) - Box( - Modifier.fillMaxWidth().clickable { /*TODO*/ } - ) { - Column(Modifier.padding(horizontal = 16.dp)) { - Text( - text = "Download Directory", - ) - Text( - text = "sdcard/Downloads", - color = MaterialTheme.colorScheme.outline, - ) + Column { + Toggleable( + title = "Unpack in download directory", + checked = unpackInDownload, + onCheckedChange = { + scope.launch { + prefs.setUnpackInDownload(it) + } + }, + ) + Toggleable( + title = "Delete package after unpacking", + checked = deleteAfterUnpack, + onCheckedChange = { + scope.launch { + 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( - Modifier.fillMaxWidth().clickable { /*TODO*/ } - ) { - Column(Modifier.padding(horizontal = 16.dp)) { - Text( - text = "Unpack Directory", - ) - Text( - text = "sdcard/Unpack", - color = MaterialTheme.colorScheme.outline, - ) + Box( + Modifier + .fillMaxWidth() + .clickable { unpackDirPicker.launch(Uri.EMPTY) } + ) { + Column(Modifier.padding(16.dp)) { + Text( + text = "Unpack Directory", + ) + Text( + text = Uri.decode(unpackDir).ifEmpty { "None" }, + color = MaterialTheme.colorScheme.outline, + ) + } } } } @@ -549,11 +645,24 @@ fun OnBoardingSetupSettings( ) Toggleable( title = "Automatically decrypt downloaded content", + checked = autoDecrypt, + onCheckedChange = { + scope.launch { + prefs.setPkg2zipAutoDecrypt(it) + } + }, ) OutlinedTextField( - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), 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") }, placeholder = { Text(text = "-x {pkgFile} \"{zRifKey}\"") }, ) @@ -576,9 +685,13 @@ fun OnBoardingSetupSettings( } @Composable -fun Toggleable(title: String) { +fun Toggleable(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { Box( - modifier = Modifier.clickable { /* TODO: */ }, + modifier = Modifier + .clickable onCheckedChange@{ + onCheckedChange(!checked) + } + .padding(vertical = 16.dp), ) { Row( modifier = Modifier.padding(horizontal = 16.dp), @@ -586,7 +699,7 @@ fun Toggleable(title: String) { horizontalArrangement = Arrangement.SpaceBetween, ) { 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 = {}, navigateToHome: () -> Unit = {}, ) { + val prefs = Preferences(LocalContext.current) + val scope = rememberCoroutineScope() + Row( modifier = Modifier .fillMaxWidth() .fillMaxHeight(0.1f), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = if (page != 2) Arrangement.SpaceBetween else Arrangement.End, ) { - FilledTonalButton( - onClick = skipToEnd, - colors = ButtonDefaults.filledTonalButtonColors( - contentColor = MaterialTheme.colorScheme.primary - ), - contentPadding = PaddingValues(horizontal = 18.dp) - ) { - Text(text = "Skip") + if (page == 1) { + FilledTonalButton( + onClick = skipToEnd, + colors = ButtonDefaults.filledTonalButtonColors( + contentColor = MaterialTheme.colorScheme.primary + ), + contentPadding = PaddingValues(horizontal = 18.dp) + ) { + Text(text = "Skip") + } } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (page != 1) { @@ -621,7 +739,15 @@ fun OnBoardingBottomButtons( } } 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") } } else { diff --git a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt index 3c4fb87..4c6d7b4 100644 --- a/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt +++ b/app/src/main/java/com/illusionware/npsbrowser/ui/pages/Settings.kt @@ -1,5 +1,8 @@ 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.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -15,34 +18,36 @@ import androidx.compose.foundation.rememberScrollState 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.FileDownload import androidx.compose.material.icons.outlined.Key import androidx.compose.material.icons.outlined.Palette 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.MaterialTheme +import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Switch 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 import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.fragment.app.Fragment import com.illusionware.npsbrowser.Preferences import com.illusionware.npsbrowser.R import com.illusionware.npsbrowser.ui.components.NPSAlertDialog @@ -54,6 +59,12 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +val Themes = hashMapOf( + 0 to "Light", + 1 to "Dark", + 2 to "Use System Theme" +) + @Composable @Preview 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)) { Appearance(onThemeChange) - Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { - Header("Updates") - Spacer(modifier = Modifier.size(2.dp)) - 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 - )) - } + Updates() + Downloads() + Pkg2Zip() } } } } -@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 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 fun ThemeDialog(onDismiss: () -> Unit, onThemeChange: (v: Int) -> Unit) { 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 fun Header(title: String) { Row(Modifier.padding(start = 56.dp)) { @@ -195,29 +378,7 @@ fun Header(title: String) { } @Composable -// TODO: remove the default value for onClick -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 = {}) { +fun DialogSetting(title: String, value: String, icon: Painter, onClick: () -> Unit) { Box( Modifier .fillMaxWidth() @@ -227,7 +388,7 @@ fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () - verticalAlignment = Alignment.CenterVertically, 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 { Text(text = title, color = MaterialTheme.colorScheme.onBackground) Text(text = value, color = ColorSecondaryLight) @@ -237,9 +398,31 @@ fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () - } @Composable -fun ToggleSetting(title: String, icon: ImageVector? = null) { +fun DialogSetting(title: String, value: String, icon: ImageVector, onClick: () -> Unit) { 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( 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), ) { if (icon != null) { - Icon(imageVector = icon, contentDescription = "TODO:", Modifier.width(56.dp)) + Icon(imageVector = icon, contentDescription = null, Modifier.width(56.dp)) } else { Box(modifier = Modifier.width(56.dp)) } Text(text = title, color = MaterialTheme.colorScheme.onBackground, modifier = Modifier.weight(1.0f)) - Switch(checked = true, onCheckedChange = { /* TODO: */ }) + Switch(checked = checked, onCheckedChange = null) } } }