45 Commits
2_5 ... 2.8.0

Author SHA1 Message Date
CherretGit
fd06b6c75b navigationBarsPadding in repo screen 2025-08-28 02:35:33 +07:00
CherretGit
ce91b578b8 bump version 2025-08-28 02:31:53 +07:00
CherretGit
68b25d8efa edit button in settings 2025-08-28 02:26:50 +07:00
CherretGit
b8d26a1a57 handle file download error 2025-08-28 02:18:38 +07:00
CherretGit
e6ffe78eb1 fix byedpi blacklist (again) 2025-08-28 01:08:27 +07:00
CherretGit
d754abf794 move card status update to ViewModel 2025-08-28 01:03:32 +07:00
CherretGit
2bdc69b1ba move card status update to ViewModel 2025-08-28 01:03:28 +07:00
CherretGit
902b2939cd remove spacer 2025-08-27 19:11:16 +07:00
CherretGit
c70752f8c5 Fix UI errors 2025-08-27 19:05:17 +07:00
CherretGit
a385e498ea fix repo 2025-08-27 17:43:33 +07:00
CherretGit
3f6086c11f add repository error handling, fix Byedpi blacklist, possible UI error fix 2025-08-27 04:18:49 +07:00
white
f7f8d3e3b9 fix list repo crash 2025-08-22 00:17:25 +03:00
CherretGit
6095a3d485 Merge remote-tracking branch 'origin/main' 2025-08-21 20:17:43 +07:00
CherretGit
8771c8fd1b Fix crashes 2025-08-21 20:16:28 +07:00
CherretGit
51d774b6aa Revert "some refactoring + creating config on every change"
This reverts commit ef04471a
2025-08-21 19:43:27 +07:00
CherretGit
879bf98ed1 Update update.json 2025-08-21 18:11:39 +07:00
CherretGit
cb283b7d62 Update changelog.md 2025-08-21 18:11:22 +07:00
CherretGit
e988bfdd2e and one more 2025-08-21 16:44:03 +07:00
CherretGit
49f61892da one more fix 2025-08-21 16:35:56 +07:00
white
2aa831baa5 way more refactoring 2025-08-21 12:22:32 +03:00
white
ef04471a05 some refactoring + creating config on every change 2025-08-21 12:10:14 +03:00
CherretGit
baec0f3838 fix 2025-08-21 15:30:15 +07:00
white
8101c44dc1 update version, change repository urls 2025-08-21 11:09:21 +03:00
white
aef5b962b5 rename parametres 2025-08-21 10:53:16 +03:00
white
28dca0716a rename parametres 2025-08-21 10:52:07 +03:00
white
0807eebeb2 fix non-root exclude switches state saving 2025-08-20 22:50:23 +03:00
CherretGit
b17a0dc4cb implement hostlist excluded 2025-08-21 01:57:22 +07:00
white
1ada6304a4 add black/white hostlist division in ui and config 2025-08-20 18:09:41 +03:00
CherretGit
e16e8e3bbe update dependencies 2025-08-20 16:34:39 +07:00
white
1e0368e467 Merge remote-tracking branch 'origin/main' 2025-08-16 15:49:24 +03:00
white
4dcf90dcfc draft of new ui 2025-08-16 15:49:03 +03:00
CherretGit
94ff869bd5 update some dependencies 2025-08-05 21:11:23 +07:00
CherretGit
435f548698 fix lags 2025-07-26 03:37:02 +07:00
CherretGit
660c3b509a fix lags 2025-07-26 03:35:28 +07:00
CherretGit
6e8960a707 links 2025-07-26 03:11:46 +07:00
CherretGit
c5f836d65c Update update.json 2025-07-17 21:32:52 +07:00
CherretGit
3f46872fec Update changelog.md 2025-07-17 21:32:05 +07:00
CherretGit
8192490e93 fix new feature 2025-07-17 20:56:20 +07:00
egor-white
53510746a6 refreshApplications() is now public 2025-07-17 11:49:43 +03:00
egor-white
0a71c725bb Merge branch 'main' of github.com:CherretGit/zaprett-app 2025-07-17 11:37:59 +03:00
egor-white
2d95cee5a6 add system apps filter 2025-07-17 11:37:43 +03:00
CherretGit
d1904901b1 update submodule(again) 2025-07-17 13:23:45 +07:00
CherretGit
fc9f12d102 fix 2025-07-17 13:17:15 +07:00
egor-white
dc53b433f6 Update update.json 2025-07-16 23:59:05 +03:00
egor-white
8331a44d15 Update changelog.md 2025-07-16 23:58:35 +03:00
32 changed files with 822 additions and 316 deletions

View File

