22 Commits
1.0.0 ... 1_3_0

Author SHA1 Message Date
CherretGit
9e95ba9922 Update workflow.yml 2025-03-30 17:44:25 +07:00
CherretGit
aac80da9a7 Update workflow.yml 2025-03-30 17:25:59 +07:00
CherretGit
2b611ce8cd Update workflow.yml 2025-03-30 16:43:35 +07:00
CherretGit
6967ee286c Update workflow.yml 2025-03-30 16:02:44 +07:00
CherretGit
83737d5df9 Update workflow.yml 2025-03-30 15:55:00 +07:00
CherretGit
a05c6af2c1 Update workflow.yml 2025-03-30 15:47:35 +07:00
CherretGit
7b7f94f0e2 Create workflow.yml 2025-03-30 15:40:41 +07:00
CherretGit
8367eac9bc Update README.md 2025-03-30 14:43:20 +07:00
CherretGit
e95792b8c9 Update README.md 2025-03-30 14:42:02 +07:00
CherretGit
7352943bb3 Update README.md 2025-03-30 14:41:22 +07:00
CherretGit
b4da6bda5a Update README.md 2025-03-28 21:16:46 +07:00
Cherret
4ecc9a40d4 Merge remote-tracking branch 'origin/main' 2025-03-28 17:27:31 +07:00
Cherret
96dc70473e Auto Update 2025-03-28 17:26:11 +07:00
CherretGit
b05616d7ef Update changelog.md 2025-03-28 17:25:48 +07:00
CherretGit
316bdea986 Create update.json 2025-03-28 17:18:41 +07:00
CherretGit
de22bb048c Create changelog.md 2025-03-28 17:18:22 +07:00
Cherret
56d3c95f07 Merge remote-tracking branch 'origin/main' 2025-03-25 21:21:28 +07:00
Cherret
df4e7f9658 Fix App Freeze 2025-03-25 21:21:15 +07:00
CherretGit
3c44a449c3 README.md 2025-03-25 12:31:43 +07:00
CherretGit
e46b0e7d4f Delete images/1 2025-03-25 12:28:11 +07:00
CherretGit
136340949d Images 2025-03-25 12:27:39 +07:00
CherretGit
894899bfc9 1 2025-03-25 12:26:26 +07:00
19 changed files with 485 additions and 134 deletions

66
.github/workflows/workflow.yml vendored Normal file
View File

@@ -0,0 +1,66 @@
name: Build and Release
on:
workflow_dispatch:
inputs:
tag:
description: 'Tag for the release'
required: true
type: string
release_name:
description: 'Release Name'
required: true
type: string
release_notes:
description: 'Release Description'
required: true
type: string
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Set up Android SDK
uses: android-actions/setup-android@v2
- name: Build APK
run: ./gradlew assembleRelease
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE }}" | base64 --decode > keystore.jks
- name: Sign the APK
run: |
$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | sort -V | tail -1)/apksigner sign \
--ks keystore.jks \
--ks-pass "pass:${{ secrets.KEY_STORE_PASSWORD }}" \
--key-pass "pass:${{ secrets.KEY_PASSWORD }}" \
--out app/build/outputs/apk/release/app-release.apk \
app/build/outputs/apk/release/app-release-unsigned.apk
- name: Verify APK signature
run: |
$ANDROID_HOME/build-tools/$(ls $ANDROID_HOME/build-tools | sort -V | tail -1)/apksigner verify \
app/build/outputs/apk/release/app-release.apk
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.event.inputs.tag }}
name: ${{ github.event.inputs.release_name }}
body: ${{ github.event.inputs.release_notes }}
files: |
app/build/outputs/apk/release/app-release.apk

12
README.md Normal file
View File

@@ -0,0 +1,12 @@
# zaprett
## О приложении
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
### Данное приложение является ремейком [приложения](https://github.com/egor-white/zaprett-app) от [egor-white](https://github.com/egor-white)
На данный момент приложение умеет:
* Включать, выключать и перезапускать модуль
* Работа с листами (добавление, включение и выключение)
* Авто обновление приложения
## Скриншоты:
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300">

