feat: initial settings implementation

This commit is contained in:
Michael 2026-01-31 20:03:11 +03:00
parent 01fabf52d2
commit 3b039d8dfd
No known key found for this signature in database
7 changed files with 330 additions and 1 deletions

View file

@ -27,6 +27,7 @@ import dev.mi6e4ka.openstore.ui.screen.details.DetailsScreen
import dev.mi6e4ka.openstore.ui.screen.results.ResultsScreen
import dev.mi6e4ka.openstore.ui.screen.reviews.ReviewsScreen
import dev.mi6e4ka.openstore.ui.screen.search.SearchScreen
import dev.mi6e4ka.openstore.ui.screen.settings.SettingsScreen
import dev.mi6e4ka.openstore.ui.theme.AppTheme
class MainActivity : ComponentActivity() {
@ -106,6 +107,9 @@ class MainActivity : ComponentActivity() {
val packageName = it.arguments?.getString("packageName") ?: ""
ReviewsScreen(packageName = packageName, navController = navController)
}
composable("settings") {
SettingsScreen(navController = navController)
}
}
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -22,11 +23,15 @@ import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
@ -54,6 +59,7 @@ import androidx.navigation.NavController
import androidx.navigation.compose.rememberNavController
import dev.mi6e4ka.openstore.BuildConfig
import dev.mi6e4ka.openstore.R
import dev.mi6e4ka.openstore.ui.screen.results.Platform
import java.net.URLEncoder
@OptIn(ExperimentalMaterial3Api::class)
@ -79,7 +85,14 @@ fun SearchScreen(navController: NavController = rememberNavController()) {
Scaffold(
modifier = Modifier.fillMaxSize(),
topBar = { TopAppBar(title = {Text(stringResource(R.string.app_name))}) },
topBar = { TopAppBar(
title = {Text(stringResource(R.string.app_name))},
actions = {
IconButton({navController.navigate("settings")}) {
Icon(painterResource(R.drawable.ic_settings_24px), "settings")
}
}
) },
) { innerPadding ->
Column(
modifier = Modifier

View file

@ -0,0 +1,281 @@
package dev.mi6e4ka.openstore.ui.screen.settings
import android.content.Context
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import dev.mi6e4ka.openstore.R
import androidx.core.content.edit
data class SettingsSection(
val id: String,
val title: String,
val description: String?,
val icon: Int,
val type: ParamType,
val defaultVal: String,
val select: List<SettingsSectionSelect>? = null
)
data class SettingsSectionSelect(
val title: String,
val value: String
)
enum class ParamType {
SWITCH,
SELECT,
MULTI_SELECT
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
navController: NavController
) {
val sections = listOf(
SettingsSection(
id = "installer_type",
title = "Установщик",
description = "Если есть проблемы при установке приложений",
icon = R.drawable.apk_install_24px,
type = ParamType.SELECT,
defaultVal = "session",
select = listOf(
SettingsSectionSelect(
title = "Session",
value = "session"
),
SettingsSectionSelect(
title = "Intent",
value = "intent"
)
)
),
SettingsSection(
id = "updates_sources",
title = "Проверка обновлений",
description = "Для приложений из каких источников проверять обновления",
icon = R.drawable.ic_update_24px,
type = ParamType.MULTI_SELECT,
defaultVal = "",
select = listOf(
SettingsSectionSelect(
title = "OpenStore",
value = "dev.mi6e4ka.dev"
),
SettingsSectionSelect(
title = "RuStore",
value = "ru.vk.store"
),
SettingsSectionSelect(
title = "Google Play",
value = "com.android.vending"
)
)
),
SettingsSection(
id = "updates_check_all",
title = "Проверять обновления для всех не системных приложений",
description = "RuStore получает полный список установленных приложений, не безопасно",
icon = R.drawable.ic_arrow_circle_up_24px,
type = ParamType.SWITCH,
defaultVal = ""
)
)
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.settings_title)) },
navigationIcon = {
IconButton(
{navController.popBackStack()}
) {
Icon(painterResource(R.drawable.ic_arrow_back_24px), "back")
}
}
)
}
) {
innerPadding ->
LazyColumn(
modifier = Modifier.fillMaxSize().padding(innerPadding)
) {
items(sections) { item ->
SettingsCard(
item = item
)
}
}
}
}
@Composable
fun SettingsCard(
item: SettingsSection
) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
var showDialog by remember { mutableStateOf(false) }
var booleanSelect by remember { mutableStateOf(
if (item.type == ParamType.SWITCH) prefs.getBoolean(item.id, false) else false
)}
ElevatedCard (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
) {
Row(
Modifier
.clickable{
if (item.type != ParamType.SWITCH) {
showDialog = true
} else {
prefs.edit { putBoolean(item.id, !booleanSelect) }
booleanSelect = !booleanSelect
}
}
.padding(15.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(15.dp)
) {
Icon(
painterResource(item.icon),
"icon",
modifier = Modifier.size(32.dp)
)
Column(
modifier = Modifier.weight(1f)
) {
Text(item.title, fontWeight = FontWeight.Medium)
Text(item.description?:"", color = Color.Gray, fontWeight = FontWeight.Normal)
}
if (item.type == ParamType.SWITCH) {
Switch(
booleanSelect,
{
v -> prefs.edit { putBoolean(item.id, v) }; booleanSelect = v
}
)
}
}
if (showDialog) {
val selected = remember { mutableStateListOf<SettingsSectionSelect>() }
if (item.type == ParamType.MULTI_SELECT) {
selected.apply { addAll(prefs.getString(item.id, item.defaultVal)!!.split(";").mapNotNull {
v ->
item.select?.firstOrNull { it.value == v }
})
}
}
var selectedVal by remember { mutableStateOf(prefs.getString(item.id, item.defaultVal)) }
AlertDialog(
onDismissRequest = { showDialog = false },
title = {
Text(item.title)
},
text = {
LazyColumn {
items(item.select?:listOf()) { entry ->
Row(
Modifier
.fillMaxWidth()
.clickable {
if (item.type == ParamType.MULTI_SELECT) {
if (entry in selected) {
selected.remove(entry)
} else {
selected.add(entry)
}
} else {
selectedVal = entry.value
}
}
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (item.type == ParamType.SELECT) {
RadioButton(
selected = selectedVal == entry.value,
onClick = { selectedVal = entry.value }
)
} else {
Checkbox(
checked = entry in selected,
onCheckedChange = {
if (it) selected.add(entry)
else selected.remove(entry)
}
)
}
Text(
entry.title,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
},
confirmButton = {
TextButton(onClick = {
if (item.type == ParamType.MULTI_SELECT) {
prefs.edit { putString(item.id, selected.joinToString(";") { it.value }) }
} else {
prefs.edit { putString(item.id, selectedVal) }
}
showDialog = false
}) {
Text("Сохранить")
}
}
)
}
}
}

View file

@ -0,0 +1,19 @@
package dev.mi6e4ka.openstore.ui.screen.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
class DetailsViewModel() : ViewModel() {
}
class SettingsViewModelFactory() : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(DetailsViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return DetailsViewModel() as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}

View file

@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM440,800L519,800L533,694Q564,686 590.5,670.5Q617,655 639,633L738,674L777,606L691,541Q696,527 698,511.5Q700,496 700,480Q700,464 698,448.5Q696,433 691,419L777,354L738,286L639,328Q617,305 590.5,289.5Q564,274 533,266L520,160L441,160L427,266Q396,274 369.5,289.5Q343,305 321,327L222,286L183,354L269,418Q264,433 262,448Q260,463 260,480Q260,496 262,511Q264,526 269,541L183,606L222,674L321,632Q343,655 369.5,670.5Q396,686 427,694L440,800ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620ZM480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480Q480,480 480,480L480,480L480,480L480,480Q480,480 480,480Q480,480 480,480L480,480Z"/>
</vector>

View file

@ -33,4 +33,5 @@
<string name="negative_first">Negative</string>
<string name="download_has_started">Download has started</string>
<string name="not_found_error">Nothing found</string>
<string name="settings_title">Settings</string>
</resources>

View file

@ -34,4 +34,5 @@
<string name="negative_first">Негативные</string>
<string name="download_has_started">Загрузка началась</string>
<string name="not_found_error">Ничего не найдено</string>
<string name="settings_title">Настройки</string>
</resources>