mirror of
https://github.com/CherretGit/zaprett-app.git
synced 2025-12-11 14:09:37 +05:00
Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
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
|
||||||
@@ -6,8 +6,7 @@
|
|||||||
На данный момент приложение умеет:
|
На данный момент приложение умеет:
|
||||||
* Включать, выключать и перезапускать модуль
|
* Включать, выключать и перезапускать модуль
|
||||||
* Работа с листами (добавление, включение и выключение)
|
* Работа с листами (добавление, включение и выключение)
|
||||||
|
* Авто обновление приложения
|
||||||
|
|
||||||
## Скриншоты:
|
## Скриншоты:
|
||||||

|
<img src="images/1.png" width="300"><img src="images/2.png" width="300"><img src="images/3.png" width="300">
|
||||||

|
|
||||||

|
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ plugins {
|
|||||||
alias(libs.plugins.android.application)
|
alias(libs.plugins.android.application)
|
||||||
alias(libs.plugins.kotlin.android)
|
alias(libs.plugins.kotlin.android)
|
||||||
alias(libs.plugins.kotlin.compose)
|
alias(libs.plugins.kotlin.compose)
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
id("com.google.firebase.crashlytics")
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
@@ -12,8 +14,8 @@ android {
|
|||||||
applicationId = "com.cherret.zaprett"
|
applicationId = "com.cherret.zaprett"
|
||||||
minSdk = 30
|
minSdk = 30
|
||||||
targetSdk = 35
|
targetSdk = 35
|
||||||
versionCode = 2
|
versionCode = 8
|
||||||
versionName = "1.1"
|
versionName = "1.7"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
@@ -46,6 +48,11 @@ dependencies {
|
|||||||
implementation("androidx.navigation:navigation-compose:2.8.9")
|
implementation("androidx.navigation:navigation-compose:2.8.9")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
implementation("androidx.compose.material:material-icons-extended:1.7.8")
|
||||||
implementation ("com.github.topjohnwu.libsu:core:6.0.0")
|
implementation ("com.github.topjohnwu.libsu:core:6.0.0")
|
||||||
|
implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.14")
|
||||||
|
implementation("com.squareup.moshi:moshi-kotlin:1.15.2")
|
||||||
|
implementation(platform("com.google.firebase:firebase-bom:33.12.0"))
|
||||||
|
implementation("com.google.firebase:firebase-analytics")
|
||||||
|
implementation("com.google.firebase:firebase-crashlytics")
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.lifecycle.runtime.ktx)
|
implementation(libs.androidx.lifecycle.runtime.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
|
|||||||
29
app/google-services.json
Normal file
29
app/google-services.json
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
{
|
||||||
|
"project_info": {
|
||||||
|
"project_number": "1005804036856",
|
||||||
|
"project_id": "zaprett-app",
|
||||||
|
"storage_bucket": "zaprett-app.firebasestorage.app"
|
||||||
|
},
|
||||||
|
"client": [
|
||||||
|
{
|
||||||
|
"client_info": {
|
||||||
|
"mobilesdk_app_id": "1:1005804036856:android:e7db5546b8bb4daf91510d",
|
||||||
|
"android_client_info": {
|
||||||
|
"package_name": "com.cherret.zaprett"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"oauth_client": [],
|
||||||
|
"api_key": [
|
||||||
|
{
|
||||||
|
"current_key": "AIzaSyASt83pAMxMI4txNAXaHDpX1R9crfoZAMk"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"services": {
|
||||||
|
"appinvite_service": {
|
||||||
|
"other_platform_oauth_client": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"configuration_version": "1"
|
||||||
|
}
|
||||||
@@ -7,6 +7,9 @@
|
|||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
@@ -17,6 +20,15 @@
|
|||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Zaprett"
|
android:theme="@style/Theme.Zaprett"
|
||||||
tools:targetApi="31">
|
tools:targetApi="31">
|
||||||
|
<provider
|
||||||
|
android:name="androidx.core.content.FileProvider"
|
||||||
|
android:authorities="${applicationId}.provider"
|
||||||
|
android:exported="false"
|
||||||
|
android:grantUriPermissions="true">
|
||||||
|
<meta-data
|
||||||
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
|
android:resource="@xml/file_paths" />
|
||||||
|
</provider>
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
@@ -28,6 +40,16 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</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>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -23,21 +23,27 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.core.content.edit
|
||||||
import androidx.navigation.NavGraph.Companion.findStartDestination
|
import androidx.navigation.NavGraph.Companion.findStartDestination
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.currentBackStackEntryAsState
|
import androidx.navigation.compose.currentBackStackEntryAsState
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
import com.cherret.zaprett.ui.screens.HomeScreen
|
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.HostsScreen
|
||||||
import com.cherret.zaprett.ui.screens.SettingsScreen
|
import com.cherret.zaprett.ui.screens.SettingsScreen
|
||||||
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
import com.cherret.zaprett.ui.theme.ZaprettTheme
|
||||||
import com.topjohnwu.superuser.Shell
|
import com.google.firebase.Firebase
|
||||||
|
import com.google.firebase.analytics.FirebaseAnalytics
|
||||||
|
import com.google.firebase.analytics.analytics
|
||||||
|
|
||||||
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) {
|
sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: androidx.compose.ui.graphics.vector.ImageVector) {
|
||||||
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
object home : Screen("home", R.string.title_home, Icons.Default.Home)
|
||||||
@@ -45,21 +51,27 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon:
|
|||||||
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
|
object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings)
|
||||||
}
|
}
|
||||||
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
|
val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings)
|
||||||
|
val hideNavBar = listOf("hosts_repo")
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
private lateinit var firebaseAnalytics: FirebaseAnalytics
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
val sharedPreferences = getSharedPreferences("settings", MODE_PRIVATE)
|
firebaseAnalytics = Firebase.analytics
|
||||||
Shell.setDefaultBuilder(Shell.Builder.create()
|
|
||||||
.setTimeout(10))
|
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
setContent {
|
setContent {
|
||||||
ZaprettTheme {
|
ZaprettTheme {
|
||||||
|
val sharedPreferences = remember { getSharedPreferences("settings", MODE_PRIVATE) }
|
||||||
|
var showPermissionDialog by remember { mutableStateOf(!Environment.isExternalStorageManager()) }
|
||||||
|
var showWelcomeDialog by remember { mutableStateOf(sharedPreferences.getBoolean("welcome_dialog", true)) }
|
||||||
BottomBar()
|
BottomBar()
|
||||||
if (!Environment.isExternalStorageManager()) {
|
if (showPermissionDialog) {
|
||||||
permissionDialog()
|
PermissionDialog { showPermissionDialog = false }
|
||||||
}
|
}
|
||||||
if (sharedPreferences.getBoolean("welcome_dialog", true)) {
|
if (showWelcomeDialog) {
|
||||||
welcomeDialog()
|
WelcomeDialog {
|
||||||
|
sharedPreferences.edit() { putBoolean("welcome_dialog", false) }
|
||||||
|
showWelcomeDialog = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -70,29 +82,31 @@ class MainActivity : ComponentActivity() {
|
|||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
NavigationBar {
|
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
||||||
val navBackStackEntry = navController.currentBackStackEntryAsState().value
|
val currentDestination = navBackStackEntry?.destination
|
||||||
val currentDestination = navBackStackEntry?.destination
|
if (currentDestination?.route !in hideNavBar) {
|
||||||
topLevelRoutes.forEach { topLevelRoute ->
|
NavigationBar {
|
||||||
NavigationBarItem(
|
topLevelRoutes.forEach { topLevelRoute ->
|
||||||
icon = {
|
NavigationBarItem(
|
||||||
Icon(
|
icon = {
|
||||||
topLevelRoute.icon,
|
Icon(
|
||||||
contentDescription = stringResource(id = topLevelRoute.nameResId)
|
topLevelRoute.icon,
|
||||||
)
|
contentDescription = stringResource(id = topLevelRoute.nameResId)
|
||||||
},
|
)
|
||||||
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
|
},
|
||||||
selected = currentDestination?.route == topLevelRoute.route,
|
label = { Text(text = stringResource(id = topLevelRoute.nameResId)) },
|
||||||
onClick = {
|
selected = currentDestination?.route == topLevelRoute.route,
|
||||||
navController.navigate(topLevelRoute.route) {
|
onClick = {
|
||||||
popUpTo(navController.graph.findStartDestination().id) {
|
navController.navigate(topLevelRoute.route) {
|
||||||
saveState = true
|
popUpTo(navController.graph.findStartDestination().id) {
|
||||||
|
saveState = true
|
||||||
|
}
|
||||||
|
launchSingleTop = true
|
||||||
|
restoreState = true
|
||||||
}
|
}
|
||||||
launchSingleTop = true
|
|
||||||
restoreState = true
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,76 +117,47 @@ class MainActivity : ComponentActivity() {
|
|||||||
Modifier.padding(innerPadding)
|
Modifier.padding(innerPadding)
|
||||||
) {
|
) {
|
||||||
composable(Screen.home.route) { HomeScreen() }
|
composable(Screen.home.route) { HomeScreen() }
|
||||||
composable(Screen.hosts.route) { HostsScreen() }
|
composable(Screen.hosts.route) { HostsScreen(navController) }
|
||||||
composable(Screen.settings.route) { SettingsScreen() }
|
composable(Screen.settings.route) { SettingsScreen() }
|
||||||
|
composable("hosts_repo") { HostsRepoScreen(navController) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun welcomeDialog() {
|
fun WelcomeDialog(onDismiss: () -> Unit) {
|
||||||
val sharedPreferences =
|
AlertDialog(
|
||||||
LocalContext.current.getSharedPreferences("settings", MODE_PRIVATE)
|
title = { Text(text = stringResource(R.string.app_name)) },
|
||||||
val editor = sharedPreferences.edit()
|
text = { Text(text = stringResource(R.string.text_welcome)) },
|
||||||
val openDialog = remember { mutableStateOf(true) }
|
onDismissRequest = onDismiss,
|
||||||
if (openDialog.value) {
|
confirmButton = {
|
||||||
AlertDialog(
|
TextButton(onClick = onDismiss) {
|
||||||
title = {
|
Text(stringResource(R.string.btn_continue))
|
||||||
Text(text = stringResource(R.string.app_name))
|
}
|
||||||
},
|
}
|
||||||
text = {
|
)
|
||||||
Text(text = stringResource(R.string.text_welcome))
|
|
||||||
},
|
|
||||||
onDismissRequest = {
|
|
||||||
editor.putBoolean("welcome_dialog", false).apply()
|
|
||||||
openDialog.value = false
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
editor.putBoolean("welcome_dialog", false).apply()
|
|
||||||
openDialog.value = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.btn_continue))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun permissionDialog() {
|
fun PermissionDialog(onDismiss: () -> Unit) {
|
||||||
val openDialog = remember { mutableStateOf(true) }
|
val context = LocalContext.current
|
||||||
if (openDialog.value) {
|
AlertDialog(
|
||||||
AlertDialog(
|
title = { Text(text = stringResource(R.string.error_no_storage_title)) },
|
||||||
title = {
|
text = { Text(text = stringResource(R.string.error_no_storage_message)) },
|
||||||
Text(text = stringResource(R.string.error_no_storage_title))
|
onDismissRequest = onDismiss,
|
||||||
},
|
confirmButton = {
|
||||||
text = {
|
TextButton(
|
||||||
Text(text = stringResource(R.string.error_no_storage_message))
|
onClick = {
|
||||||
},
|
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||||
onDismissRequest = {
|
val uri = Uri.fromParts("package", context.packageName, null)
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
intent.data = uri
|
||||||
val uri = Uri.fromParts("package", packageName, null)
|
context.startActivity(intent)
|
||||||
intent.setData(uri)
|
onDismiss()
|
||||||
startActivity(intent)
|
|
||||||
openDialog.value = false
|
|
||||||
},
|
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
|
||||||
onClick = {
|
|
||||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
|
||||||
val uri = Uri.fromParts("package", packageName, null)
|
|
||||||
intent.setData(uri)
|
|
||||||
startActivity(intent)
|
|
||||||
openDialog.value = false
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.btn_continue))
|
|
||||||
}
|
}
|
||||||
},
|
) {
|
||||||
)
|
Text(stringResource(R.string.btn_continue))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,7 @@ fun getStartOnBoot(): Boolean {
|
|||||||
} else {
|
} else {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
} catch (e: IOException) {
|
} catch (_: IOException) {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,12 +92,12 @@ fun getZaprettPath(): String {
|
|||||||
FileInputStream(configFile).use { input ->
|
FileInputStream(configFile).use { input ->
|
||||||
props.load(input)
|
props.load(input)
|
||||||
}
|
}
|
||||||
props.getProperty("zaprettdir", "/sdcard/zaprett")
|
props.getProperty("zaprettdir", Environment.getExternalStorageDirectory().path + "/zaprett")
|
||||||
} catch (e: IOException) {
|
} catch (e: IOException) {
|
||||||
throw RuntimeException(e)
|
throw RuntimeException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "/sdcard/zaprett"
|
return Environment.getExternalStorageDirectory().path + "/zaprett"
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAllLists(): Array<String> {
|
fun getAllLists(): Array<String> {
|
||||||
|
|||||||
65
app/src/main/java/com/cherret/zaprett/QSTileService.kt
Normal file
65
app/src/main/java/com/cherret/zaprett/QSTileService.kt
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
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 onStopListening() {
|
||||||
|
super.onStopListening()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
142
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal file
142
app/src/main/java/com/cherret/zaprett/Updater.kt
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
package com.cherret.zaprett
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.Callback
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Request
|
||||||
|
import okhttp3.Response
|
||||||
|
import okio.IOException
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private val client = OkHttpClient()
|
||||||
|
|
||||||
|
fun getUpdate(context: Context, callback: (UpdateInfo?) -> Unit) {
|
||||||
|
val request = Request.Builder().url("https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/update.json").build()
|
||||||
|
val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build()
|
||||||
|
val jsonAdapter = moshi.adapter(UpdateInfo::class.java)
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.use {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException()
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
val jsonString = response.body!!.string()
|
||||||
|
val updateInfo = jsonAdapter.fromJson(jsonString)
|
||||||
|
if (updateInfo != null) {
|
||||||
|
val packageVersionCode = context.packageManager.getPackageInfo(context.packageName, 0).longVersionCode
|
||||||
|
updateInfo.versionCode?.let { versionCode ->
|
||||||
|
if (versionCode > packageVersionCode)
|
||||||
|
callback(updateInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getChangelog(changelogUrl: String, callback: (String?) -> Unit) {
|
||||||
|
val request = Request.Builder().url(changelogUrl).build()
|
||||||
|
client.newCall(request).enqueue(object : Callback {
|
||||||
|
override fun onFailure(call: Call, e: IOException) {
|
||||||
|
e.printStackTrace()
|
||||||
|
callback(null)
|
||||||
|
}
|
||||||
|
override fun onResponse(call: Call, response: Response) {
|
||||||
|
response.use {
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
callback(null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val changelogText = response.body!!.string()
|
||||||
|
callback(changelogText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fun download(context: Context, url: String): Long {
|
||||||
|
val fileName = url.substringAfterLast("/")
|
||||||
|
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
if (Environment.isExternalStorageManager()) {
|
||||||
|
val file = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName)
|
||||||
|
if (file.exists()) {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val request = DownloadManager.Request(url.toUri()).apply {
|
||||||
|
setTitle(fileName)
|
||||||
|
setDescription("Загрузка $fileName")
|
||||||
|
setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||||
|
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, fileName)
|
||||||
|
setAllowedNetworkTypes(DownloadManager.Request.NETWORK_WIFI or DownloadManager.Request.NETWORK_MOBILE)
|
||||||
|
}
|
||||||
|
return downloadManager.enqueue(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun installApk(context: Context, uri: Uri) {
|
||||||
|
val file = File(uri.path!!)
|
||||||
|
val apkUri = FileProvider.getUriForFile(context, "${context.packageName}.provider", file)
|
||||||
|
if (context.packageManager.canRequestPackageInstalls()) {
|
||||||
|
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(apkUri, "application/vnd.android.package-archive")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
val packageUri = Uri.fromParts("package", context.packageName, null)
|
||||||
|
val intent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, packageUri)
|
||||||
|
context.startActivity(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun registerDownloadListener(context: Context, downloadId: Long, onDownloaded: (Uri) -> Unit) {// AI Generated
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
@SuppressLint("Range")
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
if (intent?.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE) return
|
||||||
|
if (intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) != downloadId) return
|
||||||
|
val downloadManager = context?.getSystemService(Context.DOWNLOAD_SERVICE) as? DownloadManager ?: return
|
||||||
|
downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
|
||||||
|
if (cursor.moveToFirst() && cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS)) == DownloadManager.STATUS_SUCCESSFUL) {
|
||||||
|
context.unregisterReceiver(this)
|
||||||
|
onDownloaded(cursor.getString(cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)).toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val intentFilter = IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
context.registerReceiver(receiver, intentFilter, Context.RECEIVER_EXPORTED)
|
||||||
|
} else {
|
||||||
|
ContextCompat.registerReceiver(context, receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE), ContextCompat.RECEIVER_EXPORTED)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateInfo(
|
||||||
|
val version: String?,
|
||||||
|
val versionCode: Int?,
|
||||||
|
val downloadUrl: String?,
|
||||||
|
val changelogUrl: String?
|
||||||
|
)
|
||||||
@@ -2,9 +2,13 @@ package com.cherret.zaprett.ui.screens
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Context.MODE_PRIVATE
|
import android.content.Context.MODE_PRIVATE
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Box
|
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.Column
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -12,8 +16,10 @@ import androidx.compose.material.icons.Icons
|
|||||||
import androidx.compose.material.icons.filled.PlayArrow
|
import androidx.compose.material.icons.filled.PlayArrow
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
import androidx.compose.material.icons.filled.RestartAlt
|
||||||
import androidx.compose.material.icons.filled.Stop
|
import androidx.compose.material.icons.filled.Stop
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.FilledTonalButton
|
import androidx.compose.material3.FilledTonalButton
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -21,16 +27,18 @@ import androidx.compose.material3.Scaffold
|
|||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
@@ -40,144 +48,177 @@ import androidx.compose.ui.text.style.TextAlign
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
|
import com.cherret.zaprett.download
|
||||||
|
import com.cherret.zaprett.getChangelog
|
||||||
import com.cherret.zaprett.getStatus
|
import com.cherret.zaprett.getStatus
|
||||||
|
import com.cherret.zaprett.getUpdate
|
||||||
|
import com.cherret.zaprett.installApk
|
||||||
|
import com.cherret.zaprett.registerDownloadListener
|
||||||
import com.cherret.zaprett.restartService
|
import com.cherret.zaprett.restartService
|
||||||
import com.cherret.zaprett.startService
|
import com.cherret.zaprett.startService
|
||||||
import com.cherret.zaprett.stopService
|
import com.cherret.zaprett.stopService
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeScreen() {
|
fun HomeScreen() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
|
val sharedPreferences = remember { context.getSharedPreferences("settings", MODE_PRIVATE) }
|
||||||
val cardText = remember { mutableStateOf(R.string.status_not_availible) }
|
val cardText = remember { mutableIntStateOf(R.string.status_not_availible) }
|
||||||
|
val changeLog = remember { mutableStateOf<String?>(null) }
|
||||||
|
val newVersion = remember { mutableStateOf<String?>(null) }
|
||||||
|
val updateAvailable = remember { mutableStateOf(false) }
|
||||||
|
val downloadUrl = remember { mutableStateOf<String?>(null) }
|
||||||
|
var showUpdateDialog by remember { mutableStateOf(false) }
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) {
|
if (sharedPreferences.getBoolean("auto_update", true)) {
|
||||||
getStatus {
|
getUpdate(context) {
|
||||||
if (it) {
|
if (it != null) {
|
||||||
cardText.value = R.string.status_enabled
|
downloadUrl.value = it.downloadUrl.toString()
|
||||||
}
|
getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log }
|
||||||
else {
|
newVersion.value = it.version?.toString()
|
||||||
cardText.value = R.string.status_disabled
|
updateAvailable.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
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(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
TopAppBar(
|
||||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
title = {
|
||||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
Text(
|
||||||
rotate(degrees = -30f) {
|
text = stringResource(R.string.app_name),
|
||||||
drawOval(
|
fontSize = 40.sp,
|
||||||
color = primaryColor,
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
size = Size(200f, 140f),
|
)
|
||||||
topLeft = Offset(-20f, -20f)
|
},
|
||||||
)
|
windowInsets = WindowInsets(0)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.app_name),
|
|
||||||
fontSize = 40.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
snackbarHost = { SnackbarHost(snackbarHostState) },
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
Column(
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
modifier = Modifier
|
ServiceStatusCard(context, cardText, snackbarHostState, scope)
|
||||||
.padding(paddingValues)
|
UpdateCard(updateAvailable) { showUpdateDialog = true }
|
||||||
) {
|
if (showUpdateDialog) {
|
||||||
ElevatedCard(
|
UpdateDialog(context, downloadUrl.value.orEmpty(), changeLog.value.orEmpty(), newVersion) { showUpdateDialog = false }
|
||||||
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)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
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) {
|
fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
if (sharedPreferences.getBoolean("use_module", false)) {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
getStatus {
|
getStatus { isEnabled ->
|
||||||
if (it) {
|
cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled
|
||||||
cardText.value = R.string.status_enabled
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
cardText.value = R.string.status_disabled
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
}
|
|
||||||
else {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
||||||
}
|
}
|
||||||
@@ -185,18 +226,17 @@ fun onCardClick(context: Context, cardText: MutableState<Int>, snackbarHostState
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
getStatus {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
if (it) {
|
getStatus { isEnabled ->
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started))
|
snackbarHostState.showSnackbar(
|
||||||
}
|
context.getString(
|
||||||
} else {
|
if (isEnabled) R.string.snack_already_started else R.string.snack_starting_service
|
||||||
startService {}
|
)
|
||||||
scope.launch {
|
)
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!isEnabled) startService {}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
@@ -206,22 +246,19 @@ fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, sc
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
getStatus {
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
if (it) {
|
getStatus { isEnabled ->
|
||||||
stopService{}
|
scope.launch {
|
||||||
scope.launch {
|
snackbarHostState.showSnackbar(
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_stopping_service))
|
context.getString(
|
||||||
}
|
if (isEnabled) R.string.snack_stopping_service else R.string.snack_no_service
|
||||||
}
|
)
|
||||||
else {
|
)
|
||||||
scope.launch {
|
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (isEnabled) stopService {}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
||||||
}
|
}
|
||||||
@@ -229,15 +266,45 @@ fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, sco
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
||||||
if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) {
|
val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE)
|
||||||
restartService{}
|
if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
|
restartService {}
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
snackbarHostState.showSnackbar(context.getString(R.string.snack_reload))
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
scope.launch {
|
scope.launch {
|
||||||
snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled))
|
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)}: ${context.packageManager.getPackageInfo(context.packageName, 0).versionName} → ${newVersion.value}\n" +
|
||||||
|
"${stringResource(R.string.alert_changelog)}:\n$changeLog"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDismiss()
|
||||||
|
val downloadId = download(context, downloadUrl)
|
||||||
|
registerDownloadListener(context, downloadId) { uri ->
|
||||||
|
installApk(context, uri)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
Text(stringResource(R.string.btn_update))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(stringResource(R.string.btn_dismiss))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package com.cherret.zaprett.ui.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.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()
|
||||||
|
) {
|
||||||
|
if (hostLists != null) {
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = paddingValues,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
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,30 @@ import android.provider.OpenableColumns
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.layout.*
|
||||||
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.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material.icons.filled.Download
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material.icons.filled.UploadFile
|
||||||
import androidx.compose.material3.FloatingActionButton
|
import androidx.compose.material3.*
|
||||||
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.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.*
|
||||||
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.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation.NavController
|
||||||
import com.cherret.zaprett.R
|
import com.cherret.zaprett.R
|
||||||
import com.cherret.zaprett.disableList
|
import com.cherret.zaprett.*
|
||||||
import com.cherret.zaprett.enableList
|
|
||||||
import com.cherret.zaprett.getActiveLists
|
|
||||||
import com.cherret.zaprett.getAllLists
|
|
||||||
import com.cherret.zaprett.getZaprettPath
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -63,7 +39,7 @@ import java.io.IOException
|
|||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun HostsScreen() {
|
fun HostsScreen(navController: NavController) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var allLists by remember { mutableStateOf(getAllLists()) }
|
var allLists by remember { mutableStateOf(getAllLists()) }
|
||||||
var activeLists by remember { mutableStateOf(getActiveLists()) }
|
var activeLists by remember { mutableStateOf(getActiveLists()) }
|
||||||
@@ -72,139 +48,199 @@ fun HostsScreen() {
|
|||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
val checked = remember {
|
val checked = remember {
|
||||||
mutableStateMapOf<String, Boolean>().apply {
|
mutableStateMapOf<String, Boolean>().apply {
|
||||||
allLists.forEach { list ->
|
allLists.forEach { list -> this[list] = activeLists.contains(list) }
|
||||||
this[list] = activeLists.contains(list)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val filePickerLauncher = rememberLauncherForActivityResult(
|
val filePickerLauncher = rememberLauncherForActivityResult(
|
||||||
contract = ActivityResultContracts.OpenDocument(),
|
contract = ActivityResultContracts.OpenDocument()
|
||||||
onResult = { uri: Uri? ->
|
) { uri ->
|
||||||
uri?.let {
|
uri?.let { copySelectedFile(context, it, snackbarHostState, scope) }
|
||||||
copySelectedFile(context, it, snackbarHostState, scope)
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
TopAppBar(
|
||||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
title = {
|
||||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
Text(
|
||||||
rotate(degrees = -30f) {
|
text = stringResource(R.string.title_hosts),
|
||||||
drawOval(
|
fontSize = 40.sp,
|
||||||
color = primaryColor,
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
size = Size(200f, 140f),
|
)
|
||||||
topLeft = Offset(-30f, -30f)
|
},
|
||||||
)
|
windowInsets = WindowInsets(0)
|
||||||
}
|
)
|
||||||
}
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.title_hosts),
|
|
||||||
fontSize = 40.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
content = { paddingValues ->
|
content = { paddingValues ->
|
||||||
Column(
|
PullToRefreshBox(
|
||||||
modifier = Modifier
|
isRefreshing = isRefreshing,
|
||||||
.padding(paddingValues)
|
onRefresh = {
|
||||||
|
isRefreshing = true
|
||||||
|
allLists = getAllLists()
|
||||||
|
activeLists = getActiveLists()
|
||||||
|
checked.clear()
|
||||||
|
allLists.forEach { list ->
|
||||||
|
checked[list] = activeLists.contains(list)
|
||||||
|
}
|
||||||
|
isRefreshing = false
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
PullToRefreshBox(
|
LazyColumn(
|
||||||
isRefreshing = isRefreshing,
|
contentPadding = paddingValues,
|
||||||
onRefresh = {
|
modifier = Modifier.fillMaxSize()
|
||||||
isRefreshing = true
|
|
||||||
allLists = getAllLists()
|
|
||||||
activeLists = getActiveLists()
|
|
||||||
checked.clear()
|
|
||||||
allLists.forEach { list ->
|
|
||||||
checked[list] = activeLists.contains(list)
|
|
||||||
}
|
|
||||||
isRefreshing = false
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
) {
|
) {
|
||||||
LazyColumn {
|
items(allLists) { item ->
|
||||||
items(allLists) { item ->
|
HostItem(
|
||||||
ElevatedCard(
|
item = item,
|
||||||
elevation = CardDefaults.cardElevation(
|
isChecked = checked[item] == true,
|
||||||
defaultElevation = 6.dp
|
onCheckedChange = { isChecked ->
|
||||||
),
|
checked[item] = isChecked
|
||||||
colors = CardDefaults.cardColors(
|
if (isChecked) enableList(item) else disableList(item)
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceVariant,
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
),
|
},
|
||||||
modifier = Modifier
|
onDeleteClick = {
|
||||||
.fillMaxWidth()
|
if (deleteHost(item)) {
|
||||||
.padding(start = 10.dp, top = 25.dp, end = 10.dp),
|
allLists = getAllLists()
|
||||||
) {
|
activeLists = getActiveLists()
|
||||||
Row(
|
checked.clear()
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
allLists.forEach { list ->
|
||||||
modifier = Modifier
|
checked[list] = activeLists.contains(list)
|
||||||
.fillMaxWidth()
|
}
|
||||||
.padding(16.dp)
|
|
||||||
) {
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
showRestartSnackbar(context, snackbarHostState, scope)
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
FloatingActionButton(modifier = Modifier
|
FloatingMenu(navController, filePickerLauncher)
|
||||||
.size(80.dp, 80.dp),
|
|
||||||
onClick = {addHost(filePickerLauncher)}) {
|
|
||||||
Icon(Icons.Default.Add, contentDescription = "Restart")
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
snackbarHost = {SnackbarHost(hostState = snackbarHostState)}
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun addHost(launcher: ActivityResultLauncher<Array<String>>) {
|
@Composable
|
||||||
launcher.launch(arrayOf("*/*"))
|
private fun HostItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) {
|
||||||
}
|
ElevatedCard(
|
||||||
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||||
fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||||
if (Environment.isExternalStorageManager()) {
|
modifier = Modifier
|
||||||
if (uri == null) return
|
.fillMaxWidth()
|
||||||
val contentResolver = context.contentResolver
|
.padding(start = 10.dp, top = 25.dp, end = 10.dp)
|
||||||
val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
) {
|
||||||
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
|
Row(
|
||||||
if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file"
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
} ?: "copied_file"
|
modifier = Modifier
|
||||||
val outputFile = File(getZaprettPath() + "/lists", fileName)
|
.fillMaxWidth()
|
||||||
try {
|
.padding(16.dp)
|
||||||
contentResolver.openInputStream(uri)?.use { inputStream ->
|
) {
|
||||||
FileOutputStream(outputFile).use { outputStream ->
|
Text(text = item, modifier = Modifier.weight(1f))
|
||||||
inputStream.copyTo(outputStream)
|
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,31 +1,15 @@
|
|||||||
package com.cherret.zaprett.ui.screens
|
package com.cherret.zaprett.ui.screens
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.foundation.Canvas
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.runtime.*
|
||||||
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.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.geometry.Offset
|
|
||||||
import androidx.compose.ui.geometry.Size
|
|
||||||
import androidx.compose.ui.graphics.drawscope.rotate
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.Font
|
import androidx.compose.ui.text.font.Font
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
@@ -38,204 +22,206 @@ import com.cherret.zaprett.checkRoot
|
|||||||
import com.cherret.zaprett.getStartOnBoot
|
import com.cherret.zaprett.getStartOnBoot
|
||||||
import com.cherret.zaprett.setStartOnBoot
|
import com.cherret.zaprett.setStartOnBoot
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsScreen() {
|
fun SettingsScreen() {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
|
||||||
val editor = remember { sharedPreferences.edit() }
|
val editor = remember { sharedPreferences.edit() }
|
||||||
|
|
||||||
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
|
val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) }
|
||||||
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
|
val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) }
|
||||||
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
|
val autoRestart = remember { mutableStateOf(getStartOnBoot()) }
|
||||||
|
val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) }
|
||||||
val openNoRootDialog = remember { mutableStateOf(false) }
|
val openNoRootDialog = remember { mutableStateOf(false) }
|
||||||
val openNoModuleDialog = remember { mutableStateOf(false) }
|
val openNoModuleDialog = remember { mutableStateOf(false) }
|
||||||
showNoRootDialog(openNoRootDialog)
|
var showAboutDialog by remember { mutableStateOf(false) }
|
||||||
showNoModuleDialog(openNoModuleDialog)
|
|
||||||
|
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) {
|
||||||
|
AboutDialog(context, onDismiss = { showAboutDialog = false })
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
val primaryColor = MaterialTheme.colorScheme.surfaceVariant
|
TopAppBar(
|
||||||
Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) {
|
title = {
|
||||||
Canvas(modifier = Modifier.size(100.dp, 50.dp)) {
|
Text(
|
||||||
rotate(degrees = -30f) {
|
text = stringResource(R.string.title_settings),
|
||||||
drawOval(
|
fontSize = 40.sp,
|
||||||
color = primaryColor,
|
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
||||||
size = Size(200f, 140f),
|
)
|
||||||
topLeft = Offset(-30f, -30f)
|
},
|
||||||
)
|
actions = {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
IconButton(onClick = { expanded = !expanded }) {
|
||||||
|
Icon(Icons.Default.MoreVert, contentDescription = null)
|
||||||
}
|
}
|
||||||
}
|
DropdownMenu(
|
||||||
Text(
|
expanded = expanded,
|
||||||
text = stringResource(R.string.title_settings),
|
onDismissRequest = { expanded = false }
|
||||||
fontSize = 40.sp,
|
|
||||||
fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
content = { paddingValues ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(paddingValues)
|
|
||||||
) {
|
|
||||||
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)
|
|
||||||
) {
|
) {
|
||||||
Text(
|
DropdownMenuItem(
|
||||||
text = stringResource(R.string.btn_use_root),
|
text = { Text(stringResource(R.string.about_title)) },
|
||||||
modifier = Modifier.weight(1f)
|
onClick = {
|
||||||
)
|
expanded = false
|
||||||
Switch(
|
showAboutDialog = true
|
||||||
checked = useModule.value,
|
|
||||||
onCheckedChange = { isChecked ->
|
|
||||||
useModule(
|
|
||||||
context,
|
|
||||||
isChecked,
|
|
||||||
updateOnBoot,
|
|
||||||
openNoRootDialog,
|
|
||||||
openNoModuleDialog
|
|
||||||
) {
|
|
||||||
if (it) {
|
|
||||||
useModule.value = isChecked
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Row(
|
},
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
windowInsets = WindowInsets(0)
|
||||||
modifier = Modifier
|
)
|
||||||
.fillMaxWidth()
|
},
|
||||||
.padding(16.dp)
|
content = { paddingValues ->
|
||||||
) {
|
Column(modifier = Modifier.padding(paddingValues)) {
|
||||||
Text(
|
ElevatedCard(
|
||||||
text = stringResource(R.string.btn_update_on_boot),
|
elevation = CardDefaults.cardElevation(defaultElevation = 6.dp),
|
||||||
modifier = Modifier.weight(1f)
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant),
|
||||||
)
|
modifier = Modifier
|
||||||
Switch(
|
.fillMaxWidth()
|
||||||
checked = updateOnBoot.value,
|
.padding(horizontal = 10.dp, vertical = 25.dp)
|
||||||
onCheckedChange = { updateOnBoot.value = it; editor.putBoolean("update_on_boot", it).apply()}
|
) {
|
||||||
)
|
SettingsItem(
|
||||||
}
|
title = stringResource(R.string.btn_use_root),
|
||||||
Row(
|
checked = useModule.value,
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
onCheckedChange = { isChecked ->
|
||||||
modifier = Modifier
|
useModule(
|
||||||
.fillMaxWidth()
|
context = context,
|
||||||
.padding(16.dp)
|
checked = isChecked,
|
||||||
) {
|
updateOnBoot = updateOnBoot,
|
||||||
Text(
|
openNoRootDialog = openNoRootDialog,
|
||||||
text = stringResource(R.string.btn_autorestart),
|
openNoModuleDialog = openNoModuleDialog
|
||||||
modifier = Modifier.weight(1f)
|
) { success ->
|
||||||
)
|
if (success) useModule.value = isChecked
|
||||||
Switch(
|
}
|
||||||
checked = autoRestart.value,
|
}
|
||||||
onCheckedChange = { if (autoRestart(context, it)) autoRestart.value = it;}
|
)
|
||||||
)
|
SettingsItem(
|
||||||
}
|
title = stringResource(R.string.btn_update_on_boot),
|
||||||
|
checked = updateOnBoot.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
updateOnBoot.value = it
|
||||||
|
editor.putBoolean("update_on_boot", it).apply()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SettingsItem(
|
||||||
|
title = stringResource(R.string.btn_autorestart),
|
||||||
|
checked = autoRestart.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (handleAutoRestart(context, it)) autoRestart.value = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
SettingsItem(
|
||||||
|
title = stringResource(R.string.btn_autoupdate),
|
||||||
|
checked = autoUpdate.value,
|
||||||
|
onCheckedChange = {
|
||||||
|
autoUpdate.value = it
|
||||||
|
editor.putBoolean("auto_update", it).apply()
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun useModule(context: Context, checked: Boolean, updateOnBoot: MutableState<Boolean>, openNoRootDialog: MutableState<Boolean>, openNoModuleDialog: MutableState<Boolean>, callback: (Boolean) -> Unit): 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 sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
val editor = sharedPreferences.edit()
|
val editor = sharedPreferences.edit()
|
||||||
if (checked) {
|
if (checked) {
|
||||||
checkRoot {
|
checkRoot { hasRoot ->
|
||||||
if (it) {
|
if (hasRoot) {
|
||||||
checkModuleInstallation {
|
checkModuleInstallation { hasModule ->
|
||||||
if (it) {
|
if (hasModule) {
|
||||||
editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply()
|
editor.putBoolean("use_module", true)
|
||||||
|
.putBoolean("update_on_boot", true)
|
||||||
|
.apply()
|
||||||
updateOnBoot.value = true
|
updateOnBoot.value = true
|
||||||
callback(true)
|
callback(true)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
openNoModuleDialog.value = true
|
openNoModuleDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
openNoRootDialog.value = true
|
openNoRootDialog.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else {
|
editor.putBoolean("use_module", false)
|
||||||
editor.putBoolean("use_module", false).putBoolean("update_on_boot", false)
|
.putBoolean("update_on_boot", false)
|
||||||
.apply()
|
.apply()
|
||||||
return true
|
updateOnBoot.value = false
|
||||||
|
callback(true)
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun autoRestart(context: Context, checked: Boolean): Boolean {
|
private fun handleAutoRestart(context: Context, checked: Boolean): Boolean {
|
||||||
if (context.getSharedPreferences("settings", Context.MODE_PRIVATE).getBoolean("use_module", false)) {
|
val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
|
||||||
|
return if (sharedPreferences.getBoolean("use_module", false)) {
|
||||||
setStartOnBoot(checked)
|
setStartOnBoot(checked)
|
||||||
return true
|
true
|
||||||
}
|
} else {
|
||||||
return false
|
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))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun showNoModuleDialog(openDialog: MutableState<Boolean>) {
|
private fun InfoDialog(title: String, message: String, onDismiss: () -> Unit) {
|
||||||
if (openDialog.value) {
|
AlertDialog(
|
||||||
AlertDialog(
|
title = { Text(text = title) },
|
||||||
title = {
|
text = { Text(text = message) },
|
||||||
Text(text = stringResource(R.string.error_no_module_title))
|
onDismissRequest = onDismiss,
|
||||||
},
|
confirmButton = {
|
||||||
text = {
|
TextButton(onClick = onDismiss) {
|
||||||
Text(text = stringResource(R.string.error_no_module_message))
|
Text(stringResource(R.string.btn_continue))
|
||||||
},
|
}
|
||||||
onDismissRequest = {
|
}
|
||||||
openDialog.value = false
|
)
|
||||||
},
|
}
|
||||||
confirmButton = {
|
|
||||||
TextButton(
|
@Composable
|
||||||
onClick = {
|
private fun AboutDialog(context: Context, onDismiss: () -> Unit) {
|
||||||
openDialog.value = false
|
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
|
||||||
Text(stringResource(R.string.btn_continue))
|
.size(64.dp))},
|
||||||
}
|
text = { Text(text = stringResource(R.string.about_text, context.packageManager.getPackageInfo(context.packageName, 0).versionName.toString())) },
|
||||||
},
|
onDismissRequest = onDismiss,
|
||||||
)
|
confirmButton = { }
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.cherret.zaprett.ui.theme
|
package com.cherret.zaprett.ui.theme
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
|||||||
@@ -2,33 +2,57 @@
|
|||||||
<resources>
|
<resources>
|
||||||
<string name="title_home">Главная</string>
|
<string name="title_home">Главная</string>
|
||||||
<string name="title_hosts">Хосты</string>
|
<string name="title_hosts">Хосты</string>
|
||||||
|
<string name="title_hosts_repo">Репозиторий хостов</string>
|
||||||
<string name="title_settings">Настройки</string>
|
<string name="title_settings">Настройки</string>
|
||||||
<string name="btn_continue">Продолжить</string>
|
<string name="btn_continue">Продолжить</string>
|
||||||
|
<string name="btn_update">Обновить</string>
|
||||||
|
<string name="btn_dismiss">Отмена</string>
|
||||||
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
<string name="text_welcome">Привет в zaprett! Это приложение предназначено для обхода цензуры и иных блокировок. Для полноценной работоспособности необходимо установить Magisk модуль.</string>
|
||||||
<string name="btn_use_root">Использовать модуль</string>
|
<string name="btn_use_root">Использовать модуль</string>
|
||||||
<string name="error_root_title">Root не получен</string>
|
<string name="error_root_title">Root не получен</string>
|
||||||
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
<string name="error_root_message">Не получилось получить root доступ. Дайте права root для использования модуля Magisk</string>
|
||||||
<string name="error_no_module_title">Модуль не установлен</string>
|
<string name="error_no_module_title">Модуль не установлен</string>
|
||||||
<string name="error_no_module_message">Magisk модуль zaprett не найден. Пожалуйста, установите его</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="snack_module_disabled">Модуль Magisk отключен! Не получилось выполнить действие</string>
|
||||||
<string name="title_strategy">Стратегия</string>
|
<string name="title_strategy">Стратегия</string>
|
||||||
<string name="status_not_availible">Состояние zaprett неизвестно. Нажми для обновления</string>
|
<string name="status_not_availible">Состояние zaprett неизвестно. Нажми для обновления</string>
|
||||||
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
<string name="status_enabled">Сервис zaprett работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
<string name="status_disabled">Сервис zaprett не работает. Нажми для обновления состояния</string>
|
||||||
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
<string name="status_crashed">Сервис zaprett вероятно крашнулся. Нажмите кнопку перезапуска ниже</string>
|
||||||
|
<string name="update_available">Доступно новое обновление!</string>
|
||||||
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
<string name="btn_update_on_boot">Обновлять статус при запуске Главной страницы</string>
|
||||||
<string name="btn_start_service">Запустить сервис</string>
|
<string name="btn_start_service">Запустить сервис</string>
|
||||||
<string name="btn_stop_service">Остановить сервис</string>
|
<string name="btn_stop_service">Остановить сервис</string>
|
||||||
<string name="btn_restart_service">Перезапустить сервис</string>
|
<string name="btn_restart_service">Перезапустить сервис</string>
|
||||||
|
<string name="btn_remove_host">Удалить</string>
|
||||||
|
<string name="btn_download_host">Скачать</string>
|
||||||
|
<string name="btn_add_host">Добавить</string>
|
||||||
|
<string name="btn_back">Назад</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_autorestart">Переодически перезапускать сервис</string>
|
||||||
|
<string name="btn_autoupdate">Авто обновление</string>
|
||||||
|
<string name="alert_version">Версия</string>
|
||||||
|
<string name="alert_changelog">Список изменений</string>
|
||||||
<string name="snack_already_started">Сервис уже запущен.</string>
|
<string name="snack_already_started">Сервис уже запущен.</string>
|
||||||
<string name="snack_starting_service">Запускаем сервис...</string>
|
<string name="snack_starting_service">Запускаем сервис…</string>
|
||||||
<string name="snack_no_service">Сервис не запущен</string>
|
<string name="snack_no_service">Сервис не запущен</string>
|
||||||
<string name="snack_stopping_service">Останавливаем сервис...</string>
|
<string name="snack_stopping_service">Останавливаем сервис…</string>
|
||||||
<string name="error_no_storage_title">Нет разрешения</string>
|
<string name="error_no_storage_title">Нет разрешения</string>
|
||||||
<string name="error_no_storage_message">Для правильной работы приложения необходимо разрешение на доступ к хранилищу</string>
|
<string name="error_no_storage_message">Для правильной работы приложения необходимо разрешение на доступ к хранилищу</string>
|
||||||
<string name="btn_show_full_path">Показывать полный путь к листу</string>
|
<string name="btn_show_full_path">Показывать полный путь к листу</string>
|
||||||
<string name="pls_reboot_snack">Перезагрузите устройство для вступления изменений в силу</string>
|
<string name="pls_reboot_snack">Перезагрузите устройство для вступления изменений в силу</string>
|
||||||
<string name="pls_restart_snack">Перезапустите zaprett для вступления изменений в силу</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>
|
</resources>
|
||||||
@@ -2,28 +2,44 @@
|
|||||||
<string name="app_name" translatable="false">zaprett</string>
|
<string name="app_name" translatable="false">zaprett</string>
|
||||||
<string name="title_home">Home</string>
|
<string name="title_home">Home</string>
|
||||||
<string name="title_hosts">Hosts</string>
|
<string name="title_hosts">Hosts</string>
|
||||||
|
<string name="title_hosts_repo">Hosts Repo</string>
|
||||||
<string name="title_settings">Settings</string>
|
<string name="title_settings">Settings</string>
|
||||||
<string name="btn_continue">Continue</string>
|
<string name="btn_continue">Continue</string>
|
||||||
|
<string name="btn_update">Update</string>
|
||||||
|
<string name="btn_dismiss">Dismiss</string>
|
||||||
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
<string name="text_welcome">Hello to zaprett! This application is designed to bypass censorship and other blockages. For full functionality you need to install Magisk module.</string>
|
||||||
<string name="btn_use_root">Use module</string>
|
<string name="btn_use_root">Use module</string>
|
||||||
<string name="error_root_title">Can\'t get root</string>
|
<string name="error_root_title">Can\'t get root</string>
|
||||||
<string name="error_root_message">Couldn\'t get root access. Give root access to use the Magisk module</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_title">Module is not installed</string>
|
||||||
<string name="error_no_module_message">Magisk module zaprett wasn\'t found. Please install it</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="snack_module_disabled">Magisk module disabled! Cant\'t execute action</string>
|
||||||
<string name="title_strategy">Strategy</string>
|
<string name="title_strategy">Strategy</string>
|
||||||
<string name="status_not_availible">Status of zaprett is unknown. Tap to update</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_enabled">zaprett service is working. Tap to update</string>
|
||||||
<string name="status_disabled">zaprett service disabled.</string>
|
<string name="status_disabled">zaprett service disabled.</string>
|
||||||
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
|
<string name="status_crashed">zaprett service crashed. Tap restart button below</string>
|
||||||
|
<string name="update_available">New update available!</string>
|
||||||
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
|
<string name="btn_update_on_boot">Update the status when the Home page is launched</string>
|
||||||
<string name="btn_start_service">Start service</string>
|
<string name="btn_start_service">Start service</string>
|
||||||
<string name="btn_stop_service">Stop service</string>
|
<string name="btn_stop_service">Stop service</string>
|
||||||
<string name="btn_restart_service">Restart service</string>
|
<string name="btn_restart_service">Restart service</string>
|
||||||
|
<string name="btn_remove_host">Remove</string>
|
||||||
|
<string name="btn_download_host">Download</string>
|
||||||
|
<string name="btn_add_host">Add</string>
|
||||||
|
<string name="btn_back">Back</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_autorestart">Restart service periodicaly</string>
|
||||||
|
<string name="btn_autoupdate">Autoupdate</string>
|
||||||
|
<string name="alert_version">Version</string>
|
||||||
|
<string name="alert_changelog">Changelog</string>
|
||||||
<string name="snack_already_started">Service already started.</string>
|
<string name="snack_already_started">Service already started.</string>
|
||||||
<string name="snack_starting_service">Starting service...</string>
|
<string name="snack_starting_service">Starting service…</string>
|
||||||
<string name="snack_no_service">Service is not launched</string>
|
<string name="snack_no_service">Service is not launched</string>
|
||||||
<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_title">No permission</string>
|
||||||
@@ -31,4 +47,12 @@
|
|||||||
<string name="btn_show_full_path">Show full list\'s path</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_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="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>
|
</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.android.application) apply false
|
||||||
alias(libs.plugins.kotlin.android) apply false
|
alias(libs.plugins.kotlin.android) apply false
|
||||||
alias(libs.plugins.kotlin.compose) apply false
|
alias(libs.plugins.kotlin.compose) apply false
|
||||||
|
id("com.google.gms.google-services") version "4.4.2" apply false
|
||||||
|
id("com.google.firebase.crashlytics") version "3.0.3" apply false
|
||||||
}
|
}
|
||||||
@@ -1,2 +1 @@
|
|||||||
Исправление ошибок
|
Кнопка в QS
|
||||||
Система обнновлений
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": "1.2",
|
"version": "1.6",
|
||||||
"versionCode": 3,
|
"versionCode": 7,
|
||||||
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1.2.0/app-release.apk",
|
"downloadUrl": "https://github.com/CherretGit/zaprett-app/releases/download/1_6_0/app-release.apk",
|
||||||
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
"changelogUrl": "https://raw.githubusercontent.com/CherretGit/zaprett-app/refs/heads/main/changelog.md"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user