View File

@@ -12,8 +12,8 @@ android {
applicationId = "com.cherret.zaprett"
minSdk = 30
targetSdk = 35
versionCode = 1
versionName = "1.0"
versionCode = 3
versionName = "1.2"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
@@ -46,6 +46,8 @@ dependencies {
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("com.squareup.moshi:moshi-kotlin:1.15.2")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)

View File

@@ -7,6 +7,9 @@
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
@@ -17,6 +20,15 @@
android:supportsRtl="true"
android:theme="@style/Theme.Zaprett"
tools:targetApi="31">
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
<activity
android:name=".MainActivity"
android:exported="true"

View File

@@ -23,8 +23,10 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -37,7 +39,7 @@ import com.cherret.zaprett.ui.screens.HomeScreen
import com.cherret.zaprett.ui.screens.HostsScreen
import com.cherret.zaprett.ui.screens.SettingsScreen
import com.cherret.zaprett.ui.theme.ZaprettTheme
import com.topjohnwu.superuser.Shell
import androidx.core.content.edit
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) {
object home : Screen("home", R.string.title_home, Icons.Default.Home)
@@ -48,18 +50,21 @@ val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
Shell.setDefaultBuilder(Shell.Builder.create()
.setTimeout(10))
enableEdgeToEdge()
setContent {
ZaprettTheme {
val sharedPreferences = remember { getSharedPreferences("settings", MODE_PRIVATE) }
var showPermissionDialog by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) }
BottomBar()
if (!Environment.isExternalStorageManager()) {
permissionDialog()
if (showPermissionDialog) {
PermissionDialog { showPermissionDialog = false }
}
if (showWelcomeDialog) {
WelcomeDialog {
sharedPreferences.edit() { putBoolean("welcome_dialog", false) }
showWelcomeDialog = false
}
if (sharedPreferences.getBoolean("welcome_dialog", true)) {
welcomeDialog()
}
}
}
@@ -110,69 +115,39 @@ class MainActivity : ComponentActivity() {
}
@Composable
fun welcomeDialog() {
val sharedPreferences =
LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
val editor = sharedPreferences.edit()
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
fun WelcomeDialog(onDismiss: () -> Unit) {
AlertDialog(
title = {
Text(text = stringResource(R.string.app_name))
},
text = {
Text(text = stringResource(R.string.text_welcome))
},
onDismissRequest = {
editor.putBoolean("welcome_dialog", false).apply()
openDialog.value = false
},
title = { Text(text = stringResource(R.string.app_name)) },
text = { Text(text = stringResource(R.string.text_welcome)) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = {
editor.putBoolean("welcome_dialog", false).apply()
openDialog.value = false
}
) {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.btn_continue))
}
},
)
}
)
}
@Composable
fun permissionDialog() {
val openDialog = remember { mutableStateOf(true) }
if (openDialog.value) {
fun PermissionDialog(onDismiss: () -> Unit) {
val context = LocalContext.current
AlertDialog(
title = {
Text(text = stringResource(R.string.error_no_storage_title))
},
text = {
Text(text = stringResource(R.string.error_no_storage_message))
},
onDismissRequest = {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", packageName, null)
intent.setData(uri)
startActivity(intent)
openDialog.value = false
},
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
text = { Text(text = stringResource(R.string.error_no_storage_message)) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(
onClick = {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
val uri = Uri.fromParts("package", packageName, null)
intent.setData(uri)
startActivity(intent)
openDialog.value = false
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
onDismiss()
}
) {
Text(stringResource(R.string.btn_continue))
}
},
}
)
}
}
}

View File

