mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 21:49:38 +05:00
Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e5b96b562 | ||
|
|
acf579da89 | ||
|
|
180922699c | ||
|
|
3fdd68ca55 | ||
|
|
20bdcac931 | ||
|
|
76e7fb8310 | ||
|
|
4ed064ff9c | ||
|
|
b758decd6b | ||
|
|
4a74050ec6 | ||
|
|
d9aa02a436 | ||
|
|
18b3c0e3c6 | ||
|
|
1870edfb5c | ||
|
|
9b138d55ee | ||
|
|
03e1ea4f06 | ||
|
|
9e95ba9922 | ||
|
|
aac80da9a7 | ||
|
|
2b611ce8cd | ||
|
|
6967ee286c | ||
|
|
83737d5df9 | ||
|
|
a05c6af2c1 | ||
|
|
7b7f94f0e2 | ||
|
|
8367eac9bc | ||
|
|
e95792b8c9 | ||
|
|
7352943bb3 | ||
|
|
b4da6bda5a | ||
|
|
4ecc9a40d4 | ||
|
|
96dc70473e | ||
|
|
b05616d7ef | ||
|
|
316bdea986 | ||
|
|
de22bb048c | ||
|
|
56d3c95f07 | ||
|
|
df4e7f9658 | ||
|
|
3c44a449c3 | ||
|
|
e46b0e7d4f | ||
|
|
136340949d | ||
|
|
894899bfc9 |
66
.github/workflows/workflow.yml
vendored
Normal file
66
.github/workflows/workflow.yml
vendored
Normal 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
12
README.md
Normal 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">
|
||||||
@@ -2,6 +2,8 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
id("com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -12,8 +14,8 @@ android {
|
|||||||
applicationId = "com.cherret.zaprett"
|
applicationId = "com.cherret.zaprett"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = 6
|
||||||
versionName = "1.0"
|
versionName = "1.5"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,11 @@ dependencies {
|
|||||||
implementation("androidx.navigation:navigation-compose:2.8.9")
|
implementation("androidx.navigation:navigation-compose:2.8.9")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||||
implementation ("com.github.topjohnwu.libsu:core:6.0.0")
|
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(platform("com.google.firebase:firebase-bom:33.12.0"))
|
||||||
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
|
implementation("com.google.firebase:firebase-crashlytics")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
29
app/google-services.json
Normal file
29
app/google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "1005804036856",
|
||||||
|
"project_id": "zaprett-app",
|
||||||
|
"storage_bucket": "zaprett-app.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1005804036856:android:e7db5546b8bb4daf91510d",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.cherret.zaprett"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyASt83pAMxMI4txNAXaHDpX1R9crfoZAMk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
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
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -17,6 +20,15 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Zaprett"
|
android:theme="@style/Theme.Zaprett"
|
||||||
tools:targetApi="31">
|
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
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
|
|||||||
@@ -23,11 +23,14 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
@@ -37,7 +40,9 @@ import com.cherret.zaprett.ui.screens.HomeScreen
|
|||||||
import com.cherret.zaprett.ui.screens.HostsScreen
|
import com.cherret.zaprett.ui.screens.HostsScreen
|
||||||
import com.cherret.zaprett.ui.screens.SettingsScreen
|
import com.cherret.zaprett.ui.screens.SettingsScreen
|
||||||
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.google.firebase.Firebase
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.analytics
|
||||||
|
|
||||||
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) {
|
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)
|
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
||||||
@@ -46,20 +51,25 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon:
|
|||||||
}
|
}
|
||||||
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
|
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
firebaseAnalytics = Firebase.analytics
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
||||||
.setTimeout(10))
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ZaprettTheme {
|
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()
|
BottomBar()
|
||||||
if (!Environment.isExternalStorageManager()) {
|
if (showPermissionDialog) {
|
||||||
permissionDialog()
|
PermissionDialog { showPermissionDialog = false }
|
||||||
}
|
}
|
||||||
if (sharedPreferences.getBoolean("welcome_dialog", true)) {
|
if (showWelcomeDialog) {
|
||||||
welcomeDialog()
|
WelcomeDialog {
|
||||||
|
sharedPreferences.edit() { putBoolean("welcome_dialog", false) }
|
||||||
|
showWelcomeDialog = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -110,69 +120,39 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun welcomeDialog() {
|
fun WelcomeDialog(onDismiss: () -> Unit) {
|
||||||
val sharedPreferences =
|
AlertDialog(
|
||||||
LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
title = { Text(text = stringResource(R.string.app_name)) },
|
||||||
val editor = sharedPreferences.edit()
|
text = { Text(text = stringResource(R.string.text_welcome)) },
|
||||||
val openDialog = remember { mutableStateOf(true) }
|
onDismissRequest = onDismiss,
|
||||||
if (openDialog.value) {
|
confirmButton = {
|
||||||
AlertDialog(
|
TextButton(onClick = onDismiss) {
|
||||||
title = {
|
Text(stringResource(R.string.btn_continue))
|
||||||
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
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
editor.putBoolean("welcome_dialog", false).apply()
|
|
||||||
openDialog.value = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.btn_continue))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun permissionDialog() {
|
fun PermissionDialog(onDismiss: () -> Unit) {
|
||||||
val openDialog = remember { mutableStateOf(true) }
|
val context = LocalContext.current
|
||||||
if (openDialog.value) {
|
AlertDialog(
|
||||||
AlertDialog(
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
title = {
|
text = { Text(text = stringResource(R.string.error_no_storage_message)) },
|
||||||
Text(text = stringResource(R.string.error_no_storage_title))
|
onDismissRequest = onDismiss,
|
||||||
},
|
confirmButton = {
|
||||||
text = {
|
TextButton(
|
||||||
Text(text = stringResource(R.string.error_no_storage_message))
|
onClick = {
|
||||||
},
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||||
onDismissRequest = {
|
val uri = Uri.fromParts("package", context.packageName, null)
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
intent.data = uri
|
||||||
val uri = Uri.fromParts("package", packageName, null)
|
context.startActivity(intent)
|
||||||
intent.setData(uri)
|
onDismiss()
|
||||||
startActivity(intent)
|
|
||||||
openDialog.value = false
|
|
||||||
},
|
|
||||||
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
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.btn_continue))
|
|
||||||
}
|
}
|
||||||
},
|
) {
|
||||||
)
|
Text(stringResource(R.string.btn_continue))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,38 +9,40 @@ import java.io.FileOutputStream
|
|||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.util.Properties
|
import java.util.Properties
|
||||||
|
|
||||||
fun checkRoot(): Boolean {
|
fun checkRoot(callback: (Boolean) -> Unit) {
|
||||||
val result = Shell.cmd("ls /").exec()
|
Shell.cmd("ls /").submit { result ->
|
||||||
Shell.getShell().close()
|
callback(result.isSuccess)
|
||||||
return result.isSuccess
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun checkModuleInstallation(): Boolean {
|
fun checkModuleInstallation(callback: (Boolean) -> Unit) {
|
||||||
val result = Shell.cmd("zaprett").exec()
|
Shell.cmd("zaprett").submit { result ->
|
||||||
Shell.getShell().close()
|
callback(result.out.toString().contains("zaprett"))
|
||||||
return result.out.toString().contains("zaprett")
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getStatus(callback: (Boolean) -> Unit) {
|
||||||
fun getStatus(): Boolean {
|
Shell.cmd("zaprett status").submit { result ->
|
||||||
val result = Shell.cmd("zaprett status").exec()
|
callback(result.out.toString().contains("working"))
|
||||||
Shell.getShell().close()
|
}
|
||||||
return result.out.toString().contains("working")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun startService() {
|
fun startService(callback: (Boolean) -> Unit) {
|
||||||
Shell.cmd("zaprett start").exec()
|
Shell.cmd("zaprett start").submit { result ->
|
||||||
Shell.getShell().close()
|
callback(result.isSuccess)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stopService() {
|
fun stopService(callback: (Boolean) -> Unit) {
|
||||||
Shell.cmd("zaprett stop").exec()
|
Shell.cmd("zaprett stop").submit { result ->
|
||||||
Shell.getShell().close()
|
callback(result.isSuccess)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartService() {
|
fun restartService(callback: (Boolean) -> Unit) {
|
||||||
Shell.cmd("zaprett restart").exec()
|
Shell.cmd("zaprett restart").submit { result ->
|
||||||
Shell.getShell().close()
|
callback(result.isSuccess)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getConfigFile(): File {
|
fun getConfigFile(): File {
|
||||||
|
|||||||
142
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal file
142
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal 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?,
|
||||||
|
)
|
||||||
@@ -2,6 +2,11 @@ package com.cherret.zaprett.ui.screens
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -12,6 +17,7 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
@@ -21,12 +27,16 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
@@ -40,7 +50,12 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
|
import com.cherret.zaprett.download
|
||||||
|
import com.cherret.zaprett.getChangelog
|
||||||
import com.cherret.zaprett.getStatus
|
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.restartService
|
||||||
import com.cherret.zaprett.startService
|
import com.cherret.zaprett.startService
|
||||||
import com.cherret.zaprett.stopService
|
import com.cherret.zaprett.stopService
|
||||||
@@ -51,17 +66,37 @@ import kotlinx.coroutines.launch
|
|||||||
fun HomeScreen() {
|
fun HomeScreen() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
|
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
|
||||||
val cardText = remember { mutableStateOf(R.string.status_not_availible) }
|
val cardText = remember { mutableIntStateOf(R.string.status_not_availible) }
|
||||||
|
val changeLog = remember { mutableStateOf<String?>(null) }
|
||||||
|
val newVersion = 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 scope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
LaunchedEffect(Unit) {
|
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
|
||||||
|
}
|
||||||
|
newVersion.value = it.version?.toString()
|
||||||
|
updateAvailable.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
|
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
|
||||||
if (getStatus()) {
|
getStatus {
|
||||||
cardText.value = R.string.status_enabled
|
if (it) {
|
||||||
}
|
cardText.value = R.string.status_enabled
|
||||||
else {
|
}
|
||||||
cardText.value = R.string.status_disabled
|
else {
|
||||||
|
cardText.value = R.string.status_disabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -111,6 +146,40 @@ fun HomeScreen() {
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = updateAvailable.value,
|
||||||
|
enter = fadeIn() + expandVertically(),
|
||||||
|
exit = shrinkVertically() + fadeOut()
|
||||||
|
) {
|
||||||
|
ElevatedCard(
|
||||||
|
elevation = CardDefaults.cardElevation(
|
||||||
|
defaultElevation = 6.dp
|
||||||
|
),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
|
),
|
||||||
|
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(), newVersion, onDismiss = { showUpdateDialog = false })
|
||||||
|
}
|
||||||
FilledTonalButton(
|
FilledTonalButton(
|
||||||
onClick = { onBtnStartService(context, snackbarHostState, scope) },
|
onClick = { onBtnStartService(context, snackbarHostState, scope) },
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -164,12 +233,15 @@ fun HomeScreen() {
|
|||||||
fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
if (getStatus()) {
|
getStatus {
|
||||||
cardText.value = R.string.status_enabled
|
if (it) {
|
||||||
}
|
cardText.value = R.string.status_enabled
|
||||||
else {
|
}
|
||||||
cardText.value = R.string.status_disabled
|
else {
|
||||||
|
cardText.value = R.string.status_disabled
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -180,14 +252,16 @@ fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState
|
|||||||
|
|
||||||
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
||||||
if (getStatus()) {
|
getStatus {
|
||||||
scope.launch {
|
if (it) {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
|
scope.launch {
|
||||||
}
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
|
||||||
} else {
|
}
|
||||||
startService()
|
} else {
|
||||||
scope.launch {
|
startService {}
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.btn_start_service))
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -199,15 +273,17 @@ fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, sc
|
|||||||
|
|
||||||
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
||||||
if (getStatus()) {
|
getStatus {
|
||||||
stopService()
|
if (it) {
|
||||||
scope.launch {
|
stopService{}
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.btn_stop_service))
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_stopping_service))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
else {
|
||||||
else {
|
scope.launch {
|
||||||
scope.launch {
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service))
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service))
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -220,7 +296,7 @@ fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, sco
|
|||||||
|
|
||||||
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
||||||
restartService()
|
restartService{}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||||
}
|
}
|
||||||
@@ -231,3 +307,28 @@ fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVersion: MutableState<String?>, onDismiss: () -> Unit) {
|
||||||
|
AlertDialog(
|
||||||
|
title = { Text(text = stringResource(R.string.update_available)) },
|
||||||
|
text = { Text(text = "${stringResource(R.string.alert_version)}: ${context.packageManager.getPackageInfo(context.packageName, 0).versionName} —> ${newVersion.value}\n${stringResource(R.string.alert_changelog)}:\n$changeLog") },
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDismiss()
|
||||||
|
val downloadId = download(context, downloadUrl)
|
||||||
|
registerDownloadListener(context, downloadId) { uri ->
|
||||||
|
installApk(context, uri)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_update))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.btn_dismiss))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -8,9 +8,12 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -18,15 +21,20 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.SnackbarResult
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
@@ -37,6 +45,8 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshots.Snapshot
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
@@ -47,6 +57,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
@@ -55,6 +66,7 @@ import com.cherret.zaprett.enableList
|
|||||||
import com.cherret.zaprett.getActiveLists
|
import com.cherret.zaprett.getActiveLists
|
||||||
import com.cherret.zaprett.getAllLists
|
import com.cherret.zaprett.getAllLists
|
||||||
import com.cherret.zaprett.getZaprettPath
|
import com.cherret.zaprett.getZaprettPath
|
||||||
|
import com.cherret.zaprett.restartService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -85,7 +97,6 @@ fun HostsScreen() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
@@ -125,14 +136,16 @@ fun HostsScreen() {
|
|||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
LazyColumn (
|
||||||
|
contentPadding = PaddingValues(bottom = 25.dp)
|
||||||
|
){
|
||||||
items(allLists) { item ->
|
items(allLists) { item ->
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
elevation = CardDefaults.cardElevation(
|
elevation = CardDefaults.cardElevation(
|
||||||
defaultElevation = 6.dp
|
defaultElevation = 6.dp
|
||||||
),
|
),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
),
|
),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -158,11 +171,74 @@ fun HostsScreen() {
|
|||||||
disableList(item)
|
disableList(item)
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack))
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
context.getString(R.string.pls_restart_snack),
|
||||||
|
actionLabel = context.getString(R.string.btn_restart_service)
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
restartService {}
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.snack_reload
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnackbarResult.Dismissed -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
HorizontalDivider(thickness = Dp.Hairline)
|
||||||
|
Row (modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.End) {
|
||||||
|
FilledTonalButton(
|
||||||
|
onClick = {
|
||||||
|
if (deleteHost(item)) {
|
||||||
|
allLists = getAllLists()
|
||||||
|
activeLists = getActiveLists()
|
||||||
|
checked.clear()
|
||||||
|
allLists.forEach { list ->
|
||||||
|
checked[list] = activeLists.contains(list)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scope.launch {
|
||||||
|
val result = snackbarHostState.showSnackbar(
|
||||||
|
context.getString(R.string.pls_restart_snack),
|
||||||
|
actionLabel = context.getString(R.string.btn_restart_service)
|
||||||
|
)
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
restartService {}
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
context.getString(
|
||||||
|
R.string.snack_reload
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnackbarResult.Dismissed -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 5.dp, end = 5.dp),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = stringResource(R.string.btn_remove_host),
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.btn_remove_host)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -181,10 +257,10 @@ fun HostsScreen() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
||||||
launcher.launch(arrayOf("*/*"))
|
launcher.launch(arrayOf("text/plain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
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 (Environment.isExternalStorageManager()) {
|
||||||
if (uri == null) return
|
if (uri == null) return
|
||||||
val contentResolver = context.contentResolver
|
val contentResolver = context.contentResolver
|
||||||
@@ -200,7 +276,16 @@ fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack))
|
val result = snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack), actionLabel = context.getString(R.string.btn_restart_service))
|
||||||
|
when (result) {
|
||||||
|
SnackbarResult.ActionPerformed -> {
|
||||||
|
restartService{}
|
||||||
|
scope.launch {
|
||||||
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SnackbarResult.Dismissed -> {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
e.printStackTrace()
|
e.printStackTrace()
|
||||||
@@ -208,3 +293,12 @@ fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHos
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun deleteHost(item: String): Boolean {
|
||||||
|
val hostFile = File(item)
|
||||||
|
if (hostFile.exists()) {
|
||||||
|
hostFile.delete()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ fun SettingsScreen() {
|
|||||||
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
|
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", false)) }
|
||||||
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
|
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
|
||||||
|
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
|
||||||
val openNoRootDialog = remember { mutableStateOf(false) }
|
val openNoRootDialog = remember { mutableStateOf(false) }
|
||||||
val openNoModuleDialog = remember { mutableStateOf(false) }
|
val openNoModuleDialog = remember { mutableStateOf(false) }
|
||||||
showNoRootDialog(openNoRootDialog)
|
showNoRootDialog(openNoRootDialog)
|
||||||
@@ -98,7 +99,19 @@ fun SettingsScreen() {
|
|||||||
)
|
)
|
||||||
Switch(
|
Switch(
|
||||||
checked = useModule.value,
|
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(
|
Row(
|
||||||
@@ -131,34 +144,53 @@ fun SettingsScreen() {
|
|||||||
onCheckedChange = { if (autoRestart(context, it)) autoRestart.value = it;}
|
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 sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val editor = sharedPreferences.edit()
|
val editor = sharedPreferences.edit()
|
||||||
if (checked) {
|
if (checked) {
|
||||||
if (checkRoot()) {
|
checkRoot {
|
||||||
if (checkModuleInstallation()) {
|
if (it) {
|
||||||
editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply()
|
checkModuleInstallation {
|
||||||
updateOnBoot.value = true
|
if (it) {
|
||||||
return true
|
editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply()
|
||||||
|
updateOnBoot.value = true
|
||||||
|
callback(true)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
openNoModuleDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
openNoModuleDialog.value = true
|
openNoRootDialog.value = true
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
openNoRootDialog.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false)
|
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false).apply()
|
||||||
.apply()
|
updateOnBoot.value = false
|
||||||
return true
|
callback(true)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.cherret.zaprett.ui.theme
|
package com.cherret.zaprett.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<string name="title_hosts">Хосты</string>
|
<string name="title_hosts">Хосты</string>
|
||||||
<string name="title_settings">Настройки</string>
|
<string name="title_settings">Настройки</string>
|
||||||
<string name="btn_continue">Продолжить</string>
|
<string name="btn_continue">Продолжить</string>
|
||||||
|
<string name="btn_update">Обновить</string>
|
||||||
|
<string name="btn_dismiss">Отмена</string>
|
||||||
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
||||||
<string name="btn_use_root">Использовать модуль</string>
|
<string name="btn_use_root">Использовать модуль</string>
|
||||||
<string name="error_root_title">Root не получен</string>
|
<string name="error_root_title">Root не получен</string>
|
||||||
@@ -17,11 +19,16 @@
|
|||||||
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
||||||
|
<string name="update_available">Доступно новое обновление!</string>
|
||||||
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
||||||
<string name="btn_start_service">Запустить сервис</string>
|
<string name="btn_start_service">Запустить сервис</string>
|
||||||
<string name="btn_stop_service">Остановить сервис</string>
|
<string name="btn_stop_service">Остановить сервис</string>
|
||||||
<string name="btn_restart_service">Перезапустить сервис</string>
|
<string name="btn_restart_service">Перезапустить сервис</string>
|
||||||
|
<string name="btn_remove_host">Удалить</string>
|
||||||
<string name="btn_autorestart">Переодически перезапускать сервис</string>
|
<string name="btn_autorestart">Переодически перезапускать сервис</string>
|
||||||
|
<string name="btn_autoupdate">Авто обновление</string>
|
||||||
|
<string name="alert_version">Версия</string>
|
||||||
|
<string name="alert_changelog">Список изменений</string>
|
||||||
<string name="snack_already_started">Сервис уже запущен.</string>
|
<string name="snack_already_started">Сервис уже запущен.</string>
|
||||||
<string name="snack_starting_service">Запускаем сервис...</string>
|
<string name="snack_starting_service">Запускаем сервис...</string>
|
||||||
<string name="snack_no_service">Сервис не запущен</string>
|
<string name="snack_no_service">Сервис не запущен</string>
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<string name="title_hosts">Hosts</string>
|
<string name="title_hosts">Hosts</string>
|
||||||
<string name="title_settings">Settings</string>
|
<string name="title_settings">Settings</string>
|
||||||
<string name="btn_continue">Continue</string>
|
<string name="btn_continue">Continue</string>
|
||||||
|
<string name="btn_update">Update</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="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="btn_use_root">Use module</string>
|
||||||
<string name="error_root_title">Can\'t get root</string>
|
<string name="error_root_title">Can\'t get root</string>
|
||||||
@@ -17,11 +19,16 @@
|
|||||||
<string name="status_enabled">zaprett service is working. Tap to update</string>
|
<string name="status_enabled">zaprett service is working. Tap to update</string>
|
||||||
<string name="status_disabled">zaprett service disabled.</string>
|
<string name="status_disabled">zaprett service disabled.</string>
|
||||||
<string name="status_crashed">zaprett service crashed. Tap restart button below</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_update_on_boot">Update the status when the Home page is launched</string>
|
||||||
<string name="btn_start_service">Start service</string>
|
<string name="btn_start_service">Start service</string>
|
||||||
<string name="btn_stop_service">Stop service</string>
|
<string name="btn_stop_service">Stop service</string>
|
||||||
<string name="btn_restart_service">Restart service</string>
|
<string name="btn_restart_service">Restart service</string>
|
||||||
|
<string name="btn_remove_host">Remove</string>
|
||||||
<string name="btn_autorestart">Restart service periodicaly</string>
|
<string name="btn_autorestart">Restart service periodicaly</string>
|
||||||
|
<string name="btn_autoupdate">Autoupdate</string>
|
||||||
|
<string name="alert_version">Version</string>
|
||||||
|
<string name="alert_changelog">Changelog</string>
|
||||||
<string name="snack_already_started">Service already started.</string>
|
<string name="snack_already_started">Service already started.</string>
|
||||||
<string name="snack_starting_service">Starting service...</string>
|
<string name="snack_starting_service">Starting service...</string>
|
||||||
<string name="snack_no_service">Service is not launched</string>
|
<string name="snack_no_service">Service is not launched</string>
|
||||||
|
|||||||
4
app/src/main/res/xml/file_paths.xml
Normal file
4
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<paths>
|
||||||
|
<external-path name="downloads" path="Download/" />
|
||||||
|
</paths>
|
||||||
@@ -3,4 +3,6 @@ plugins {
|
|||||||
alias(libs.plugins.android.application) apply false
|
alias(libs.plugins.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
|
id("com.google.firebase.crashlytics") version "3.0.3" apply false
|
||||||
}
|
}
|
||||||
2
changelog.md
Normal file
2
changelog.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
Исправление багов
|
||||||
|
Добавление Firebase Analytics
|
||||||
BIN
images/1.png
Normal file
BIN
images/1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
BIN
images/2.png
Normal file
BIN
images/2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
BIN
images/3.png
Normal file
BIN
images/3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
6
update.json
Normal file
6
update.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"version": "1.4",
|
||||||
|
"versionCode": 5,
|
||||||
|
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_4_0/app-release.apk",
|
||||||
|
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user