mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-10 21:49:38 +05:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de65eaea5a | ||
|
|
e5f82030fd | ||
|
|
10eeb2c9d5 | ||
|
|
cb475ac8c9 | ||
|
|
66ff70e728 | ||
|
|
2aa26a9b35 | ||
|
|
9a3e8d8cee | ||
|
|
458741485b | ||
|
|
ca51cb1e81 | ||
|
|
542b14ddd2 | ||
|
|
c88687882e | ||
|
|
355987fddb | ||
|
|
b43b197233 | ||
|
|
eec81a32d9 | ||
|
|
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
|
||||
13
README.md
Normal file
13
README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# zaprett
|
||||
## О приложении
|
||||
Приложение разработано для работы с модулем [zaprett](https://github.com/egor-white/zaprett)
|
||||
### [Официальный Telegram-канал приложения](https://t.me/zaprett_module)
|
||||
### Данное приложение является ремейком [приложения](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.kotlin.android)
|
||||
alias(libs.plugins.kotlin.compose)
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -12,15 +14,16 @@ android {
|
||||
applicationId = "com.cherret.zaprett"
|
||||
minSdk = 30
|
||||
targetSdk = 35
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
versionCode = 9
|
||||
versionName = "1.8"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
isMinifyEnabled = true
|
||||
isShrinkResources = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
@@ -36,6 +39,7 @@ android {
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
buildConfig = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +50,11 @@ 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(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.lifecycle.runtime.ktx)
|
||||
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" />
|
||||
<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,16 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Zaprett"
|
||||
tools:targetApi="31">
|
||||
<meta-data android:name="firebase_analytics_collection_enabled" android:value="false" />
|
||||
<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"
|
||||
@@ -28,6 +41,16 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".QSTileService"
|
||||
android:exported="true"
|
||||
android:label="@string/qs_name"
|
||||
android:icon="@drawable/ic_launcher_monochrome"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -23,21 +23,27 @@ 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
|
||||
import androidx.core.content.edit
|
||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.cherret.zaprett.ui.screens.HomeScreen
|
||||
import com.cherret.zaprett.ui.screens.HostsRepoScreen
|
||||
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 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) {
|
||||
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
||||
@@ -45,21 +51,28 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon:
|
||||
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
|
||||
}
|
||||
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
|
||||
val hideNavBar = listOf("hosts_repo")
|
||||
class MainActivity : ComponentActivity() {
|
||||
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
||||
.setTimeout(10))
|
||||
firebaseAnalytics = Firebase.analytics
|
||||
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)) }
|
||||
firebaseAnalytics.setAnalyticsCollectionEnabled(sharedPreferences.getBoolean("send_firebase_analytics", true))
|
||||
BottomBar()
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
permissionDialog()
|
||||
if (showPermissionDialog) {
|
||||
PermissionDialog { showPermissionDialog = false }
|
||||
}
|
||||
if (sharedPreferences.getBoolean("welcome_dialog", true)) {
|
||||
welcomeDialog()
|
||||
if (showWelcomeDialog) {
|
||||
WelcomeDialog {
|
||||
sharedPreferences.edit() { putBoolean("welcome_dialog", false) }
|
||||
showWelcomeDialog = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,29 +83,31 @@ class MainActivity : ComponentActivity() {
|
||||
val navController = rememberNavController()
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar {
|
||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
topLevelRoutes.forEach { topLevelRoute ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
topLevelRoute.icon,
|
||||
contentDescription = stringResource(id = topLevelRoute.nameResId)
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
|
||||
selected = currentDestination?.route == topLevelRoute.route,
|
||||
onClick = {
|
||||
navController.navigate(topLevelRoute.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||
val currentDestination = navBackStackEntry?.destination
|
||||
if (currentDestination?.route !in hideNavBar) {
|
||||
NavigationBar {
|
||||
topLevelRoutes.forEach { topLevelRoute ->
|
||||
NavigationBarItem(
|
||||
icon = {
|
||||
Icon(
|
||||
topLevelRoute.icon,
|
||||
contentDescription = stringResource(id = topLevelRoute.nameResId)
|
||||
)
|
||||
},
|
||||
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
|
||||
selected = currentDestination?.route == topLevelRoute.route,
|
||||
onClick = {
|
||||
navController.navigate(topLevelRoute.route) {
|
||||
popUpTo(navController.graph.findStartDestination().id) {
|
||||
saveState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
launchSingleTop = true
|
||||
restoreState = true
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -103,76 +118,47 @@ class MainActivity : ComponentActivity() {
|
||||
Modifier.padding(innerPadding)
|
||||
) {
|
||||
composable(Screen.home.route) { HomeScreen() }
|
||||
composable(Screen.hosts.route) { HostsScreen() }
|
||||
composable(Screen.hosts.route) { HostsScreen(navController) }
|
||||
composable(Screen.settings.route) { SettingsScreen() }
|
||||
composable("hosts_repo") { HostsRepoScreen(navController) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun welcomeDialog() {
|
||||
val sharedPreferences =
|
||||
LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
val editor = sharedPreferences.edit()
|
||||
val openDialog = remember { mutableStateOf(true) }
|
||||
if (openDialog.value) {
|
||||
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
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
editor.putBoolean("welcome_dialog", false).apply()
|
||||
openDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
fun WelcomeDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.app_name)) },
|
||||
text = { Text(text = stringResource(R.string.text_welcome)) },
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun permissionDialog() {
|
||||
val openDialog = remember { mutableStateOf(true) }
|
||||
if (openDialog.value) {
|
||||
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
|
||||
},
|
||||
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))
|
||||
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 = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
val uri = Uri.fromParts("package", context.packageName, null)
|
||||
intent.data = uri
|
||||
context.startActivity(intent)
|
||||
onDismiss()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -77,7 +79,7 @@ fun getStartOnBoot(): Boolean {
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
} catch (_: IOException) {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -90,12 +92,12 @@ fun getZaprettPath(): String {
|
||||
FileInputStream(configFile).use { input ->
|
||||
props.load(input)
|
||||
}
|
||||
props.getProperty("zaprettdir", "/sdcard/zaprett")
|
||||
props.getProperty("zaprettdir", Environment.getExternalStorageDirectory().path + "/zaprett")
|
||||
} catch (e: IOException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
return "/sdcard/zaprett"
|
||||
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
||||
}
|
||||
|
||||
fun getAllLists(): Array<String> {
|
||||
|
||||
61
app/src/main/java/com/cherret/zaprett/QSTileService.kt
Normal file
61
app/src/main/java/com/cherret/zaprett/QSTileService.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
package com.cherret.zaprett
|
||||
|
||||
import android.service.quicksettings.Tile
|
||||
import android.service.quicksettings.TileService
|
||||
|
||||
class QSTileService: TileService() {
|
||||
override fun onTileAdded() {
|
||||
super.onTileAdded()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onStartListening() {
|
||||
super.onStartListening()
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onClick() {
|
||||
super.onClick()
|
||||
if (qsTile.state == Tile.STATE_INACTIVE) {
|
||||
qsTile.subtitle = getString(R.string.qs_starting)
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
startService {}
|
||||
}
|
||||
else {
|
||||
qsTile.subtitle = getString(R.string.qs_stopping)
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
stopService {}
|
||||
}
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
override fun onTileRemoved() {
|
||||
super.onTileRemoved()
|
||||
}
|
||||
|
||||
fun updateStatus() {
|
||||
if (getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
||||
getStatus {
|
||||
if (it) {
|
||||
qsTile.label = getString(R.string.qs_name)
|
||||
qsTile.subtitle = getString(R.string.qs_working)
|
||||
qsTile.state = Tile.STATE_ACTIVE
|
||||
qsTile.updateTile()
|
||||
} else {
|
||||
qsTile.label = getString(R.string.qs_name)
|
||||
qsTile.subtitle = getString(R.string.qs_not_working)
|
||||
qsTile.state = Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
qsTile.label = getString(R.string.qs_name)
|
||||
qsTile.subtitle = getString(R.string.qs_not_available)
|
||||
qsTile.state = Tile.STATE_UNAVAILABLE
|
||||
qsTile.updateTile()
|
||||
}
|
||||
}
|
||||
}
|
||||
101
app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt
Normal file
101
app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt
Normal file
@@ -0,0 +1,101 @@
|
||||
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 androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import com.squareup.moshi.Moshi
|
||||
import com.squareup.moshi.Types
|
||||
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
|
||||
import java.security.MessageDigest
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
fun getHostList(callback: (List<HostsInfo>?) -> Unit) {
|
||||
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-hosts-repo/refs/heads/main/hosts.json").build()
|
||||
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||
val type = Types.newParameterizedType(List::class.java, HostsInfo::class.java)
|
||||
val jsonAdapter = moshi.adapter<List<HostsInfo>>(type)
|
||||
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) {
|
||||
callback(updateInfo)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun registerDownloadListenerHost(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 dm = context?.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager ?: return
|
||||
val query = DownloadManager.Query().setFilterById(downloadId)
|
||||
dm.query(query)?.use { cursor ->
|
||||
if (cursor.moveToFirst()) {
|
||||
val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
||||
val uriString = cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
||||
if (uriString != null) {
|
||||
val uri = uriString.toUri()
|
||||
context.unregisterReceiver(this)
|
||||
onDownloaded(uri)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileSha256(file: File): String {
|
||||
val digest = MessageDigest.getInstance("SHA-256")
|
||||
file.inputStream().use { input ->
|
||||
val buffer = ByteArray(1024)
|
||||
var bytesRead: Int
|
||||
while (input.read(buffer).also { bytesRead = it } != -1) {
|
||||
digest.update(buffer, 0, bytesRead)
|
||||
}
|
||||
}
|
||||
return digest.digest().joinToString("") { "%02x".format(it) }
|
||||
}
|
||||
|
||||
data class HostsInfo(
|
||||
val name: String,
|
||||
val description: String,
|
||||
val hash: String,
|
||||
val url: String
|
||||
)
|
||||
139
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal file
139
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal file
@@ -0,0 +1,139 @@
|
||||
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
|
||||
|
||||
private val client = OkHttpClient()
|
||||
|
||||
fun getUpdate(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)
|
||||
updateInfo?.versionCode?.let { versionCode ->
|
||||
if (versionCode > BuildConfig.VERSION_CODE)
|
||||
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,9 +2,13 @@ package com.cherret.zaprett.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Context.MODE_PRIVATE
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
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.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
@@ -12,8 +16,10 @@ 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.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
@@ -21,16 +27,18 @@ 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.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
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
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
@@ -39,139 +47,179 @@ import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.cherret.zaprett.BuildConfig
|
||||
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
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen() {
|
||||
val context = LocalContext.current
|
||||
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 snackbarHostState = remember { SnackbarHostState() }
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
|
||||
if (getStatus()) {
|
||||
cardText.value = R.string.status_enabled
|
||||
if (sharedPreferences.getBoolean("auto_update", true)) {
|
||||
getUpdate() {
|
||||
if (it != null) {
|
||||
downloadUrl.value = it.downloadUrl.toString()
|
||||
getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log }
|
||||
newVersion.value = it.version?.toString()
|
||||
updateAvailable.value = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
cardText.value = R.string.status_disabled
|
||||
}
|
||||
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
|
||||
getStatus { isEnabled ->
|
||||
cardText.intValue = if (isEnabled) R.string.status_enabled else R.string.status_disabled
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
||||
rotate(degrees = -30f) {
|
||||
drawOval(
|
||||
color = primaryColor,
|
||||
size = Size(200f, 140f),
|
||||
topLeft = Offset(-20f, -20f)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
}
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.app_name),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||
content = { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 6.dp
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.primary,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
|
||||
.size(width = 240.dp, height = 150.dp),
|
||||
onClick = { onCardClick(context, cardText, snackbarHostState, scope) }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(cardText.value),
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnStartService(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, top = 8.dp, end = 5.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(R.string.btn_start_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.btn_start_service)
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnStopService(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, top = 8.dp, end = 5.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Stop,
|
||||
contentDescription = stringResource(R.string.btn_stop_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.btn_stop_service)
|
||||
)
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnRestart(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, top = 8.dp, end = 5.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
contentDescription = stringResource(R.string.btn_restart_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
stringResource(R.string.btn_restart_service)
|
||||
)
|
||||
Column(modifier = Modifier.padding(paddingValues)) {
|
||||
ServiceStatusCard(context, cardText, snackbarHostState, scope)
|
||||
UpdateCard(updateAvailable) { showUpdateDialog = true }
|
||||
if (showUpdateDialog) {
|
||||
UpdateDialog(context, downloadUrl.value.orEmpty(), changeLog.value.orEmpty(), newVersion) { showUpdateDialog = false }
|
||||
}
|
||||
ServiceControlButtons(context, snackbarHostState, scope)
|
||||
}
|
||||
},
|
||||
snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceStatusCard(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(6.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primary),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
|
||||
.size(width = 240.dp, height = 150.dp),
|
||||
onClick = { onCardClick(context, cardText, snackbarHostState, scope) }
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(cardText.value),
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun UpdateCard(updateAvailable: MutableState<Boolean>, onClick: () -> Unit) {
|
||||
AnimatedVisibility(
|
||||
visible = updateAvailable.value,
|
||||
enter = fadeIn() + expandVertically(),
|
||||
exit = shrinkVertically() + fadeOut()
|
||||
) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(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 = onClick
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.update_available),
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ServiceControlButtons(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnStartService(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.PlayArrow,
|
||||
contentDescription = stringResource(R.string.btn_start_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(stringResource(R.string.btn_start_service))
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnStopService(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Stop,
|
||||
contentDescription = stringResource(R.string.btn_stop_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(stringResource(R.string.btn_stop_service))
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = { onBtnRestart(context, snackbarHostState, scope) },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 5.dp, vertical = 8.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.RestartAlt,
|
||||
contentDescription = stringResource(R.string.btn_restart_service),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(stringResource(R.string.btn_restart_service))
|
||||
}
|
||||
}
|
||||
|
||||
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()) {
|
||||
cardText.value = R.string.status_enabled
|
||||
getStatus { isEnabled ->
|
||||
cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled
|
||||
}
|
||||
else {
|
||||
cardText.value = R.string.status_disabled
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
||||
}
|
||||
@@ -179,16 +227,17 @@ 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()) {
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
|
||||
}
|
||||
} else {
|
||||
startService()
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.btn_start_service))
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(
|
||||
if (isEnabled) R.string.snack_already_started else R.string.snack_starting_service
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!isEnabled) startService {}
|
||||
}
|
||||
} else {
|
||||
scope.launch {
|
||||
@@ -198,20 +247,19 @@ 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()
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
getStatus { isEnabled ->
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.btn_stop_service))
|
||||
snackbarHostState.showSnackbar(
|
||||
context.getString(
|
||||
if (isEnabled) R.string.snack_stopping_service else R.string.snack_no_service
|
||||
)
|
||||
)
|
||||
}
|
||||
if (isEnabled) stopService {}
|
||||
}
|
||||
else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service))
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
||||
}
|
||||
@@ -219,15 +267,44 @@ 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()
|
||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
restartService {}
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||
}
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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, BuildConfig.VERSION_NAME, newVersion.value.toString(), 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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
package com.cherret.zaprett.ui.screens
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material.icons.filled.InstallMobile
|
||||
import androidx.compose.material.icons.filled.Update
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FilledTonalButton
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Text
|
||||
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.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.cherret.zaprett.HostsInfo
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.download
|
||||
import com.cherret.zaprett.getAllLists
|
||||
import com.cherret.zaprett.getFileSha256
|
||||
import com.cherret.zaprett.getHostList
|
||||
import com.cherret.zaprett.getZaprettPath
|
||||
import com.cherret.zaprett.registerDownloadListenerHost
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HostsRepoScreen(navController: NavController) {
|
||||
val context = LocalContext.current
|
||||
var allLists by remember { mutableStateOf(getAllLists()) }
|
||||
var hostLists by remember { mutableStateOf<List<HostsInfo>?>(null) }
|
||||
val isUpdate = remember { mutableStateMapOf<String, Boolean>() }
|
||||
val snackbarHostState = remember { SnackbarHostState() }
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
LaunchedEffect(Unit) {
|
||||
getHostList {
|
||||
hostLists = it
|
||||
}
|
||||
}
|
||||
LaunchedEffect(hostLists) {
|
||||
if (hostLists != null) {
|
||||
withContext(Dispatchers.IO) {
|
||||
hostLists!!.forEach { item ->
|
||||
isUpdate[item.name] = !allLists.any { getFileSha256(File(it)) == item.hash }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Scaffold(
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_hosts_repo),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
navigationIcon = {
|
||||
IconButton(onClick = { navController.popBackStack() }) {
|
||||
Icon(
|
||||
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
|
||||
contentDescription = stringResource(R.string.btn_back)
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
getHostList {
|
||||
hostLists = it
|
||||
isRefreshing = false
|
||||
}
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
when {
|
||||
hostLists?.isEmpty() != false -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.btn_no_hosts),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
items(hostLists.orEmpty()) { item ->
|
||||
var isButtonEnabled by remember { mutableStateOf(!allLists.any { File(it).name == item.name }) }
|
||||
var isInstalling by remember { mutableStateOf(false) }
|
||||
var isButtonUpdateEnabled by remember { mutableStateOf(true) }
|
||||
var isUpdateInstalling by remember { mutableStateOf(false) }
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 6.dp
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceContainer,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.name,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = item.description,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
if (isUpdate[item.name] == true && allLists.any { File(it).name == item.name }) {
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
isUpdateInstalling = true
|
||||
isButtonUpdateEnabled = false
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListenerHost(
|
||||
context,
|
||||
downloadId
|
||||
) { uri ->
|
||||
val sourceFile = File(uri.path!!)
|
||||
val targetFile = File(
|
||||
getZaprettPath() + "/lists",
|
||||
uri.lastPathSegment!!
|
||||
)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
isUpdateInstalling = false
|
||||
getHostList {
|
||||
hostLists = it
|
||||
}
|
||||
isUpdate[item.name] = false
|
||||
}
|
||||
},
|
||||
enabled = isButtonUpdateEnabled,
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, end = 5.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Update,
|
||||
contentDescription = stringResource(R.string.btn_remove_host),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
if (!isUpdateInstalling) stringResource(R.string.btn_update_host) else stringResource(
|
||||
R.string.btn_updating_host
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
FilledTonalButton(
|
||||
onClick = {
|
||||
isInstalling = true
|
||||
isButtonEnabled = false
|
||||
val downloadId = download(context, item.url)
|
||||
registerDownloadListenerHost(
|
||||
context,
|
||||
downloadId
|
||||
) { uri ->
|
||||
val sourceFile = File(uri.path!!)
|
||||
val targetFile = File(
|
||||
getZaprettPath() + "/lists",
|
||||
uri.lastPathSegment!!
|
||||
)
|
||||
sourceFile.copyTo(targetFile, overwrite = true)
|
||||
sourceFile.delete()
|
||||
isInstalling = false
|
||||
getHostList {
|
||||
hostLists = it
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = isButtonEnabled,
|
||||
modifier = Modifier
|
||||
.padding(start = 5.dp, end = 5.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.InstallMobile,
|
||||
contentDescription = stringResource(R.string.btn_remove_host),
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Text(
|
||||
if (isButtonEnabled) stringResource(R.string.btn_install_host) else if (isInstalling) stringResource(
|
||||
R.string.btn_installing_host
|
||||
) else stringResource(R.string.btn_installed_host)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
|
||||
)
|
||||
}
|
||||
@@ -7,54 +7,31 @@ import android.provider.OpenableColumns
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.FloatingActionButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.Download
|
||||
import androidx.compose.material.icons.filled.UploadFile
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateMapOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation.NavController
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.disableList
|
||||
import com.cherret.zaprett.enableList
|
||||
import com.cherret.zaprett.getActiveLists
|
||||
import com.cherret.zaprett.getAllLists
|
||||
import com.cherret.zaprett.getZaprettPath
|
||||
import com.cherret.zaprett.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
@@ -63,7 +40,7 @@ import java.io.IOException
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HostsScreen() {
|
||||
fun HostsScreen(navController: NavController) {
|
||||
val context = LocalContext.current
|
||||
var allLists by remember { mutableStateOf(getAllLists()) }
|
||||
var activeLists by remember { mutableStateOf(getActiveLists()) }
|
||||
@@ -72,139 +49,216 @@ fun HostsScreen() {
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
val checked = remember {
|
||||
mutableStateMapOf<String, Boolean>().apply {
|
||||
allLists.forEach { list ->
|
||||
this[list] = activeLists.contains(list)
|
||||
}
|
||||
allLists.forEach { list -> this[list] = activeLists.contains(list) }
|
||||
}
|
||||
}
|
||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.OpenDocument(),
|
||||
onResult = { uri: Uri? ->
|
||||
uri?.let {
|
||||
copySelectedFile(context, it, snackbarHostState, scope)
|
||||
}
|
||||
}
|
||||
)
|
||||
contract = ActivityResultContracts.OpenDocument()
|
||||
) { uri ->
|
||||
uri?.let { copySelectedFile(context, it, snackbarHostState, scope) }
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
||||
rotate(degrees = -30f) {
|
||||
drawOval(
|
||||
color = primaryColor,
|
||||
size = Size(200f, 140f),
|
||||
topLeft = Offset(-30f, -30f)
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.title_hosts),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
}
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_hosts),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
content = { paddingValues ->
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
allLists = getAllLists()
|
||||
activeLists = getActiveLists()
|
||||
checked.clear()
|
||||
allLists.forEach { list ->
|
||||
checked[list] = activeLists.contains(list)
|
||||
}
|
||||
isRefreshing = false
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
PullToRefreshBox(
|
||||
isRefreshing = isRefreshing,
|
||||
onRefresh = {
|
||||
isRefreshing = true
|
||||
allLists = getAllLists()
|
||||
activeLists = getActiveLists()
|
||||
checked.clear()
|
||||
allLists.forEach { list ->
|
||||
checked[list] = activeLists.contains(list)
|
||||
}
|
||||
isRefreshing = false
|
||||
},
|
||||
modifier = Modifier
|
||||
LazyColumn(
|
||||
contentPadding = paddingValues,
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
LazyColumn {
|
||||
items(allLists) { item ->
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 6.dp
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
when {
|
||||
allLists.isEmpty() != false -> {
|
||||
item {
|
||||
Box(
|
||||
modifier = Modifier.fillParentMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = item,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
checked[item] = isChecked
|
||||
if (isChecked) {
|
||||
enableList(item)
|
||||
} else {
|
||||
disableList(item)
|
||||
}
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack))
|
||||
}
|
||||
}
|
||||
stringResource(R.string.btn_no_hosts),
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
items(allLists) { item ->
|
||||
HostItem(
|
||||
item = item,
|
||||
isChecked = checked[item] == true,
|
||||
onCheckedChange = { isChecked ->
|
||||
checked[item] = isChecked
|
||||
if (isChecked) enableList(item) else disableList(item)
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
},
|
||||
onDeleteClick = {
|
||||
if (deleteHost(item)) {
|
||||
allLists = getAllLists()
|
||||
activeLists = getActiveLists()
|
||||
checked.clear()
|
||||
allLists.forEach { list ->
|
||||
checked[list] = activeLists.contains(list)
|
||||
}
|
||||
}
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
FloatingActionButton(modifier = Modifier
|
||||
.size(80.dp, 80.dp),
|
||||
onClick = {addHost(filePickerLauncher)}) {
|
||||
Icon(Icons.Default.Add, contentDescription = "Restart")
|
||||
}
|
||||
FloatingMenu(navController, filePickerLauncher)
|
||||
},
|
||||
snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
|
||||
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||
)
|
||||
}
|
||||
|
||||
fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
||||
launcher.launch(arrayOf("*/*"))
|
||||
}
|
||||
|
||||
fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
if (uri == null) return
|
||||
val contentResolver = context.contentResolver
|
||||
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file"
|
||||
} ?: "copied_file"
|
||||
val outputFile = File(getZaprettPath() + "/lists", fileName)
|
||||
try {
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
FileOutputStream(outputFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
@Composable
|
||||
private fun HostItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(text = item, modifier = Modifier.weight(1f))
|
||||
Switch(checked = isChecked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
HorizontalDivider(thickness = Dp.Hairline)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
FilledTonalButton(
|
||||
onClick = onDeleteClick,
|
||||
modifier = Modifier.padding(horizontal = 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))
|
||||
}
|
||||
scope.launch {
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher<Array<String>>) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.size(80.dp),
|
||||
onClick = { expanded = !expanded }
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = stringResource(R.string.btn_add_host))
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.btn_download_host)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
navController.navigate("hosts_repo") { launchSingleTop = true }
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.Download, contentDescription = stringResource(R.string.btn_download_host))
|
||||
}
|
||||
)
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.btn_add_host)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
addHost(launcher)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(Icons.Default.UploadFile, contentDescription = stringResource(R.string.btn_add_host))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
||||
launcher.launch(arrayOf("text/plain"))
|
||||
}
|
||||
|
||||
private fun copySelectedFile(context: Context, uri: Uri, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
if (!Environment.isExternalStorageManager()) return
|
||||
|
||||
val contentResolver = context.contentResolver
|
||||
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
||||
if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file"
|
||||
} ?: "copied_file"
|
||||
|
||||
val outputFile = File(getZaprettPath() + "/lists", fileName)
|
||||
|
||||
try {
|
||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
||||
FileOutputStream(outputFile).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
showRestartSnackbar(context, snackbarHostState, scope)
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
private fun deleteHost(item: String): Boolean {
|
||||
val hostFile = File(item)
|
||||
return if (hostFile.exists()) {
|
||||
hostFile.delete()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun showRestartSnackbar(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||
scope.launch {
|
||||
val result = snackbarHostState.showSnackbar(
|
||||
context.getString(R.string.pls_restart_snack),
|
||||
actionLabel = context.getString(R.string.btn_restart_service)
|
||||
)
|
||||
if (result == SnackbarResult.ActionPerformed) {
|
||||
restartService {}
|
||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,135 +1,171 @@
|
||||
package com.cherret.zaprett.ui.screens
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.CardDefaults
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.drawscope.rotate
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.Font
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.cherret.zaprett.BuildConfig
|
||||
import com.cherret.zaprett.R
|
||||
import com.cherret.zaprett.checkModuleInstallation
|
||||
import com.cherret.zaprett.checkRoot
|
||||
import com.cherret.zaprett.getStartOnBoot
|
||||
import com.cherret.zaprett.setStartOnBoot
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SettingsScreen() {
|
||||
val context = LocalContext.current
|
||||
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||
val editor = remember { sharedPreferences.edit() }
|
||||
|
||||
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
|
||||
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
|
||||
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
|
||||
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
|
||||
val sendFirebaseAnalytics = remember { mutableStateOf(sharedPreferences.getBoolean("send_firebase_analytics", true)) }
|
||||
val openNoRootDialog = remember { mutableStateOf(false) }
|
||||
val openNoModuleDialog = remember { mutableStateOf(false) }
|
||||
showNoRootDialog(openNoRootDialog)
|
||||
showNoModuleDialog(openNoModuleDialog)
|
||||
var showAboutDialog = remember { mutableStateOf(false) }
|
||||
|
||||
val settingsList = listOf(
|
||||
SettingItem(
|
||||
title = stringResource(R.string.btn_use_root),
|
||||
checked = useModule.value,
|
||||
onToggle = { isChecked ->
|
||||
useModule(
|
||||
context = context,
|
||||
checked = isChecked,
|
||||
updateOnBoot = updateOnBoot,
|
||||
openNoRootDialog = openNoRootDialog,
|
||||
openNoModuleDialog = openNoModuleDialog
|
||||
) { success ->
|
||||
if (success) useModule.value = isChecked
|
||||
}
|
||||
}
|
||||
),
|
||||
SettingItem(
|
||||
title = stringResource(R.string.btn_update_on_boot),
|
||||
checked = updateOnBoot.value,
|
||||
onToggle = {
|
||||
updateOnBoot.value = it
|
||||
editor.putBoolean("update_on_boot", it).apply()
|
||||
}
|
||||
),
|
||||
SettingItem(
|
||||
title = stringResource(R.string.btn_autorestart),
|
||||
checked = autoRestart.value,
|
||||
onToggle = {
|
||||
if (handleAutoRestart(context, it)) autoRestart.value = it
|
||||
}
|
||||
),
|
||||
SettingItem(
|
||||
title = stringResource(R.string.btn_autoupdate),
|
||||
checked = autoUpdate.value,
|
||||
onToggle = {
|
||||
autoUpdate.value = it
|
||||
editor.putBoolean("auto_update", it).apply()
|
||||
}
|
||||
),
|
||||
SettingItem(
|
||||
title = stringResource(R.string.btn_send_firebase_analytics),
|
||||
checked = sendFirebaseAnalytics.value,
|
||||
onToggle = {
|
||||
sendFirebaseAnalytics.value = it
|
||||
editor.putBoolean("send_firebase_analytics", it).apply()
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
if (openNoRootDialog.value) {
|
||||
InfoDialog(
|
||||
title = stringResource(R.string.error_root_title),
|
||||
message = stringResource(R.string.error_root_message),
|
||||
onDismiss = { openNoRootDialog.value = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (openNoModuleDialog.value) {
|
||||
InfoDialog(
|
||||
title = stringResource(R.string.error_no_module_title),
|
||||
message = stringResource(R.string.error_no_module_message),
|
||||
onDismiss = { openNoModuleDialog.value = false }
|
||||
)
|
||||
}
|
||||
|
||||
if (showAboutDialog.value) {
|
||||
AboutDialog(onDismiss = { showAboutDialog.value = false })
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
||||
rotate(degrees = -30f) {
|
||||
drawOval(
|
||||
color = primaryColor,
|
||||
size = Size(200f, 140f),
|
||||
topLeft = Offset(-30f, -30f)
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
text = stringResource(R.string.title_settings),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
IconButton(onClick = { expanded = !expanded }) {
|
||||
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||
}
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.about_title)) },
|
||||
onClick = {
|
||||
expanded = false
|
||||
showAboutDialog.value = true
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Text(
|
||||
text = stringResource(R.string.title_settings),
|
||||
fontSize = 40.sp,
|
||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||
)
|
||||
}
|
||||
},
|
||||
windowInsets = WindowInsets(0)
|
||||
)
|
||||
},
|
||||
content = { paddingValues ->
|
||||
Column(
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.fillMaxSize(),
|
||||
contentPadding = PaddingValues(horizontal = 10.dp, vertical = 25.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ElevatedCard(
|
||||
elevation = CardDefaults.cardElevation(
|
||||
defaultElevation = 6.dp
|
||||
),
|
||||
colors = CardDefaults.cardColors(
|
||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
items(settingsList) { setting ->
|
||||
ElevatedCard(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.btn_use_root),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = useModule.value,
|
||||
onCheckedChange = { if (useModule(context, it, updateOnBoot, openNoRootDialog, openNoModuleDialog)) useModule.value = it}
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.btn_update_on_boot),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = updateOnBoot.value,
|
||||
onCheckedChange = { updateOnBoot.value = it; editor.putBoolean("update_on_boot", it).apply()}
|
||||
)
|
||||
}
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.btn_autorestart),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = autoRestart.value,
|
||||
onCheckedChange = { if (autoRestart(context, it)) autoRestart.value = it;}
|
||||
)
|
||||
Row(modifier = Modifier.clickable {
|
||||
setting.onToggle(!setting.checked)
|
||||
}) {
|
||||
SettingsItem(
|
||||
title = setting.title,
|
||||
checked = setting.checked,
|
||||
onCheckedChange = setting.onToggle
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,88 +173,93 @@ fun SettingsScreen() {
|
||||
)
|
||||
}
|
||||
|
||||
fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>): Boolean {
|
||||
@Composable
|
||||
private fun SettingsItem(title: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
Switch(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit) {
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
val editor = sharedPreferences.edit()
|
||||
if (checked) {
|
||||
if (checkRoot()) {
|
||||
if (checkModuleInstallation()) {
|
||||
editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply()
|
||||
updateOnBoot.value = true
|
||||
return true
|
||||
|
||||
checkRoot { hasRoot ->
|
||||
if (hasRoot) {
|
||||
checkModuleInstallation { hasModule ->
|
||||
if (hasModule) {
|
||||
editor.putBoolean("use_module", true)
|
||||
.putBoolean("update_on_boot", true)
|
||||
.apply()
|
||||
updateOnBoot.value = true
|
||||
callback(true)
|
||||
} else {
|
||||
openNoModuleDialog.value = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openNoRootDialog.value = true
|
||||
}
|
||||
else {
|
||||
openNoModuleDialog.value = true
|
||||
}
|
||||
} else {
|
||||
openNoRootDialog.value = true
|
||||
}
|
||||
}
|
||||
else {
|
||||
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false)
|
||||
} else {
|
||||
editor.putBoolean("use_module", false)
|
||||
.putBoolean("update_on_boot", false)
|
||||
.apply()
|
||||
return true
|
||||
updateOnBoot.value = false
|
||||
callback(true)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun autoRestart(context: Context, checked: Boolean): Boolean {
|
||||
if (context.getSharedPreferences("settings", Context.MODE_PRIVATE).getBoolean("use_module", false)) {
|
||||
private fun handleAutoRestart(context: Context, checked: Boolean): Boolean {
|
||||
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||
return if (sharedPreferences.getBoolean("use_module", false)) {
|
||||
setStartOnBoot(checked)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun showNoRootDialog(openDialog: MutableState<Boolean>) {
|
||||
if (openDialog.value) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.error_root_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.error_root_message))
|
||||
},
|
||||
onDismissRequest = {
|
||||
openDialog.value = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
openDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
},
|
||||
)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun showNoModuleDialog(openDialog: MutableState<Boolean>) {
|
||||
if (openDialog.value) {
|
||||
AlertDialog(
|
||||
title = {
|
||||
Text(text = stringResource(R.string.error_no_module_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(R.string.error_no_module_message))
|
||||
},
|
||||
onDismissRequest = {
|
||||
openDialog.value = false
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
openDialog.value = false
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun InfoDialog(title: String, message: String, onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { Text(text = title) },
|
||||
text = { Text(text = message) },
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = {
|
||||
TextButton(onClick = onDismiss) {
|
||||
Text(stringResource(R.string.btn_continue))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AboutDialog(onDismiss: () -> Unit) {
|
||||
AlertDialog(
|
||||
title = { Text(text = stringResource(R.string.about_title)) },
|
||||
icon = {Icon(painterResource(R.drawable.ic_launcher_monochrome), contentDescription = stringResource(R.string.app_name), modifier = Modifier
|
||||
.size(64.dp))},
|
||||
text = { Text(text = stringResource(R.string.about_text, BuildConfig.VERSION_NAME)) },
|
||||
onDismissRequest = onDismiss,
|
||||
confirmButton = { }
|
||||
)
|
||||
}
|
||||
|
||||
data class SettingItem(
|
||||
val title: String,
|
||||
val checked: Boolean,
|
||||
val onToggle: (Boolean) -> Unit
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -2,33 +2,58 @@
|
||||
<resources>
|
||||
<string name="title_home">Главная</string>
|
||||
<string name="title_hosts">Хосты</string>
|
||||
<string name="title_hosts_repo">Репозиторий</string>
|
||||
<string name="title_settings">Настройки</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="btn_use_root">Использовать модуль</string>
|
||||
<string name="error_root_title">Root не получен</string>
|
||||
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
||||
<string name="error_no_module_title">Модуль не установлен</string>
|
||||
<string name="error_no_module_message">Magisk модуль zaprett не найден. Пожалуйста, установите его</string>
|
||||
<string name="snack_reload">Перезагружаем zaprett...</string>
|
||||
<string name="snack_reload">Перезагружаем zaprett…</string>
|
||||
<string name="snack_module_disabled">Модуль Magisk отключен! Не получилось выполнить действие</string>
|
||||
<string name="title_strategy">Стратегия</string>
|
||||
<string name="status_not_availible">Состояние zaprett неизвестно. Нажми для обновления</string>
|
||||
<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_remove_host">Удалить</string>
|
||||
<string name="btn_download_host">Скачать</string>
|
||||
<string name="btn_add_host">Добавить</string>
|
||||
<string name="btn_back">Назад</string>
|
||||
<string name="btn_no_hosts">Нет хостов</string>
|
||||
<string name="btn_install_host">Установить</string>
|
||||
<string name="btn_update_host">Обновить</string>
|
||||
<string name="btn_installing_host">Установка</string>
|
||||
<string name="btn_updating_host">Обновление</string>
|
||||
<string name="btn_installed_host">Установленно</string>
|
||||
<string name="btn_autorestart">Переодически перезапускать сервис</string>
|
||||
<string name="btn_autoupdate">Авто обновление</string>
|
||||
<string name="btn_send_firebase_analytics">Отправлять аналитику Firebase</string>
|
||||
<string name="alert_version">Version: %1$s → %2$s\nСписок изменений:\n%3$s</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_stopping_service">Останавливаем сервис...</string>
|
||||
<string name="snack_stopping_service">Останавливаем сервис…</string>
|
||||
<string name="error_no_storage_title">Нет разрешения</string>
|
||||
<string name="error_no_storage_message">Для правильной работы приложения необходимо разрешение на доступ к хранилищу</string>
|
||||
<string name="btn_show_full_path">Показывать полный путь к листу</string>
|
||||
<string name="pls_reboot_snack">Перезагрузите устройство для вступления изменений в силу</string>
|
||||
<string name="pls_restart_snack">Перезапустите zaprett для вступления изменений в силу</string>
|
||||
<string name="qs_name">Zaprett</string>
|
||||
<string name="qs_starting">Запуск…</string>
|
||||
<string name="qs_stopping">Остановка…</string>
|
||||
<string name="qs_not_available">Не доступно</string>
|
||||
<string name="qs_working">Работает</string>
|
||||
<string name="qs_not_working">Не работает</string>
|
||||
<string name="about_title">О приложении</string>
|
||||
<string name="about_text">Zaprett app от Cherret\nВерсия: %1$s</string>
|
||||
</resources>
|
||||
@@ -2,33 +2,58 @@
|
||||
<string name="app_name" translatable="false">zaprett</string>
|
||||
<string name="title_home">Home</string>
|
||||
<string name="title_hosts">Hosts</string>
|
||||
<string name="title_hosts_repo">Repository</string>
|
||||
<string name="title_settings">Settings</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="btn_use_root">Use module</string>
|
||||
<string name="error_root_title">Can\'t get root</string>
|
||||
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</string>
|
||||
<string name="error_no_module_title">Module is not installed</string>
|
||||
<string name="error_no_module_message">Magisk module zaprett wasn\'t found. Please install it</string>
|
||||
<string name="snack_reload">Reloading zaprett...</string>
|
||||
<string name="snack_reload">Reloading zaprett…</string>
|
||||
<string name="snack_module_disabled">Magisk module disabled! Cant\'t execute action</string>
|
||||
<string name="title_strategy">Strategy</string>
|
||||
<string name="status_not_availible">Status of zaprett is unknown. 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_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_remove_host">Remove</string>
|
||||
<string name="btn_download_host">Download</string>
|
||||
<string name="btn_add_host">Add</string>
|
||||
<string name="btn_back">Back</string>
|
||||
<string name="btn_no_hosts">No Hosts</string>
|
||||
<string name="btn_install_host">Install</string>
|
||||
<string name="btn_update_host">Update</string>
|
||||
<string name="btn_installing_host">Installing</string>
|
||||
<string name="btn_updating_host">Updating</string>
|
||||
<string name="btn_installed_host">Installed</string>
|
||||
<string name="btn_autorestart">Restart service periodicaly</string>
|
||||
<string name="btn_autoupdate">Autoupdate</string>
|
||||
<string name="btn_send_firebase_analytics">Send Firebase Analytics</string>
|
||||
<string name="alert_version">Version: %1$s → %2$s\nChangelog:\n%3$s</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_stopping_service">Stopping service...</string>
|
||||
<string name="snack_stopping_service">Stopping service…</string>
|
||||
<string name="error_no_storage_title">No permission</string>
|
||||
<string name="error_no_storage_message">The application requires permission to acess the storage to work properly</string>
|
||||
<string name="btn_show_full_path">Show full list\'s path</string>
|
||||
<string name="pls_reboot_snack">Reboot your device for the changes to take effect</string>
|
||||
<string name="pls_restart_snack">Restart zaprett service for the changes to take effects</string>
|
||||
<string name="qs_name">Zaprett</string>
|
||||
<string name="qs_starting">Starting…</string>
|
||||
<string name="qs_stopping">Stopping…</string>
|
||||
<string name="qs_not_available">Not available</string>
|
||||
<string name="qs_working">Working</string>
|
||||
<string name="qs_not_working">Not working</string>
|
||||
<string name="about_title">About app</string>
|
||||
<string name="about_text">Zaprett App by Cherret\nVersion: %1$s</string>
|
||||
</resources>
|
||||
5
app/src/main/res/xml/file_paths.xml
Normal file
5
app/src/main/res/xml/file_paths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<external-path name="downloads" path="Download/" />
|
||||
<external-files-path name="apk" path="zaprett/" />
|
||||
</paths>
|
||||
@@ -3,4 +3,6 @@ plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.kotlin.android) 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 @@
|
||||
Добавлена возможность загружать хосты из репозитория
|
||||
Рефакторинг кода
|
||||
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.7",
|
||||
"versionCode": 8,
|
||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_7_0/app-release.apk",
|
||||
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||
}
|
||||
Reference in New Issue
Block a user