@@ -9,38 +9,40 @@ import java.io.FileOutputStream
import java.io.IOException
import java.util.Properties
fun checkRoot(): Boolean {
val result = Shell.cmd("ls /").exec()
Shell.getShell().close()
return result.isSuccess
fun checkRoot(callback: (Boolean) -> Unit) {
Shell.cmd("ls /").submit { result ->
callback(result.isSuccess)
}
}
fun checkModuleInstallation(): Boolean {
val result = Shell.cmd("zaprett").exec()
Shell.getShell().close()
return result.out.toString().contains("zaprett")
fun checkModuleInstallation(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett").submit { result ->
callback(result.out.toString().contains("zaprett"))
}
}
fun getStatus(): Boolean {
val result = Shell.cmd("zaprett status").exec()
Shell.getShell().close()
return result.out.toString().contains("working")
fun getStatus(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett status").submit { result ->
callback(result.out.toString().contains("working"))
}
}
fun startService() {
Shell.cmd("zaprett start").exec()
Shell.getShell().close()
fun startService(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett start").submit { result ->
callback(result.isSuccess)
}
}
fun stopService() {
Shell.cmd("zaprett stop").exec()
Shell.getShell().close()
fun stopService(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett stop").submit { result ->
callback(result.isSuccess)
}
}
fun restartService() {
Shell.cmd("zaprett restart").exec()
Shell.getShell().close()
fun restartService(callback: (Boolean) -> Unit) {
Shell.cmd("zaprett restart").submit { result ->
callback(result.isSuccess)
}
}
fun getConfigFile(): File {

View File

@@ -0,0 +1,142 @@
package com.cherret.zaprett
import android.annotation.SuppressLint
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.Settings
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.Call
import okhttp3.Callback
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import okio.IOException
import java.io.File
val client = OkHttpClient()
fun getUpdate(context: Context, callback: (UpdateInfo?) -> Unit) {
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json").build()
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
val jsonAdapter = moshi.adapter(UpdateInfo::class.java)
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) {
throw IOException()
callback(null)
}
val jsonString = response.body!!.string()
val updateInfo = jsonAdapter.fromJson(jsonString)
if (updateInfo != null) {
val packageVersionCode = context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
updateInfo.versionCode?.let { versionCode ->
if (versionCode > packageVersionCode)
callback(updateInfo)
}
}
}
}
})
}
fun getChangelog(changelogUrl: String, callback: (String?) -> Unit) {
val request = Request.Builder().url(changelogUrl).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)
return
}
val changelogText = response.body!!.string()
callback(changelogText)
}
}
})
}
fun download(context: Context, url: String): Long {
val fileName = url.substringAfterLast("/")
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
if (Environment.isExternalStorageManager()) {
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName)
if (file.exists()) {
file.delete()
}
}
val request = DownloadManager.Request(url.toUri()).apply {
setTitle(fileName)
setDescription("Загрузка $fileName")
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
}
return downloadManager.enqueue(request)
}
fun installApk(context: Context, uri: Uri) {
val file = File(uri.path!!)
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
if (context.packageManager.canRequestPackageInstalls()) {
val intent = Intent(Intent.ACTION_VIEW).apply {
setDataAndType(apkUri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
context.startActivity(intent)
}
else {
val packageUri = Uri.fromParts("package", context.packageName, null)
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri)
context.startActivity(intent)
}
}
fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
val receiver = object : BroadcastReceiver() {
@SuppressLint("Range")
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadId) return
val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager ?: return
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
if (cursor.moveToFirst() && cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
context.unregisterReceiver(this)
onDownloaded(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).toUri())
}
}
}
}
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
} else {
ContextCompat.registerReceiver(context, receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), ContextCompat.RECEIVER_EXPORTED)
}
}
data class UpdateInfo(
val version: String?,
val versionCode: Int?,
val downloadUrl: String?,
val changelogUrl: String?,
)

View File