@@ -9,14 +9,14 @@ plugins {
android {
namespace = "com.cherret.zaprett"
compileSdk = 35
compileSdk = 36
defaultConfig {
applicationId = "com.cherret.zaprett"
minSdk = 29
targetSdk = 35
versionCode = 17
versionName = "2.5"
versionCode = 20
versionName = "2.8"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
@@ -76,19 +76,19 @@ tasks.preBuild {
}
dependencies {
implementation("androidx.compose.material3:material3:1.3.1")
implementation("androidx.compose.material3:material3-window-size-class:1.3.1")
implementation("androidx.compose.material3:material3-adaptive-navigation-suite:1.4.0-alpha10")
implementation("androidx.navigation:navigation-compose:2.8.9")
implementation("androidx.compose.material:material-icons-extended:1.7.8")
implementation ("com.github.topjohnwu.libsu:core:6.0.0")
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.0")
implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
implementation("com.google.firebase:firebase-analytics")
implementation("com.google.firebase:firebase-crashlytics")
implementation("androidx.fragment:fragment-compose:1.8.8")
implementation("io.coil-kt.coil3:coil-compose:3.2.0")
implementation(libs.compose.material3)
implementation(libs.compose.material3.window.size)
implementation(libs.compose.material3.adaptive.nav)
implementation(libs.navigation.compose)
implementation(libs.compose.icons)
implementation(libs.libsu.core)
implementation(libs.okhttp)
implementation(libs.serialization.json)
implementation(platform(libs.firebase.bom))
implementation(libs.firebase.analytics)
implementation(libs.firebase.crashlytics)
implementation(libs.fragment.compose)
implementation(libs.coil.compose)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)

View File

@@ -189,7 +189,7 @@ class MainActivity : ComponentActivity() {
contentDescription = stringResource(id = topLevelRoute.nameResId)
)
},
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) }, alwaysShowLabel = false,
selected = currentDestination?.route == topLevelRoute.route,
onClick = {
navController.navigate(topLevelRoute.route) {

View File

@@ -13,8 +13,12 @@ import android.util.Log
import androidx.core.app.NotificationCompat
import com.cherret.zaprett.MainActivity
import com.cherret.zaprett.R
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.utils.getActiveExcludeLists
import com.cherret.zaprett.utils.getActiveLists
import com.cherret.zaprett.utils.getActiveStrategy
import com.cherret.zaprett.utils.getAppsListMode
import com.cherret.zaprett.utils.getHostListMode
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -127,21 +131,23 @@ class ByeDpiVpnService : VpnService() {
builder.addAddress("fd00::1", 128)
.addRoute("::", 0)
}
val applist = getAppsListMode(sharedPreferences)
if (applist.equals("none")) {
builder.addDisallowedApplication(applicationContext.packageName)
}
if (applist.equals("whitelist")) {
val whitelist = sharedPreferences.getStringSet("whitelist", emptySet())
whitelist!!.forEach {
builder.addAllowedApplication(it)
val appList = getAppsListMode(sharedPreferences)
when (appList) {
"blacklist" -> {
builder.addDisallowedApplication(applicationContext.packageName)
val blacklist = sharedPreferences.getStringSet("blacklist", emptySet())?: emptySet()
blacklist.forEach {
builder.addDisallowedApplication(it)
}
}
}
if (applist.equals("blacklist")) {
builder.addDisallowedApplication(applicationContext.packageName)
val blacklist = sharedPreferences.getStringSet("blacklist", emptySet())
blacklist!!.forEach {
builder.addDisallowedApplication(it)
"whitelist" -> {
val whitelist = sharedPreferences.getStringSet("whitelist", emptySet())?: emptySet()
whitelist.forEach {
builder.addAllowedApplication(it)
}
}
else -> {
builder.addDisallowedApplication(applicationContext.packageName)
}
}
Log.d("builder", builder.toString())
@@ -179,9 +185,9 @@ class ByeDpiVpnService : VpnService() {
private fun startByeDpi() {
val socksIp = sharedPreferences.getString("ip", "127.0.0.1")?: "127.0.0.1"
val socksPort = sharedPreferences.getString("port", "1080")?: "1080"
val listSet = sharedPreferences.getStringSet("lists", emptySet())?: emptySet()
val listSet = if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences) else getActiveExcludeLists(sharedPreferences)
CoroutineScope(Dispatchers.IO).launch {
val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), prepareList(listSet))
val args = parseArgs(socksIp, socksPort, getActiveStrategy(sharedPreferences), prepareList(listSet), sharedPreferences)
val result = NativeBridge().startProxy(args)
if (result < 0) {
Log.d("proxy","Failed to start byedpi proxy")
@@ -190,7 +196,7 @@ class ByeDpiVpnService : VpnService() {
}
}
}
suspend fun prepareList(actlists: Set<String>): String {
private suspend fun prepareList(actlists: Array<String>): String {
if (actlists.isNotEmpty()) {
val lists: Array<File> = actlists.map { File(it) }.toTypedArray()
val hostlist = withContext(Dispatchers.IO) {
@@ -212,15 +218,23 @@ class ByeDpiVpnService : VpnService() {
return ""
}
fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String): Array<String> {
private fun parseArgs(ip: String, port: String, rawArgs: List<String>, list : String, sharedPreferences: SharedPreferences): Array<String> {
val regex = Regex("""--?\S+(?:=(?:[^"'\s]+|"[^"]*"|'[^']*'))?|[^\s]+""")
val parsedArgs = rawArgs
.flatMap { args -> regex.findAll(args).map { it.value } }
.flatMap { arg ->
when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$hostlist" && list.isEmpty() -> emptyList()
else -> listOf(arg)
if (getHostListMode(sharedPreferences) == "whitelist") {
when {
arg == "\$hostlist" && list.isNotEmpty() -> listOf("-H", list)
arg == "\$hostlist" && list.isEmpty() -> emptyList()
else -> listOf(arg)
}
} else {
if (list.isNotEmpty()) {
listOf("-H", list, "-An", arg).filter { it != "\$hostlist" }
} else {
listOf("-An", arg).filter { it != "\$hostlist" }
}
}
}
.toMutableList()

View File

@@ -0,0 +1,5 @@
package com.cherret.zaprett.data;
enum class AppListType {
Blacklist,
Whitelist
}

View File

@@ -0,0 +1,5 @@
package com.cherret.zaprett.data
enum class ItemType {
byedpi, nfqws, list, list_exclude
}

View File

@@ -1,4 +1,4 @@
package com.cherret.zaprett.byedpi
package com.cherret.zaprett.data
enum class ServiceStatus {
Disconnected,

View File

@@ -0,0 +1,11 @@
package com.cherret.zaprett.data
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.ui.graphics.vector.ImageVector
import com.cherret.zaprett.R
data class ServiceStatusUI(
val textRes: Int = R.string.status_not_availible,
val icon: ImageVector = Icons.AutoMirrored.Filled.Help
)

View File

@@ -67,6 +67,7 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.R
import com.cherret.zaprett.data.ServiceStatusUI
import com.cherret.zaprett.ui.viewmodel.HomeViewModel
import kotlinx.coroutines.CoroutineScope
@@ -78,8 +79,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
val requestVpnPermission by viewModel.requestVpnPermission.collectAsState()
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val cardText = viewModel.cardText
val cardIcon = viewModel.cardIcon;
val status by viewModel.serviceStatus.collectAsState()
val changeLog = viewModel.changeLog
val newVersion = viewModel.newVersion
val updateAvailable = viewModel.updateAvailable
@@ -124,7 +124,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
Column(modifier = Modifier
.padding(paddingValues)
.verticalScroll(rememberScrollState())) {
ServiceStatusCard(viewModel, cardText, cardIcon, snackbarHostState, scope)
ServiceStatusCard(viewModel, status, snackbarHostState, scope)
UpdateCard(updateAvailable) { viewModel.showUpdateDialog() }
if (showUpdateDialog) {
UpdateDialog(viewModel, changeLog.value.orEmpty(), newVersion) { viewModel.dismissUpdateDialog() }
@@ -142,7 +142,7 @@ fun HomeScreen(viewModel: HomeViewModel = viewModel(), vpnLauncher: ActivityResu
}
@Composable
private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<Int>, cardIcon : MutableState<ImageVector>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
private fun ServiceStatusCard(viewModel: HomeViewModel, status: ServiceStatusUI, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
ElevatedCard(
elevation = CardDefaults.cardElevation(6.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
@@ -162,14 +162,14 @@ private fun ServiceStatusCard(viewModel: HomeViewModel, cardText: MutableState<I
)
{
Icon(
painter = rememberVectorPainter(cardIcon.value),
painter = rememberVectorPainter(status.icon),
modifier = Modifier
.width(60.dp)
.height(60.dp),
contentDescription = "icon"
)
Text(
text = stringResource(cardText.value),
text = stringResource(status.textRes),
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
fontSize = 16.sp,
//maxLines = 3,
@@ -340,5 +340,4 @@ fun UpdateDialog(viewModel: HomeViewModel, changeLog: String, newVersion: Mutabl
}
}
)
}

View File

@@ -1,14 +1,20 @@
package com.cherret.zaprett.ui.screen
import android.content.Context
import android.content.SharedPreferences
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -29,6 +35,9 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Switch
@@ -57,11 +66,13 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation.NavController
import com.cherret.zaprett.R
import com.cherret.zaprett.ui.viewmodel.HostsViewModel
import com.cherret.zaprett.utils.getHostListMode
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewModel()) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
val allLists = viewModel.allItems
@@ -70,7 +81,9 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
val filePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.OpenDocument()
) { uri ->
uri?.let { viewModel.copySelectedFile(context, "/lists", it) }
uri?.let {
if (getHostListMode(prefs) == "whitelist") viewModel.copySelectedFile(context, "/lists/include", it)
else viewModel.copySelectedFile(context, "/lists/exclude", it) }
}
LaunchedEffect(Unit) {
@@ -99,9 +112,15 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 80.dp
),
modifier = Modifier.navigationBarsPadding().fillMaxSize()
) {
item {
ListTypeChoose(viewModel, prefs)
}
when {
allLists.isEmpty() -> {
item {
@@ -215,6 +234,38 @@ private fun FloatingMenu(navController: NavController, launcher: ActivityResultL
}
}
@Composable
fun ListTypeChoose(viewModel: HostsViewModel, prefs : SharedPreferences) {
val listType = remember { mutableStateOf(getHostListMode(prefs))}
val options = listOf(stringResource(R.string.title_whitelist), stringResource(R.string.title_blacklist))
val selectedIndex = if (listType.value == "whitelist") 0 else 1
SingleChoiceSegmentedButtonRow (
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
options.forEachIndexed { index, label ->
SegmentedButton(
shape = SegmentedButtonDefaults.itemShape(
index = index,
count = options.size
),
onClick = {
listType.value = if (index == 0) "whitelist" else "blacklist"
viewModel.setListType(listType.value)
},
selected = index == selectedIndex,
label = {
Text(
label
)
}
)
}
}
}
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
launcher.launch(arrayOf("text/plain"))
}

View File

@@ -1,12 +1,20 @@
package com.cherret.zaprett.ui.screen
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.os.Build
import android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -15,6 +23,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.InstallMobile
import androidx.compose.material.icons.filled.Update
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -27,10 +36,13 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -46,6 +58,8 @@ import androidx.compose.ui.unit.sp
import androidx.navigation.NavController
import com.cherret.zaprett.R
import com.cherret.zaprett.ui.viewmodel.BaseRepoViewModel
import kotlinx.serialization.SerializationException
import java.io.IOException
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -57,11 +71,83 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
val isUpdateInstalling = viewModel.isUpdateInstalling
val isRefreshing = viewModel.isRefreshing.value
val snackbarHostState = remember { SnackbarHostState() }
val error by viewModel.errorFlow.collectAsState()
val downloadError by viewModel.downloadErrorFlow.collectAsState()
LaunchedEffect(Unit) {
viewModel.refresh()
}
if (error != null) {
AlertDialog(
onDismissRequest = {
viewModel.clearError()
navController.popBackStack()
},
title = { Text(stringResource(R.string.error_text)) },
text = {
Text(
when (error) {
is IOException -> stringResource(R.string.error_server_data)
is SerializationException -> stringResource(R.string.error_processing_data)
else -> stringResource(R.string.error_unknown)
}
)
},
dismissButton = {
TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("Error log", error?.message)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.btn_copy_log))
}
},
confirmButton = {
TextButton(onClick = {
viewModel.clearError()
navController.popBackStack()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
if (downloadError != null) {
AlertDialog(
onDismissRequest = {
viewModel.clearDownloadError()
},
title = { Text(stringResource(R.string.error_text)) },
text = {
Text(stringResource(R.string.download_error))
},
dismissButton = {
TextButton(onClick = {
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
val clip: ClipData = ClipData.newPlainText("Error log", downloadError)
clipboard.setPrimaryClip(clip)
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
Toast.makeText(context, context.getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
}
}) {
Text(stringResource(R.string.btn_copy_log))
}
},
confirmButton = {
TextButton(onClick = {
viewModel.clearDownloadError()
}) {
Text(stringResource(R.string.btn_continue))
}
}
)
}
Scaffold(
topBar = {
TopAppBar(
@@ -91,7 +177,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
) {
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
modifier = Modifier.navigationBarsPadding().fillMaxSize()
) {
if (hostLists.isEmpty()) {
item {

View File

@@ -3,8 +3,7 @@ package com.cherret.zaprett.ui.screen
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import android.graphics.drawable.Drawable
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Arrangement
@@ -14,46 +13,41 @@ import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowForward
import androidx.compose.material.icons.filled.AttachMoney
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
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.unit.IntOffset
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.Popup
import androidx.lifecycle.viewmodel.compose.viewModel
import coil3.compose.AsyncImage
import coil3.request.ImageRequest
import com.cherret.zaprett.BuildConfig
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.ui.viewmodel.AppListType
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.data.AppListType
import com.cherret.zaprett.ui.viewmodel.SettingsViewModel
import com.cherret.zaprett.utils.checkModuleInstallation
import com.cherret.zaprett.utils.checkRoot
import com.cherret.zaprett.utils.getAppsListMode
import com.cherret.zaprett.utils.getStartOnBoot
import com.cherret.zaprett.utils.isInList
import com.cherret.zaprett.utils.setAppsListMode
import com.cherret.zaprett.utils.setStartOnBoot
import com.cherret.zaprett.utils.stopService
import androidx.core.content.edit
import androidx.core.net.toUri
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -62,7 +56,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
val editor = remember { sharedPreferences.edit() }
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", true)) }
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) }
@@ -79,6 +73,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
val showWhiteDialog = remember { mutableStateOf(false) }
val showBlackDialog = remember { mutableStateOf(false) }
val showAppsListsSheet = remember { mutableStateOf(false) }
val showSystemApps = remember { mutableStateOf(sharedPreferences.getBoolean("show_system_apps", false)) }
val settingsList = listOf(
Setting.Section(stringResource(R.string.general_section)),
@@ -89,7 +84,6 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
useModule(
context = context,
checked = isChecked,
updateOnBoot = updateOnBoot,
openNoRootDialog = openNoRootDialog,
openNoModuleDialog = openNoModuleDialog
) { success ->
@@ -127,14 +121,14 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
Setting.Action(
title = stringResource(R.string.btn_repository_url_lists),
onClick = {
textDialogValue.value = sharedPreferences.getString("hosts_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json"
textDialogValue.value = sharedPreferences.getString("hosts_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json"
showHostsRepoUrlDialog.value = true
}
),
Setting.Action(
title = stringResource(R.string.btn_repository_url_strategies),
onClick = {
textDialogValue.value = sharedPreferences.getString("strategy_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json"
textDialogValue.value = sharedPreferences.getString("strategy_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json") ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json"
showStrategyRepoUrlDialog.value = true
}
),
@@ -229,7 +223,7 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
if (showStrategyRepoUrlDialog.value) {
TextDialog(stringResource(R.string.btn_repository_url_strategies), stringResource(R.string.hint_enter_repository_url_strategies), textDialogValue.value, onConfirm = {
editor.putString("strategy_repo_url", it).apply()
editor.putString("strategies_repo_url", it).apply()
}, onDismiss = { showStrategyRepoUrlDialog.value = false })
}
@@ -258,7 +252,9 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
viewModel.clearList()
},
viewModel = viewModel,
listType = AppListType.Whitelist
listType = AppListType.Whitelist,
sharedPreferences,
showSystemApps
)
}
@@ -269,7 +265,9 @@ fun SettingsScreen(viewModel : SettingsViewModel = viewModel()) {
viewModel.clearList()
},
viewModel = viewModel,
listType = AppListType.Blacklist
listType = AppListType.Blacklist,
sharedPreferences,
showSystemApps
)
}
@@ -493,7 +491,7 @@ private fun SettingsSection(title: String) {
)
}
private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit) {
private fun useModule(context: Context, checked: Boolean, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit) {
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
if (checked) {
@@ -511,7 +509,9 @@ private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableS
}
editor.remove("lists").apply()
editor.remove("active_strategy").apply()
updateOnBoot.value = true
editor.remove("applist").apply()
editor.remove("whitelist").apply()
editor.remove("blacklist").apply()
callback(true)
} else {
openNoModuleDialog.value = true
@@ -525,7 +525,6 @@ private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableS
editor.putBoolean("use_module", false)
.putBoolean("update_on_boot", false)
.apply()
updateOnBoot.value = false
callback(true)
}
}
@@ -594,11 +593,52 @@ private fun TextDialog(title: String, message: String, initialText: String, onCo
@Composable
private fun AboutDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
AlertDialog(
title = { Text(text = stringResource(R.string.about_title)) },
icon = {Icon(painterResource(R.drawable.ic_launcher_monochrome), contentDescription = stringResource(R.string.app_name), modifier = Modifier
.size(64.dp))},
text = { Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME)) },
text = {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME))
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW,
"https://github.com/CherretGit/zaprett-app".toUri())
context.startActivity(intent)
}) {
Icon(painterResource(R.drawable.github), "GitHub")
}
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW,
"https://t.me/zaprett_module".toUri())
context.startActivity(intent)
}) {
Icon(painterResource(R.drawable.telegram), "Telegram")
}
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW,
"https://matrix.to/#/#zaprett-group:matrix.cherret.ru".toUri())
context.startActivity(intent)
}) {
Icon(painterResource(R.drawable.matrix), "Matrix")
}
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW,
"https://pay.cloudtips.ru/p/672192fd".toUri())
context.startActivity(intent)
}) {
Icon(Icons.Default.AttachMoney, "Donate")
}
}
}
},
onDismissRequest = onDismiss,
confirmButton = { }
)
@@ -608,42 +648,96 @@ private fun AboutDialog(onDismiss: () -> Unit) {
private fun ChooseAppsDialog(
onDismissRequest: () -> Unit,
viewModel: SettingsViewModel,
listType: AppListType
listType: AppListType,
prefs: SharedPreferences,
showSystemApps : MutableState<Boolean>
) {
val appsList by viewModel.appsList.collectAsState()
val selectedPackages by viewModel.selectedPackages.collectAsState()
var searchQuery by remember { mutableStateOf("") }
val filteredList = remember(searchQuery, appsList) {
if (searchQuery.isBlank()) appsList
else appsList.filter { it.contains(searchQuery, ignoreCase = true) }
}
var expanded by remember { mutableStateOf(false) }
val title = if (listType == AppListType.Whitelist) stringResource(R.string.title_whitelist) else stringResource(R.string.title_blacklist)
LaunchedEffect(listType) {
viewModel.setListType(listType)
}
Dialog(onDismissRequest = onDismissRequest) {
Card(
modifier = Modifier
.fillMaxWidth()
.height(600.dp)
.wrapContentHeight()
.padding(16.dp),
shape = RoundedCornerShape(16.dp),
) {
Column (
modifier = Modifier.fillMaxSize()
){
Text(
text = title,
modifier = Modifier.padding(16.dp),
fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
Column(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()) {
Row(modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = title,
modifier = Modifier.weight(1f),
fontWeight = FontWeight.Bold,
fontSize = 24.sp
)
Box {
IconButton(onClick = { expanded = !expanded }) {
Icon(Icons.Default.MoreVert, contentDescription = "More options")
}
DropdownMenu(
expanded = expanded,
onDismissRequest = { expanded = false }
) {
DropdownMenuItem(
text = {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.btn_show_system_apps),
fontSize = 12.sp,
textAlign = TextAlign.Center
)
Checkbox(
checked = showSystemApps.value,
onCheckedChange = null
)
}
},
onClick = {
prefs.edit { putBoolean("show_system_apps", !showSystemApps.value) }
showSystemApps.value = !showSystemApps.value
viewModel.refreshApplications()
expanded = false
}
)
}
}
}
Row {
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
placeholder = { Text(stringResource(R.string.search_field)) },
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
verticalArrangement = Arrangement.SpaceBetween,
.heightIn(max = 400.dp),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(appsList){
AppItem(viewModel(), it, isInList(listType, it, LocalContext.current.getSharedPreferences("settings",
Context.MODE_PRIVATE), LocalContext.current), { isChecked ->
items(filteredList) {
AppItem(viewModel(), it, selectedPackages.contains(it), { isChecked ->
if (isChecked){ viewModel.addToList(listType, it) }
else { viewModel.removeFromList(listType, it) }
}
@@ -653,7 +747,6 @@ private fun ChooseAppsDialog(
Row(
modifier = Modifier
.fillMaxWidth()
//.height(24.dp)
.padding(bottom = 8.dp, end = 8.dp),
horizontalArrangement = Arrangement.End,
verticalAlignment = Alignment.CenterVertically
@@ -671,7 +764,11 @@ private fun ChooseAppsDialog(
}
@Composable
private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled : Boolean, onCheckedChange: (Boolean) -> Unit){
private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled : Boolean, onCheckedChange: (Boolean) -> Unit) {
var bitmap by remember { mutableStateOf<Drawable?>(null) }
LaunchedEffect(packageName) {
bitmap = viewModel.getAppIconBitmap(packageName)
}
Row (
modifier = Modifier
.fillMaxWidth()
@@ -680,7 +777,7 @@ private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled
verticalAlignment = Alignment.CenterVertically,
) {
AsyncImage(
model = ImageRequest.Builder(LocalContext.current).data(viewModel.getAppIconBitmap(packageName)).build(),
model = bitmap,
contentDescription = null,
modifier = Modifier
.padding(4.dp)
@@ -697,7 +794,6 @@ private fun AppItem(viewModel: SettingsViewModel, packageName : String, enabled
checked = enabled,
onCheckedChange = onCheckedChange
)
}
}

View File

@@ -6,10 +6,12 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
@@ -105,8 +107,11 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
modifier = Modifier.fillMaxSize()
) {
LazyColumn(
contentPadding = paddingValues,
modifier = Modifier.fillMaxSize()
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding(),
bottom = paddingValues.calculateBottomPadding() + 80.dp
),
modifier = Modifier.navigationBarsPadding().fillMaxSize()
) {
when {
allLists.isEmpty() -> {

View File

@@ -1,6 +0,0 @@
package com.cherret.zaprett.ui.viewmodel;
public enum AppListType {
Blacklist,
Whitelist
}

View File

@@ -11,18 +11,28 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.cherret.zaprett.utils.RepoItemInfo
import com.cherret.zaprett.R
import com.cherret.zaprett.data.ItemType
import com.cherret.zaprett.utils.download
import com.cherret.zaprett.utils.getFileSha256
import com.cherret.zaprett.utils.getHostListMode
import com.cherret.zaprett.utils.getZaprettPath
import com.cherret.zaprett.utils.registerDownloadListenerHost
import com.cherret.zaprett.utils.restartService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import java.io.File
abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(application) {
val context = application.applicationContext
val sharedPreferences: SharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val _errorFlow = MutableStateFlow<Throwable?>(null)
val errorFlow: StateFlow<Throwable?> = _errorFlow
private val _downloadErrorFlow = MutableStateFlow<String?>(null)
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
protected set
@@ -35,33 +45,47 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
val isUpdateInstalling = mutableStateMapOf<String, Boolean>()
abstract fun getInstalledLists(): Array<String>
abstract fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit)
abstract fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit)
fun refresh() {
isRefreshing.value = true
getRepoList { list ->
getRepoList { result ->
viewModelScope.launch(Dispatchers.IO) {
val safeList = list ?: emptyList()
val useModule = sharedPreferences.getBoolean("use_module", false)
val filteredList = safeList.filter { item ->
when (item.type) {
"list" -> true
"nfqws" -> useModule
"byedpi" -> !useModule
else -> false
result
.onSuccess { safeList ->
val useModule = sharedPreferences.getBoolean("use_module", false)
val listType = getHostListMode(sharedPreferences)
val filteredList = safeList.filter { item ->
when (item.type) {
ItemType.list -> listType == "whitelist"
ItemType.list_exclude -> listType == "blacklist"
ItemType.nfqws -> useModule
ItemType.byedpi -> !useModule
}
}
hostLists.value = filteredList
isUpdate.clear()
val existingHashes = getInstalledLists().map { getFileSha256(File(it)) }
for (item in filteredList) {
isUpdate[item.name] = item.hash !in existingHashes
}
}
.onFailure { e ->
_errorFlow.value = e
}
}
hostLists.value = filteredList
isUpdate.clear()
val existingHashes = getInstalledLists().map { getFileSha256(File(it)) }
for (item in filteredList) {
isUpdate[item.name] = item.hash !in existingHashes
}
isRefreshing.value = false
}
isRefreshing.value = false
}
}
fun clearError() {
_errorFlow.value = null
}
fun clearDownloadError() {
_downloadErrorFlow.value = null
}
fun isItemInstalled(item: RepoItemInfo): Boolean {
return getInstalledLists().any { File(it).name == item.name }
}
@@ -69,13 +93,14 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
fun install(item: RepoItemInfo) {
isInstalling[item.name] = true
val downloadId = download(context, item.url)
registerDownloadListenerHost(context, downloadId) { uri ->
registerDownloadListenerHost(context, downloadId, { uri ->
viewModelScope.launch(Dispatchers.IO) {
val sourceFile = File(uri.path!!)
val targetDir = when (item.type) {
"byedpi" -> File(getZaprettPath(), "strategies/byedpi")
"nfqws" -> File(getZaprettPath(), "strategies/nfqws")
else -> File(getZaprettPath(), "lists")
ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi")
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
ItemType.list -> File(getZaprettPath(), "lists/include")
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
}
val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true)
@@ -84,29 +109,47 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
isUpdate[item.name] = false
refresh()
}
}
}, onError = {
isInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
_downloadErrorFlow.value = it
})
}
fun update(item: RepoItemInfo) {
isUpdateInstalling[item.name] = true
val downloadId = download(context, item.url)
registerDownloadListenerHost(context, downloadId) { uri ->
viewModelScope.launch(Dispatchers.IO) {
val sourceFile = File(uri.path!!)
val targetDir = when (item.type) {
"byedpi" -> File(getZaprettPath(), "strategies/byedpi")
"nfqws" -> File(getZaprettPath(), "strategies/nfqws")
else -> File(getZaprettPath(), "lists")
registerDownloadListenerHost(
context,
downloadId,
onDownloaded = { uri ->
viewModelScope.launch(Dispatchers.IO) {
val sourceFile = File(uri.path!!)
val targetDir = when (item.type) {
ItemType.byedpi -> File(getZaprettPath(), "strategies/byedpi")
ItemType.nfqws -> File(getZaprettPath(), "strategies/nfqws")
ItemType.list -> File(getZaprettPath(), "lists/include")
ItemType.list_exclude -> File(getZaprettPath(), "lists/exclude")
}
val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
isUpdateInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
}
val targetFile = File(targetDir, uri.lastPathSegment!!)
sourceFile.copyTo(targetFile, overwrite = true)
sourceFile.delete()
},
onError = { error ->
isUpdateInstalling[item.name] = false
isUpdate[item.name] = false
refresh()
_downloadErrorFlow.value = error
}
}
)
}
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
viewModelScope.launch {
val result = snackbarHostState.showSnackbar(

View File

@@ -7,7 +7,6 @@ import android.net.Uri
import android.provider.Settings
import android.widget.Toast
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Help
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material3.SnackbarHostState
@@ -17,7 +16,8 @@ import androidx.core.content.ContextCompat
import androidx.lifecycle.AndroidViewModel
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.data.ServiceStatusUI
import com.cherret.zaprett.utils.download
import com.cherret.zaprett.utils.getActiveStrategy
import com.cherret.zaprett.utils.getBinVersion
@@ -32,6 +32,7 @@ import com.cherret.zaprett.utils.startService
import com.cherret.zaprett.utils.stopService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
@@ -40,10 +41,8 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
private val _requestVpnPermission = MutableStateFlow(false)
val requestVpnPermission = _requestVpnPermission.asStateFlow()
var cardText = mutableIntStateOf(R.string.status_not_availible) // MVP temporarily(maybe)
private set
var cardIcon = mutableStateOf(Icons.AutoMirrored.Filled.Help)
private set
private val _serviceStatus = MutableStateFlow(ServiceStatusUI())
val serviceStatus: StateFlow<ServiceStatusUI> = _serviceStatus.asStateFlow()
var moduleVer = mutableStateOf(context.getString(R.string.unknown_text))
private set
@@ -84,55 +83,37 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
}
}
fun checkServiceStatus() {
if (prefs.getBoolean("use_module", false) && prefs.getBoolean("update_on_boot", false)) {
private fun updateServiceStatus(useModule: Boolean) {
if (useModule) {
getStatus { isEnabled ->
if (isEnabled){
cardText.intValue = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.intValue = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
_serviceStatus.value = if (isEnabled) {
ServiceStatusUI(R.string.status_enabled, Icons.Filled.CheckCircle)
} else {
ServiceStatusUI(R.string.status_disabled, Icons.Filled.Cancel)
}
}
}
else {
if (ByeDpiVpnService.status == ServiceStatus.Connected){
cardText.intValue = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.intValue = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
} else {
_serviceStatus.value = if (ByeDpiVpnService.status == ServiceStatus.Connected) {
ServiceStatusUI(R.string.status_enabled, Icons.Filled.CheckCircle)
} else {
ServiceStatusUI(R.string.status_disabled, Icons.Filled.Cancel)
}
}
}
fun onCardClick() {
if (prefs.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled){
cardText.intValue = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.intValue = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
}
} else {
if (ByeDpiVpnService.status == ServiceStatus.Connected){
cardText.intValue = R.string.status_enabled
cardIcon.value = Icons.Filled.CheckCircle
}
else {
cardText.intValue = R.string.status_disabled
cardIcon.value = Icons.Filled.Cancel
}
fun checkServiceStatus() {
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
if (updateOnBoot) {
val useModule = prefs.getBoolean("use_module", false)
updateServiceStatus(useModule)
}
}
fun onCardClick() {
val useModule = prefs.getBoolean("use_module", false)
updateServiceStatus(useModule)
}
fun startVpn() {
ContextCompat.startForegroundService(context, Intent(context, ByeDpiVpnService::class.java).apply { action = "START_VPN" })
}

View File

@@ -7,5 +7,5 @@ import com.cherret.zaprett.utils.getHostList
class HostRepoViewModel(application: Application): BaseRepoViewModel(application) {
override fun getInstalledLists(): Array<String> = getAllLists()
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getHostList(sharedPreferences, callback)
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) = getHostList(sharedPreferences, callback)
}

View File

@@ -5,16 +5,24 @@ import android.content.Context
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.utils.disableList
import com.cherret.zaprett.utils.enableList
import com.cherret.zaprett.utils.getActiveExcludeLists
import com.cherret.zaprett.utils.getActiveLists
import com.cherret.zaprett.utils.getAllExcludeLists
import com.cherret.zaprett.utils.getAllLists
import com.cherret.zaprett.utils.getHostListMode
import com.cherret.zaprett.utils.getStatus
import com.cherret.zaprett.utils.setHostListMode
import kotlinx.coroutines.CoroutineScope
import java.io.File
class HostsViewModel(application: Application): BaseListsViewModel(application) {
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
override fun loadAllItems(): Array<String> = getAllLists()
override fun loadActiveItems(): Array<String> = getActiveLists(sharedPreferences)
override fun loadAllItems(): Array<String> =
if (getHostListMode(sharedPreferences) == "whitelist") getAllLists()
else getAllExcludeLists()
override fun loadActiveItems(): Array<String> =
if (getHostListMode(sharedPreferences) == "whitelist") getActiveLists(sharedPreferences)
else getActiveExcludeLists(sharedPreferences)
override fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val wasChecked = checked[item] == true
@@ -24,6 +32,7 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled && wasChecked) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(context, snackbarHostState, scope)
}
}
@@ -36,6 +45,7 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(
context,
snackbarHostState,
@@ -45,4 +55,9 @@ class HostsViewModel(application: Application): BaseListsViewModel(application)
}
}
}
fun setListType(type : String) {
setHostListMode(sharedPreferences, type)
refresh()
}
}

View File

@@ -2,53 +2,41 @@ package com.cherret.zaprett.ui.viewmodel
import android.app.Application
import android.content.Context.MODE_PRIVATE
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
import androidx.lifecycle.AndroidViewModel
import androidx.core.graphics.createBitmap
import com.cherret.zaprett.data.AppListType
import com.cherret.zaprett.utils.addPackageToList
import com.cherret.zaprett.utils.getAppList
import com.cherret.zaprett.utils.removePackageFromList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.withContext
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
private val context = application
private val prefs = context.getSharedPreferences("settings", MODE_PRIVATE)
private val _appsList = MutableStateFlow<List<String>>(emptyList())
val appsList: StateFlow<List<String>> = _appsList
private val _selectedPackages = MutableStateFlow<Set<String>>(emptySet())
val selectedPackages: StateFlow<Set<String>> = _selectedPackages.asStateFlow()
private val _currentListType = MutableStateFlow(AppListType.Whitelist)
init {
refreshApplications()
}
fun getAppIconBitmap(packageName: String): Bitmap? {
val pm: PackageManager = getApplication<Application>().packageManager
suspend fun getAppIconBitmap(packageName: String): Drawable? = withContext(Dispatchers.IO) {
val pm: PackageManager = context.packageManager
val drawable: Drawable = try {
pm.getApplicationIcon(packageName)
} catch (e: PackageManager.NameNotFoundException) {
return null
return@withContext null
}
return drawableToBitmap(drawable)
}
private fun drawableToBitmap(drawable: Drawable): Bitmap {
if (drawable is BitmapDrawable) {
return drawable.bitmap
}
val bitmap = createBitmap(
drawable.intrinsicWidth.coerceAtLeast(1),
drawable.intrinsicHeight.coerceAtLeast(1)
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
return bitmap
return@withContext drawable
}
fun getApplicationName(packageName: String) : String? {
@@ -68,23 +56,32 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
fun clearList() {
_appsList.value = emptyList()
_selectedPackages.value = emptySet()
}
fun refreshApplications() {
val context = getApplication<Application>()
val packages = context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
val packages = if (prefs.getBoolean("show_system_apps", false)){
context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA)
}
else {
context.packageManager.getInstalledPackages(PackageManager.GET_META_DATA).filter { pkgInfo ->
(pkgInfo.applicationInfo!!.flags and ApplicationInfo.FLAG_SYSTEM) == 0
}
}
val allPackages = packages.map { it.packageName }
val listType = _currentListType.value
val result = when (listType) {
AppListType.Whitelist -> {
val whitelistSet = getAppList(AppListType.Whitelist, context.getSharedPreferences("settings", MODE_PRIVATE), context)
val whitelistSet = getAppList(AppListType.Whitelist, prefs, context)
val (whitelisted, others) = allPackages.partition { it in whitelistSet }
_selectedPackages.value = whitelistSet
(whitelisted + others).filter { it != context.packageName }
}
AppListType.Blacklist -> {
val blacklistSet = getAppList(AppListType.Blacklist, context.getSharedPreferences("settings", MODE_PRIVATE), context)
val blacklistSet = getAppList(AppListType.Blacklist, prefs, context)
val (blacklisted, others) = allPackages.partition { it in blacklistSet }
_selectedPackages.value = blacklistSet
(blacklisted + others).filter { it != context.packageName }
}
}
@@ -92,12 +89,14 @@ class SettingsViewModel(application: Application) : AndroidViewModel(application
}
fun addToList(listType: AppListType, packageName: String) {
addPackageToList(listType, packageName, context.getSharedPreferences("settings", MODE_PRIVATE), context)
addPackageToList(listType, packageName, prefs, context)
selectedPackages.value.plus(packageName)
refreshApplications()
}
fun removeFromList(listType: AppListType, packageName: String) {
removePackageFromList(listType, packageName, context.getSharedPreferences("settings", MODE_PRIVATE), context)
removePackageFromList(listType, packageName, prefs, context)
selectedPackages.value.minus(packageName)
refreshApplications()
}

View File

@@ -13,5 +13,5 @@ class StrategyRepoViewModel(application: Application): BaseRepoViewModel(applica
} else {
getAllByeDPIStrategies()
}
override fun getRepoList(callback: (List<RepoItemInfo>?) -> Unit) = getStrategiesList(sharedPreferences, callback)
override fun getRepoList(callback: (Result<List<RepoItemInfo>>) -> Unit) = getStrategiesList(sharedPreferences, callback)
}

View File

@@ -5,7 +5,7 @@ import android.content.Context
import android.content.SharedPreferences
import androidx.compose.material3.SnackbarHostState
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.data.ServiceStatus
import com.cherret.zaprett.utils.disableStrategy
import com.cherret.zaprett.utils.enableStrategy
import com.cherret.zaprett.utils.getActiveByeDPIStrategies
@@ -36,6 +36,7 @@ class StrategyViewModel(application: Application): BaseListsViewModel(applicatio
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled && wasChecked) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(context, snackbarHostState, scope)
}
}
@@ -64,6 +65,7 @@ class StrategyViewModel(application: Application): BaseListsViewModel(applicatio
if (sharedPreferences.getBoolean("use_module", false)) {
getStatus { isEnabled ->
if (isEnabled) {
snackbarHostState.currentSnackbarData?.dismiss()
showRestartSnackbar(
context,
snackbarHostState,

View File

@@ -1,6 +1,5 @@
package com.cherret.zaprett.utils
import android.app.Application
import android.content.Context
import android.content.Context.MODE_PRIVATE
import android.content.SharedPreferences
@@ -13,8 +12,7 @@ import java.io.FileOutputStream
import java.io.IOException
import java.util.Properties
import androidx.core.content.edit
import com.cherret.zaprett.ui.viewmodel.AppListType
import com.cherret.zaprett.ui.viewmodel.SettingsViewModel
import com.cherret.zaprett.data.AppListType
fun checkRoot(callback: (Boolean) -> Unit) {
Shell.getShell().isRoot.let { callback(it) }
@@ -74,7 +72,7 @@ fun setStartOnBoot(startOnBoot: Boolean) {
FileInputStream(configFile).use { input ->
props.load(input)
}
props.setProperty("autorestart", startOnBoot.toString())
props.setProperty("start_on_boot", startOnBoot.toString())
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
@@ -92,7 +90,7 @@ fun getStartOnBoot(): Boolean {
FileInputStream(configFile).use { input ->
props.load(input)
}
props.getProperty("autorestart", "false").toBoolean()
props.getProperty("start_on_boot", "false").toBoolean()
} else {
false
}
@@ -118,32 +116,38 @@ fun getZaprettPath(): String {
}
fun getAllLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/")
if (listsDir.exists() && listsDir.isDirectory) {
val onlyNames = listsDir.list() ?: return emptyArray()
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
}
return emptyArray()
val listsDir = File("${getZaprettPath()}/lists/include")
return listsDir.listFiles { file -> file.isFile }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getAllExcludeLists(): Array<String> {
val listsDir = File("${getZaprettPath()}/lists/exclude/")
return listsDir.listFiles { file -> file.isFile }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getAllNfqwsStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/nfqws")
if (listsDir.exists() && listsDir.isDirectory) {
val onlyNames = listsDir.list() ?: return emptyArray()
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
}
return emptyArray()
return listsDir.listFiles { file -> file.isFile }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getAllByeDPIStrategies(): Array<String> {
val listsDir = File("${getZaprettPath()}/strategies/byedpi")
if (listsDir.exists() && listsDir.isDirectory) {
val onlyNames = listsDir.list() ?: return emptyArray()
return onlyNames.map { "$listsDir/$it" }.toTypedArray()
}
return emptyArray()
return listsDir.listFiles { file -> file.isFile }
?.map { it.absolutePath }
?.toTypedArray()
?: emptyArray()
}
fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile()
@@ -153,7 +157,7 @@ fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("activelists", "")
val activeLists = props.getProperty("active_lists", "")
Log.d("Active lists", activeLists)
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
@@ -167,6 +171,28 @@ fun getActiveLists(sharedPreferences: SharedPreferences): Array<String> {
return sharedPreferences.getStringSet("lists", emptySet())?.toTypedArray() ?: emptyArray()
}
}
fun getActiveExcludeLists(sharedPreferences: SharedPreferences): Array<String> {
if (sharedPreferences.getBoolean("use_module", false)) {
val configFile = getConfigFile()
if (configFile.exists()) {
val props = Properties()
return try {
FileInputStream(configFile).use { input ->
props.load(input)
}
val activeLists = props.getProperty("active_exclude_lists", "")
if (activeLists.isNotEmpty()) activeLists.split(",")
.toTypedArray() else emptyArray()
} catch (e: IOException) {
throw RuntimeException(e)
}
}
return emptyArray()
}
else {
return sharedPreferences.getStringSet("exclude_lists", emptySet())?.toTypedArray() ?: emptyArray()
}
}
fun getActiveNfqwsStrategies(): Array<String> {
val configFile = File("${getZaprettPath()}/config")
@@ -212,14 +238,21 @@ fun enableList(path: String, sharedPreferences: SharedPreferences) {
props.load(input)
}
}
val activeLists = props.getProperty("activelists", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
val activeLists = props.getProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
else "active_exclude_lists",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path !in activeLists) {
activeLists.add(path)
}
props.setProperty("activelists", activeLists.joinToString(","))
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
else "active_exclude_lists",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
@@ -228,10 +261,14 @@ fun enableList(path: String, sharedPreferences: SharedPreferences) {
}
}
else {
val currentSet = sharedPreferences.getStringSet("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
val currentSet = sharedPreferences.getStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path !in currentSet) {
currentSet.add(path)
sharedPreferences.edit { putStringSet("lists", currentSet) }
sharedPreferences.edit { putStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", currentSet) }
}
}
}
@@ -276,14 +313,21 @@ fun disableList(path: String, sharedPreferences: SharedPreferences) {
props.load(input)
}
}
val activeLists = props.getProperty("activelists", "")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
val activeLists = props.getProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
else "active_exclude_lists",
"")
.split(",")
.filter { it.isNotBlank() }
.toMutableList()
if (path in activeLists) {
activeLists.remove(path)
}
props.setProperty("activelists", activeLists.joinToString(","))
props.setProperty(
if (getHostListMode(sharedPreferences) == "whitelist") "active_lists"
else "active_exclude_lists",
activeLists.joinToString(",")
)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
@@ -292,13 +336,20 @@ fun disableList(path: String, sharedPreferences: SharedPreferences) {
}
}
else {
val currentSet = sharedPreferences.getStringSet("lists", emptySet())?.toMutableSet() ?: mutableSetOf()
val currentSet = sharedPreferences.getStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", emptySet())?.toMutableSet() ?: mutableSetOf()
if (path in currentSet) {
currentSet.remove(path)
sharedPreferences.edit { putStringSet("lists", currentSet) }
sharedPreferences.edit { putStringSet(
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists", currentSet) }
}
if (currentSet.isEmpty()) {
sharedPreferences.edit { remove("lists") }
sharedPreferences.edit { remove(
if (getHostListMode(sharedPreferences) == "whitelist") "lists"
else "exclude_lists"
) }
}
}
}
@@ -519,9 +570,9 @@ fun getAppsListMode(prefs : SharedPreferences) : String {
FileInputStream(configFile).use { input ->
props.load(input)
}
val applist = props.getProperty("applist", "")!!
Log.d("App list", "Equals to ${applist}")
return if (applist.equals("whitelist") || applist.equals("blacklist") || applist.equals("none")) applist
val applist = props.getProperty("app_list", "")!!
Log.d("App list", "Equals to $applist")
return if (applist == "whitelist" || applist == "blacklist" || applist == "none") applist
else "none"
} catch (e: IOException) {
throw RuntimeException(e)
@@ -536,6 +587,61 @@ fun getAppsListMode(prefs : SharedPreferences) : String {
fun setAppsListMode(prefs: SharedPreferences, mode: String) {
if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile()
val props = Properties()
if (configFile.exists()) {
try {
FileInputStream(configFile).use { input ->
props.load(input)
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
props.setProperty("app_list", mode)
try {
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
else {
prefs.edit { putString("app-list", mode) }
}
Log.d("App List", "Changed to $mode")
}
fun setHostListMode(prefs: SharedPreferences, mode: String) {
if (prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile()
val props = Properties()
if (configFile.exists()) {
try {
FileInputStream(configFile).use { input ->
props.load(input)
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
props.setProperty("list_type", mode)
try {
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
} catch (e: IOException) {
throw RuntimeException(e)
}
}
else {
prefs.edit { putString("list_type", mode) }
}
Log.d("App List", "Changed to $mode")
}
fun getHostListMode(prefs : SharedPreferences) : String {
if(prefs.getBoolean("use_module", false)) {
val configFile = getConfigFile()
if (configFile.exists()) {
val props = Properties()
@@ -543,17 +649,16 @@ fun setAppsListMode(prefs: SharedPreferences, mode: String) {
FileInputStream(configFile).use { input ->
props.load(input)
}
props.setProperty("applist", mode)
FileOutputStream(configFile).use { output ->
props.store(output, "Don't place '/' in end of directory! Example: /sdcard")
}
val hostlist = props.getProperty("list_type", "whitelist")!!
return if (hostlist == "whitelist" || hostlist == "blacklist") hostlist
else "whitelist"
} catch (e: IOException) {
throw RuntimeException(e)
}
}
}
else {
prefs.edit().putString("applist", mode).apply()
return prefs.getString("list_type", "whitelist")!!
}
Log.d("App List", "Changed to ${mode}")
return "whitelist"
}

View File

@@ -7,7 +7,7 @@ import android.service.quicksettings.TileService
import androidx.core.content.ContextCompat
import com.cherret.zaprett.R
import com.cherret.zaprett.byedpi.ByeDpiVpnService
import com.cherret.zaprett.byedpi.ServiceStatus
import com.cherret.zaprett.data.ServiceStatus
class QSTileService: TileService() {
private lateinit var prefs: SharedPreferences

View File

@@ -11,6 +11,7 @@ import android.net.Uri
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.cherret.zaprett.data.ItemType
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import okhttp3.Call
@@ -25,47 +26,50 @@ import java.security.MessageDigest
private val client = OkHttpClient()
private val json = Json { ignoreUnknownKeys = true }
fun getHostList(sharedPreferences: SharedPreferences, callback: (List<RepoItemInfo>?) -> Unit) {
val request = Request.Builder().url(sharedPreferences.getString("hosts_repo_url","https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json").build()
fun getHostList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
getRepo(
sharedPreferences.getString(
"hosts_repo_url",
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json"
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/hosts.json",
callback
)
}
fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (Result<List<RepoItemInfo>>) -> Unit) {
getRepo(
sharedPreferences.getString(
"strategies_repo_url",
"https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json"
) ?: "https://raw.githubusercontent.com/CherretGit/zaprett-repo/refs/heads/main/strategies.json",
callback
)
}
fun getRepo(url: String, callback: (Result<List<RepoItemInfo>>) -> Unit) {
val request = Request.Builder().url(url).build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
callback(null)
callback(Result.failure(e))
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) {
callback(null)
callback(Result.failure(IOException("Unexpected HTTP code ${response.code}")))
return
}
val jsonString = response.body.string()
val hostsInfo = json.decodeFromString<List<RepoItemInfo>>(jsonString)
callback(hostsInfo)
val result = runCatching {
json.decodeFromString<List<RepoItemInfo>>(jsonString)
}
callback(result)
}
}
})
}
fun getStrategiesList(sharedPreferences: SharedPreferences, callback: (List<RepoItemInfo>?) -> Unit) {
val request = Request.Builder().url(sharedPreferences.getString("strategies_repo_url", "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json")?: "https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/strategies.json").build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
callback(null)
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) {
callback(null)
}
val jsonString = response.body.string()
val strategiesInfo = json.decodeFromString<List<RepoItemInfo>>(jsonString)
callback(strategiesInfo)
}
}
})
}
fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit, onError: (String) -> Unit) {// AI Generated
val receiver = object : BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {
@@ -76,12 +80,32 @@ fun registerDownloadListenerHost(context: Context, downloadId: Long, onDownloade
dm.query(query)?.use { cursor ->
if (cursor.moveToFirst()) {
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
if (status == DownloadManager.STATUS_SUCCESSFUL) {
val uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
if (uriString != null) {
val uri = uriString.toUri()
val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
when (status) {
DownloadManager.STATUS_SUCCESSFUL -> {
val uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
if (uriString != null) {
val uri = uriString.toUri()
context.unregisterReceiver(this)
onDownloaded(uri)
}
}
DownloadManager.STATUS_FAILED -> {
context.unregisterReceiver(this)
onDownloaded(uri)
val errorMessage = when (reason) {
DownloadManager.ERROR_CANNOT_RESUME -> "Cannot resume download"
DownloadManager.ERROR_DEVICE_NOT_FOUND -> "Device not found"
DownloadManager.ERROR_FILE_ALREADY_EXISTS -> "File already exists"
DownloadManager.ERROR_FILE_ERROR -> "File error"
DownloadManager.ERROR_HTTP_DATA_ERROR -> "HTTP data error"
DownloadManager.ERROR_INSUFFICIENT_SPACE -> "Insufficient space"
DownloadManager.ERROR_TOO_MANY_REDIRECTS -> "Too many redirects"
DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> "Unhandled HTTP code"
DownloadManager.ERROR_UNKNOWN -> "Unknown error"
else -> "Download failed: reason=$reason"
}
onError(errorMessage)
}
}
}
@@ -113,7 +137,7 @@ data class RepoItemInfo(
val name: String,
val author: String,
val description: String,
val type: String? = null,
val type: ItemType,
val hash: String,
val url: String
)

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M0.632,0.55v22.9L2.28,23.45L2.28,24L0,24L0,0h2.28v0.55zM7.675,7.81v1.157h0.033c0.309,-0.443 0.683,-0.784 1.117,-1.024 0.433,-0.245 0.936,-0.365 1.5,-0.365 0.54,0 1.033,0.107 1.481,0.314 0.448,0.208 0.785,0.582 1.02,1.108 0.254,-0.374 0.6,-0.706 1.034,-0.992 0.434,-0.287 0.95,-0.43 1.546,-0.43 0.453,0 0.872,0.056 1.26,0.167 0.388,0.11 0.716,0.286 0.993,0.53 0.276,0.245 0.489,0.559 0.646,0.951 0.152,0.392 0.23,0.863 0.23,1.417v5.728h-2.349L16.186,11.52c0,-0.286 -0.01,-0.559 -0.032,-0.812a1.755,1.755 0,0 0,-0.18 -0.66,1.106 1.106,0 0,0 -0.438,-0.448c-0.194,-0.11 -0.457,-0.166 -0.785,-0.166 -0.332,0 -0.6,0.064 -0.803,0.189a1.38,1.38 0,0 0,-0.48 0.499,1.946 1.946,0 0,0 -0.231,0.696 5.56,5.56 0,0 0,-0.06 0.785v4.768h-2.35v-4.8c0,-0.254 -0.004,-0.503 -0.018,-0.752a2.074,2.074 0,0 0,-0.143 -0.688,1.052 1.052,0 0,0 -0.415,-0.503c-0.194,-0.125 -0.476,-0.19 -0.854,-0.19 -0.111,0 -0.259,0.024 -0.439,0.074 -0.18,0.051 -0.36,0.143 -0.53,0.282 -0.171,0.138 -0.319,0.337 -0.439,0.595 -0.12,0.259 -0.18,0.6 -0.18,1.02v4.966L5.46,16.375L5.46,7.81zM23.368,23.45L23.368,0.55L21.72,0.55L21.72,0L24,0v24h-2.28v-0.55z"/>
</vector>

View File

@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M11.944,0A12,12 0,0 0,0 12a12,12 0,0 0,12 12,12 12,0 0,0 12,-12A12,12 0,0 0,12 0a12,12 0,0 0,-0.056 0zM16.906,7.224c0.1,-0.002 0.321,0.023 0.465,0.14a0.506,0.506 0,0 1,0.171 0.325c0.016,0.093 0.036,0.306 0.02,0.472 -0.18,1.898 -0.962,6.502 -1.36,8.627 -0.168,0.9 -0.499,1.201 -0.82,1.23 -0.696,0.065 -1.225,-0.46 -1.9,-0.902 -1.056,-0.693 -1.653,-1.124 -2.678,-1.8 -1.185,-0.78 -0.417,-1.21 0.258,-1.91 0.177,-0.184 3.247,-2.977 3.307,-3.23 0.007,-0.032 0.014,-0.15 -0.056,-0.212s-0.174,-0.041 -0.249,-0.024c-0.106,0.024 -1.793,1.14 -5.061,3.345 -0.48,0.33 -0.913,0.49 -1.302,0.48 -0.428,-0.008 -1.252,-0.241 -1.865,-0.44 -0.752,-0.245 -1.349,-0.374 -1.297,-0.789 0.027,-0.216 0.325,-0.437 0.893,-0.663 3.498,-1.524 5.83,-2.529 6.998,-3.014 3.332,-1.386 4.025,-1.627 4.476,-1.635z"/>
</vector>

View File

@@ -78,7 +78,7 @@
<string name="qs_working">Работает</string>
<string name="qs_not_working">Не работает</string>
<string name="about_title">О приложении</string>
<string name="about_text">Zaprett app от Cherret, egor-white\nВерсия: %1$s</string>
<string name="about_text">zaprett app от Cherret, egor-white\nВерсия: %1$s</string>
<string name="btn_whitelist">Белый список приложений</string>
<string name="btn_blacklist">Чёрный список приложений</string>
<string name="shared_section">Общие настройки</string>
@@ -87,4 +87,13 @@
<string name="title_blacklist">Чёрный список</string>
<string name="btn_applist">Списки приложений</string>
<string name="radio_disabed">Выключено</string>
<string name="btn_show_system_apps">Показывать системные приложения</string>
<string name="search_field">Поиск…</string>
<string name="error_text">Ошибка</string>
<string name="error_server_data">Ошибка получения данных с сервера, пожалуйста проверьте интернет соединение</string>
<string name="error_processing_data">Ошибка при обработке данных</string>
<string name="error_unknown">Неизвестная ошибка</string>
<string name="btn_copy_log">Скопировать лог</string>
<string name="log_copied">Лог скопирован</string>
<string name="download_error">Произошла ошибка скачивания, пожалуйста, сообщите о ней разработчикам</string>
</resources>

View File

@@ -91,4 +91,13 @@
<string name="title_blacklist">Black list</string>
<string name="btn_applist">Apps lists</string>
<string name="radio_disabed">Disabled</string>
<string name="btn_show_system_apps">Show system apps</string>
<string name="search_field">Search…</string>
<string name="error_text">Error</string>
<string name="error_server_data">Error retrieving data from the server, please check your internet connection</string>
<string name="error_processing_data">Error processing data</string>
<string name="error_unknown">Unknown error</string>
<string name="btn_copy_log">Copy log</string>
<string name="log_copied">Log copied</string>
<string name="download_error">Occurred a file download error, please report to developers</string>
</resources>

View File

@@ -1 +1,4 @@
Исправление ошибок из 2.3
Добавление черных списков
Исправление ошибок
Внимание! Эта версия приложения совместима с модулем версии 5.0+

View File

@@ -1,14 +1,23 @@
[versions]
agp = "8.10.1"
kotlin = "2.0.21"
coreKtx = "1.10.1"
agp = "8.12.1"
kotlin = "2.2.10"
coreKtx = "1.17.0"
junit = "4.13.2"
junitVersion = "1.1.5"
espressoCore = "3.5.1"
lifecycleRuntimeKtx = "2.6.1"
activityCompose = "1.8.0"
composeBom = "2024.09.00"
lifecycleService = "2.9.1"
junitVersion = "1.3.0"
espressoCore = "3.7.0"
lifecycleRuntimeKtx = "2.9.2"
activityCompose = "1.10.1"
composeBom = "2025.08.00"
compose-material3 = "1.3.2"
compose-material3-adaptive = "1.4.0-beta02"
navigation = "2.9.3"
compose-icons = "1.7.8"
libsu = "6.0.0"
okhttp = "5.1.0"
serialization = "1.9.0"
firebase-bom = "34.1.0"
fragment-compose = "1.8.9"
coil3 = "3.3.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
@@ -25,6 +34,20 @@ androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-toolin
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
compose-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "compose-material3" }
compose-material3-window-size = { group = "androidx.compose.material3", name = "material3-window-size-class", version.ref = "compose-material3" }
compose-material3-adaptive-nav = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "compose-material3-adaptive" }
navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigation" }
compose-icons = { group = "androidx.compose.material", name = "material-icons-extended", version.ref = "compose-icons" }
libsu-core = { group = "com.github.topjohnwu.libsu", name = "core", version.ref = "libsu" }
okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" }
serialization-json = { group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version.ref = "serialization" }
firebase-bom = { group = "com.google.firebase", name = "firebase-bom", version.ref = "firebase-bom" }
firebase-analytics = { group = "com.google.firebase", name = "firebase-analytics" }
firebase-crashlytics = { group = "com.google.firebase", name = "firebase-crashlytics" }
fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragment-compose" }
coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil3" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }

View File

@@ -1,6 +1,6 @@
#Sat Mar 22 14:00:49 GMT+07:00 2025
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@@ -1,6 +1,6 @@
{
"version": "2.4",
"versionCode": 16,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2_4/app-release.apk",
"version": "2.7",
"versionCode": 19,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/2_7_0/app-release.apk",
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
}