mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 05:29:37 +05:00
Compare commits
2 Commits
ad614b3b4a
...
717368b4e9
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
717368b4e9 | ||
|
|
36885a8410 |
@@ -16,8 +16,8 @@ android {
|
||||
applicationId = "com.cherret.zaprett"
|
||||
minSdk = 29
|
||||
targetSdk = 35
|
||||
versionCode = 23
|
||||
versionName = "2.11"
|
||||
versionCode = 24
|
||||
versionName = "2.12"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ import com.cherret.zaprett.ui.viewmodel.HostRepoViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.IpsetRepoViewModel
|
||||
import com.cherret.zaprett.ui.viewmodel.StrategyRepoViewModel
|
||||
import com.cherret.zaprett.utils.checkModuleInstallation
|
||||
import com.cherret.zaprett.utils.checkStoragePermission
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.analytics
|
||||
@@ -110,16 +111,7 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
var showStoragePermissionDialog by remember {
|
||||
mutableStateOf(
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
!Environment.isExternalStorageManager()
|
||||
} else {
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
)
|
||||
mutableStateOf(!checkStoragePermission(this))
|
||||
}
|
||||
var showNotificationPermissionDialog by remember {
|
||||
mutableStateOf(
|
||||
@@ -272,5 +264,4 @@ class MainActivity : ComponentActivity() {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -31,10 +32,13 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
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.MutableState
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -67,6 +71,7 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
@@ -147,10 +152,26 @@ fun HostsScreen(navController: NavController, viewModel: HostsViewModel = viewMo
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -19,6 +19,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -31,10 +32,12 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
|
||||
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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -68,6 +71,7 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
@@ -148,10 +152,26 @@ fun IpsetsScreen(navController: NavController, viewModel: IpsetViewModel = viewM
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Composable
|
||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
|
||||
@@ -57,6 +57,7 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
val error by viewModel.errorFlow.collectAsState()
|
||||
val downloadError by viewModel.downloadErrorFlow.collectAsState()
|
||||
val showPermissionDialog by viewModel.showPermissionDialog.collectAsState()
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.refresh()
|
||||
}
|
||||
@@ -131,6 +132,24 @@ fun RepoScreen(navController: NavController, viewModel: BaseRepoViewModel) {
|
||||
)
|
||||
}
|
||||
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.DropdownMenuItem
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
@@ -25,10 +26,12 @@ 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.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
@@ -60,6 +63,7 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
val allLists = viewModel.allItems
|
||||
val checked = viewModel.checked
|
||||
val isRefreshing = viewModel.isRefreshing
|
||||
val showPermissionDialog by viewModel.showNoPermissionDialog.collectAsState()
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
@@ -139,6 +143,24 @@ fun StrategyScreen(navController: NavController, viewModel: StrategyViewModel =
|
||||
},
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
if (showPermissionDialog) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||
text = { Text(text = stringResource(R.string.no_storage_permission_message)) },
|
||||
onDismissRequest = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
viewModel.hideNoPermissionDialog()
|
||||
navController.popBackStack()
|
||||
}) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
|
||||
@@ -192,5 +192,4 @@ private fun NoHostsCard(noHostsCard: MutableState<Boolean>) {
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,9 +16,12 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.utils.checkStoragePermission
|
||||
import com.cherret.zaprett.utils.getZaprettPath
|
||||
import com.cherret.zaprett.utils.restartService
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
@@ -34,20 +37,32 @@ abstract class BaseListsViewModel(application: Application) : AndroidViewModel(a
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var _showNoPermissionDialog = MutableStateFlow(false)
|
||||
val showNoPermissionDialog: StateFlow<Boolean> = _showNoPermissionDialog
|
||||
|
||||
abstract fun loadAllItems(): Array<String>
|
||||
abstract fun loadActiveItems(): Array<String>
|
||||
abstract fun onCheckedChange(item: String, isChecked: Boolean, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
abstract fun deleteItem(item: String, snackbarHostState: SnackbarHostState, scope: CoroutineScope)
|
||||
|
||||
fun refresh() {
|
||||
isRefreshing = true
|
||||
allItems = loadAllItems().toList()
|
||||
activeItems = loadActiveItems().toList()
|
||||
checked.clear()
|
||||
allItems.forEach { list ->
|
||||
checked[list] = activeItems.contains(list)
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
isRefreshing = true
|
||||
allItems = loadAllItems().toList()
|
||||
activeItems = loadActiveItems().toList()
|
||||
checked.clear()
|
||||
allItems.forEach { list ->
|
||||
checked[list] = activeItems.contains(list)
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
false -> _showNoPermissionDialog.value = true
|
||||
}
|
||||
isRefreshing = false
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showNoPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
|
||||
@@ -13,6 +13,7 @@ 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.checkStoragePermission
|
||||
import com.cherret.zaprett.utils.download
|
||||
import com.cherret.zaprett.utils.getFileSha256
|
||||
import com.cherret.zaprett.utils.getHostListMode
|
||||
@@ -36,6 +37,9 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
|
||||
val downloadErrorFlow: StateFlow<String?> = _downloadErrorFlow
|
||||
|
||||
private var _showPermissionDialog = MutableStateFlow(false)
|
||||
val showPermissionDialog: StateFlow<Boolean> = _showPermissionDialog
|
||||
|
||||
var hostLists = mutableStateOf<List<RepoItemInfo>>(emptyList())
|
||||
protected set
|
||||
|
||||
@@ -95,32 +99,41 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
}
|
||||
|
||||
fun install(item: RepoItemInfo) {
|
||||
isInstalling[item.name] = true
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListener(context, downloadId, { 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")
|
||||
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
|
||||
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
|
||||
}
|
||||
val targetFile = File(targetDir, uri.lastPathSegment!!)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
isInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
when (checkStoragePermission(context)) {
|
||||
true -> {
|
||||
isInstalling[item.name] = true
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListener(context, downloadId, { 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")
|
||||
ItemType.ipset -> File(getZaprettPath(), "ipset/include")
|
||||
ItemType.ipset_exclude -> File(getZaprettPath(), "ipset/exclude")
|
||||
}
|
||||
val targetFile = File(targetDir, uri.lastPathSegment!!)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
isInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
}
|
||||
}, onError = {
|
||||
isInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
_downloadErrorFlow.value = it
|
||||
})
|
||||
}
|
||||
}, onError = {
|
||||
isInstalling[item.name] = false
|
||||
isUpdate[item.name] = false
|
||||
refresh()
|
||||
_downloadErrorFlow.value = it
|
||||
})
|
||||
false -> _showPermissionDialog.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun hideNoPermissionDialog() {
|
||||
_showPermissionDialog.value = false
|
||||
}
|
||||
|
||||
fun update(item: RepoItemInfo) {
|
||||
@@ -157,7 +170,6 @@ abstract class BaseRepoViewModel(application: Application) : AndroidViewModel(ap
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
fun showRestartSnackbar(snackbarHostState: SnackbarHostState) {
|
||||
viewModelScope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
|
||||
@@ -103,7 +103,7 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
}
|
||||
|
||||
fun checkServiceStatus() {
|
||||
val updateOnBoot = prefs.getBoolean("update_on_boot", false)
|
||||
val updateOnBoot = prefs.getBoolean("update_on_boot", true)
|
||||
if (updateOnBoot) {
|
||||
val useModule = prefs.getBoolean("use_module", false)
|
||||
updateServiceStatus(useModule)
|
||||
|
||||
@@ -18,12 +18,12 @@ import java.io.File
|
||||
|
||||
class StrategyViewModel(application: Application): BaseListsViewModel(application) {
|
||||
private val sharedPreferences = application.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
private val useModule = sharedPreferences.getBoolean("use_module", false)
|
||||
private val strategyProvider: StrategyProvider = if (useModule) {
|
||||
NfqwsStrategyProvider()
|
||||
} else {
|
||||
ByeDPIStrategyProvider(sharedPreferences)
|
||||
}
|
||||
private val strategyProvider: StrategyProvider
|
||||
get() = if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
NfqwsStrategyProvider()
|
||||
} else {
|
||||
ByeDPIStrategyProvider(sharedPreferences)
|
||||
}
|
||||
|
||||
override fun loadAllItems(): Array<String> = strategyProvider.getAll()
|
||||
override fun loadActiveItems(): Array<String> = strategyProvider.getActive()
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package com.cherret.zaprett.utils
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.edit
|
||||
import com.cherret.zaprett.data.AppListType
|
||||
import com.cherret.zaprett.data.ZaprettConfig
|
||||
@@ -44,6 +48,17 @@ private fun writeConfig(config: ZaprettConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
fun checkStoragePermission(context: Context): Boolean {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
Environment.isExternalStorageManager()
|
||||
} else {
|
||||
ContextCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
}
|
||||
|
||||
fun checkRoot(callback: (Boolean) -> Unit) {
|
||||
Shell.getShell().isRoot.let { callback(it) }
|
||||
}
|
||||
|
||||
@@ -117,4 +117,5 @@
|
||||
<string name="selection_no_hosts_title">Нет активных листов</string>
|
||||
<string name="selection_no_hosts_message">Не обнаружено активных списков хостов, включите один или несколько, иначе подбор не сработает</string>
|
||||
<string name="selection_available_domains">Доступные домены</string>
|
||||
<string name="no_storage_permission_message">Нет разрешения на доступ к файлам</string>
|
||||
</resources>
|
||||
@@ -122,4 +122,5 @@
|
||||
<string name="selection_no_hosts_title">No active hosts</string>
|
||||
<string name="selection_no_hosts_message">No active host lists found, please enable one or more, otherwise the selection will not work</string>
|
||||
<string name="selection_available_domains">Available domains</string>
|
||||
<string name="no_storage_permission_message">Missing permission to access files</string>
|
||||
</resources>
|
||||
Reference in New Issue
Block a user