@@ -12,6 +12,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.RestartAlt
import androidx.compose.material.icons.filled.Stop
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.FilledTonalButton
@@ -21,12 +22,15 @@ 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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
@@ -40,7 +44,12 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.cherret.zaprett.R
import com.cherret.zaprett.download
import com.cherret.zaprett.getChangelog
import com.cherret.zaprett.getStatus
import com.cherret.zaprett.getUpdate
import com.cherret.zaprett.installApk
import com.cherret.zaprett.registerDownloadListener
import com.cherret.zaprett.restartService
import com.cherret.zaprett.startService
import com.cherret.zaprett.stopService
@@ -52,17 +61,35 @@ fun HomeScreen() {
val context = LocalContext.current
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
val cardText = remember { mutableStateOf(R.string.status_not_availible) }
val changeLog = remember { mutableStateOf<String?>(null) }
val updateAvailable = remember {mutableStateOf(false)}
val downloadUrl = remember { mutableStateOf<String?>(null) }
var showUpdateDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(Unit) {
if (sharedPreferences.getBoolean("auto_update", true)) {
getUpdate(context) {
if (it != null) {
downloadUrl.value = it.downloadUrl.toString()
getChangelog(it.changelogUrl.toString()) {
changeLog.value = it
}
updateAvailable.value = true
}
}
}
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
if (getStatus()) {
getStatus {
if (it) {
cardText.value = R.string.status_enabled
}
else {
cardText.value = R.string.status_disabled
}
}
}
}
Scaffold(
topBar = {
@@ -111,6 +138,36 @@ fun HomeScreen() {
textAlign = TextAlign.Center,
)
}
if (updateAvailable.value) {
ElevatedCard(
elevation = CardDefaults.cardElevation(
defaultElevation = 6.dp
),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
),
modifier = Modifier
.fillMaxWidth()
.padding(start = 10.dp, top = 10.dp, end = 10.dp)
.size(width = 140.dp, height = 70.dp),
onClick = {
showUpdateDialog = true
}
)
{
Text(
text = stringResource(R.string.update_available),
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
textAlign = TextAlign.Center,
)
}
}
if (showUpdateDialog) {
UpdateDialog(context, downloadUrl.value.toString(), changeLog.value.toString(), onDismiss = { showUpdateDialog = false })
}
FilledTonalButton(
onClick = { onBtnStartService(context, snackbarHostState, scope) },
modifier = Modifier
@@ -164,13 +221,16 @@ fun HomeScreen() {
fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
if (sharedPreferences.getBoolean("use_module", false)) {
if (getStatus()) {
getStatus {
if (it) {
cardText.value = R.string.status_enabled
}
else {
cardText.value = R.string.status_disabled
}
}
}
else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
@@ -180,14 +240,16 @@ fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
if (getStatus()) {
getStatus {
if (it) {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
}
} else {
startService()
startService {}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.btn_start_service))
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
}
}
}
} else {
@@ -199,10 +261,11 @@ fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, sc
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
if (getStatus()) {
stopService()
getStatus {
if (it) {
stopService{}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.btn_stop_service))
snackbarHostState.showSnackbar(context.getString(R.string.snack_stopping_service))
}
}
else {
@@ -211,6 +274,7 @@ fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, sco
}
}
}
}
else {
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
@@ -220,7 +284,7 @@ fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, sco
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
restartService()
restartService{}
scope.launch {
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
}
@@ -231,3 +295,30 @@ fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope:
}
}
}
@Composable
fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, onDismiss: () -> Unit) {
AlertDialog(
title = { Text(text = stringResource(R.string.update_available)) },
text = { Text(text = changeLog) },
onDismissRequest = onDismiss,
confirmButton = {
TextButton(onClick = {
onDismiss()
val downloadId = download(context, downloadUrl)
registerDownloadListener(context, downloadId) { uri ->
installApk(context, uri)
}
}) {
Text(stringResource(R.string.btn_continue))
}
},
dismissButton = {
TextButton(onClick = onDismiss) {
Text(stringResource(R.string.btn_dismiss))
}
}
)
}

View File

@@ -184,7 +184,7 @@ fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
launcher.launch(arrayOf("*/*"))
}
fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {// AI Generated
if (Environment.isExternalStorageManager()) {
if (uri == null) return
val contentResolver = context.contentResolver

View File

@@ -46,6 +46,7 @@ fun SettingsScreen() {
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
val openNoRootDialog = remember { mutableStateOf(false) }
val openNoModuleDialog = remember { mutableStateOf(false) }
showNoRootDialog(openNoRootDialog)
@@ -98,7 +99,19 @@ fun SettingsScreen() {
)
Switch(
checked = useModule.value,
onCheckedChange = { if (useModule(context, it, updateOnBoot, openNoRootDialog, openNoModuleDialog)) useModule.value = it}
onCheckedChange = { isChecked ->
useModule(
context,
isChecked,
updateOnBoot,
openNoRootDialog,
openNoModuleDialog
) {
if (it) {
useModule.value = isChecked
}
}
}
)
}
Row(
@@ -131,33 +144,52 @@ fun SettingsScreen() {
onCheckedChange = { if (autoRestart(context, it)) autoRestart.value = it;}
)
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = stringResource(R.string.btn_autoupdate),
modifier = Modifier.weight(1f)
)
Switch(
checked = autoUpdate.value,
onCheckedChange = { autoUpdate.value = it; editor.putBoolean("auto_update", it).apply()}
)
}
}
}
}
)
}
fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>): Boolean {
fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit): Boolean {
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val editor = sharedPreferences.edit()
if (checked) {
if (checkRoot()) {
if (checkModuleInstallation()) {
checkRoot {
if (it) {
checkModuleInstallation {
if (it) {
editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply()
updateOnBoot.value = true
return true
callback(true)
}
else {
openNoModuleDialog.value = true
}
} else {
openNoRootDialog.value = true
}
}
else {
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false)
.apply()
openNoRootDialog.value = true
}
}
}
else {
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false).apply()
updateOnBoot.value = false
return true
}
return false

View File

@@ -1,6 +1,5 @@
package com.cherret.zaprett.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme

View File

@@ -4,6 +4,7 @@
<string name="title_hosts">Хосты</string>
<string name="title_settings">Настройки</string>
<string name="btn_continue">Продолжить</string>
<string name="btn_dismiss">Отмена</string>
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
<string name="btn_use_root">Использовать модуль</string>
<string name="error_root_title">Root не получен</string>
@@ -17,11 +18,13 @@
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
<string name="update_available">Доступно новое обновление!</string>
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
<string name="btn_start_service">Запустить сервис</string>
<string name="btn_stop_service">Остановить сервис</string>
<string name="btn_restart_service">Перезапустить сервис</string>
<string name="btn_autorestart">Переодически перезапускать сервис</string>
<string name="btn_autoupdate">Авто обновление</string>
<string name="snack_already_started">Сервис уже запущен.</string>
<string name="snack_starting_service">Запускаем сервис...</string>
<string name="snack_no_service">Сервис не запущен</string>

View File

@@ -4,6 +4,7 @@
<string name="title_hosts">Hosts</string>
<string name="title_settings">Settings</string>
<string name="btn_continue">Continue</string>
<string name="btn_dismiss">Dismiss</string>
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
<string name="btn_use_root">Use module</string>
<string name="error_root_title">Can\'t get root</string>
@@ -17,11 +18,13 @@
<string name="status_enabled">zaprett service is working. Tap to update</string>
<string name="status_disabled">zaprett service disabled.</string>
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
<string name="update_available">New update available!</string>
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
<string name="btn_start_service">Start service</string>
<string name="btn_stop_service">Stop service</string>
<string name="btn_restart_service">Restart service</string>
<string name="btn_autorestart">Restart service periodicaly</string>
<string name="btn_autoupdate">Autoupdate</string>
<string name="snack_already_started">Service already started.</string>
<string name="snack_starting_service">Starting service...</string>
<string name="snack_no_service">Service is not launched</string>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<external-path name="downloads" path="Download/" />
</paths>

2
changelog.md Normal file
View File

@@ -0,0 +1,2 @@
Исправление ошибок
Система обновлений

BIN
images/1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

BIN
images/2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

BIN
images/3.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

6
update.json Normal file
View File

@@ -0,0 +1,6 @@
{
"version": "1.2",
"versionCode": 3,
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1.2.0/app-release.apk",
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
}