From 458741485b9c8d207c25ceac46b438f9242de464 Mon Sep 17 00:00:00 2001 From: Cherret Date: Sun, 27 Apr 2025 23:55:30 +0700 Subject: [PATCH] hosts repo and code refactor --- app/build.gradle.kts | 4 +- .../java/com/cherret/zaprett/MainActivity.kt | 49 ++- .../main/java/com/cherret/zaprett/Module.kt | 6 +- .../cherret/zaprett/RepositoryDownloader.kt | 101 +++++ .../main/java/com/cherret/zaprett/Updater.kt | 4 +- .../cherret/zaprett/ui/screens/HomeScreen.kt | 335 +++++++------- .../zaprett/ui/screens/HostsRepoScreen.kt | 234 ++++++++++ .../cherret/zaprett/ui/screens/HostsScreen.kt | 410 ++++++++---------- .../zaprett/ui/screens/SettingsScreen.kt | 355 +++++++-------- app/src/main/res/values-ru/strings.xml | 11 + app/src/main/res/values/strings.xml | 11 + app/src/main/res/xml/file_paths.xml | 1 + 12 files changed, 887 insertions(+), 634 deletions(-) create mode 100644 app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt create mode 100644 app/src/main/java/com/cherret/zaprett/ui/screens/HostsRepoScreen.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 45f5b84..88df42c 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,8 +14,8 @@ android { applicationId = "com.cherret.zaprett" minSdk = 30 targetSdk = 35 - versionCode = 7 - versionName = "1.6" + versionCode = 8 + versionName = "1.7" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/cherret/zaprett/MainActivity.kt b/app/src/main/java/com/cherret/zaprett/MainActivity.kt index a43472e..7100361 100644 --- a/app/src/main/java/com/cherret/zaprett/MainActivity.kt +++ b/app/src/main/java/com/cherret/zaprett/MainActivity.kt @@ -37,6 +37,7 @@ import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController import com.cherret.zaprett.ui.screens.HomeScreen +import com.cherret.zaprett.ui.screens.HostsRepoScreen import com.cherret.zaprett.ui.screens.HostsScreen import com.cherret.zaprett.ui.screens.SettingsScreen import com.cherret.zaprett.ui.theme.ZaprettTheme @@ -50,6 +51,7 @@ sealed class Screen(val route: String, @StringRes val nameResId: Int, val icon: object settings : Screen("settings", R.string.title_settings, Icons.Default.Settings) } val topLevelRoutes = listOf(Screen.home, Screen.hosts, Screen.settings) +val hideNavBar = listOf("hosts_repo") class MainActivity : ComponentActivity() { private lateinit var firebaseAnalytics: FirebaseAnalytics override fun onCreate(savedInstanceState: Bundle?) { @@ -80,29 +82,31 @@ class MainActivity : ComponentActivity() { val navController = rememberNavController() Scaffold( bottomBar = { - NavigationBar { - val navBackStackEntry = navController.currentBackStackEntryAsState().value - val currentDestination = navBackStackEntry?.destination - topLevelRoutes.forEach { topLevelRoute -> - NavigationBarItem( - icon = { - Icon( - topLevelRoute.icon, - contentDescription = stringResource(id = topLevelRoute.nameResId) - ) - }, - label = { Text(text = stringResource(id = topLevelRoute.nameResId)) }, - selected = currentDestination?.route == topLevelRoute.route, - onClick = { - navController.navigate(topLevelRoute.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true + val navBackStackEntry = navController.currentBackStackEntryAsState().value + val currentDestination = navBackStackEntry?.destination + if (currentDestination?.route !in hideNavBar) { + NavigationBar { + topLevelRoutes.forEach { topLevelRoute -> + NavigationBarItem( + icon = { + Icon( + topLevelRoute.icon, + contentDescription = stringResource(id = topLevelRoute.nameResId) + ) + }, + label = { Text(text = stringResource(id = topLevelRoute.nameResId)) }, + selected = currentDestination?.route == topLevelRoute.route, + onClick = { + navController.navigate(topLevelRoute.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true } - launchSingleTop = true - restoreState = true } - } - ) + ) + } } } } @@ -113,8 +117,9 @@ class MainActivity : ComponentActivity() { Modifier.padding(innerPadding) ) { composable(Screen.home.route) { HomeScreen() } - composable(Screen.hosts.route) { HostsScreen() } + composable(Screen.hosts.route) { HostsScreen(navController) } composable(Screen.settings.route) { SettingsScreen() } + composable("hosts_repo") { HostsRepoScreen(navController) } } } } diff --git a/app/src/main/java/com/cherret/zaprett/Module.kt b/app/src/main/java/com/cherret/zaprett/Module.kt index 12fe064..f5c5ec4 100644 --- a/app/src/main/java/com/cherret/zaprett/Module.kt +++ b/app/src/main/java/com/cherret/zaprett/Module.kt @@ -79,7 +79,7 @@ fun getStartOnBoot(): Boolean { } else { false } - } catch (e: IOException) { + } catch (_: IOException) { false } } @@ -92,12 +92,12 @@ fun getZaprettPath(): String { FileInputStream(configFile).use { input -> props.load(input) } - props.getProperty("zaprettdir", "/sdcard/zaprett") + props.getProperty("zaprettdir", Environment.getExternalStorageDirectory().path + "/zaprett") } catch (e: IOException) { throw RuntimeException(e) } } - return "/sdcard/zaprett" + return Environment.getExternalStorageDirectory().path + "/zaprett" } fun getAllLists(): Array { diff --git a/app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt b/app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt new file mode 100644 index 0000000..9ce273e --- /dev/null +++ b/app/src/main/java/com/cherret/zaprett/RepositoryDownloader.kt @@ -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?) -> 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>(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 +) \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/Updater.kt b/app/src/main/java/com/cherret/zaprett/Updater.kt index a891816..c8217b2 100644 --- a/app/src/main/java/com/cherret/zaprett/Updater.kt +++ b/app/src/main/java/com/cherret/zaprett/Updater.kt @@ -23,7 +23,7 @@ import okhttp3.Response import okio.IOException import java.io.File -val client = OkHttpClient() +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() @@ -138,5 +138,5 @@ data class UpdateInfo( val version: String?, val versionCode: Int?, val downloadUrl: String?, - val changelogUrl: String?, + val changelogUrl: String? ) \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/screens/HomeScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screens/HomeScreen.kt index d2abdb3..57a5c24 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screens/HomeScreen.kt @@ -7,9 +7,8 @@ import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -20,6 +19,7 @@ import androidx.compose.material.icons.filled.Stop import androidx.compose.material3.AlertDialog import androidx.compose.material3.CardDefaults import androidx.compose.material3.ElevatedCard +import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -28,6 +28,7 @@ import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Text import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState @@ -38,9 +39,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font @@ -62,6 +60,7 @@ import com.cherret.zaprett.stopService import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch +@OptIn(ExperimentalMaterial3Api::class) @Composable fun HomeScreen() { val context = LocalContext.current @@ -69,180 +68,157 @@ fun HomeScreen() { val cardText = remember { mutableIntStateOf(R.string.status_not_availible) } val changeLog = remember { mutableStateOf(null) } val newVersion = remember { mutableStateOf(null) } - val updateAvailable = remember {mutableStateOf(false)} + val updateAvailable = remember { mutableStateOf(false) } val downloadUrl = remember { mutableStateOf(null) } var showUpdateDialog by remember { mutableStateOf(false) } val scope = rememberCoroutineScope() val snackbarHostState = remember { SnackbarHostState() } + LaunchedEffect(Unit) { if (sharedPreferences.getBoolean("auto_update", true)) { getUpdate(context) { if (it != null) { downloadUrl.value = it.downloadUrl.toString() - getChangelog(it.changelogUrl.toString()) { - changeLog.value = it - } + getChangelog(it.changelogUrl.toString()) { log -> changeLog.value = log } newVersion.value = it.version?.toString() updateAvailable.value = true } } } if (sharedPreferences.getBoolean("use_module", false) && sharedPreferences.getBoolean("update_on_boot", false)) { - getStatus { - if (it) { - cardText.value = R.string.status_enabled - } - else { - cardText.value = R.string.status_disabled - } + getStatus { isEnabled -> + cardText.intValue = if (isEnabled) R.string.status_enabled else R.string.status_disabled } } } + Scaffold( topBar = { - val primaryColor = MaterialTheme.colorScheme.surfaceVariant - Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { - Canvas(modifier = Modifier.size(100.dp, 50.dp)) { - rotate(degrees = -30f) { - drawOval( - color = primaryColor, - size = Size(200f, 140f), - topLeft = Offset(-20f, -20f) - ) - } - } - Text( - text = stringResource(R.string.app_name), - fontSize = 40.sp, - fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) - ) - } + TopAppBar( + title = { + Text( + text = stringResource(R.string.app_name), + fontSize = 40.sp, + fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) + ) + }, + windowInsets = WindowInsets(0) + ) }, + snackbarHost = { SnackbarHost(snackbarHostState) }, content = { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) - ) { - ElevatedCard( - elevation = CardDefaults.cardElevation( - defaultElevation = 6.dp - ), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.primary, - ), - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, top = 25.dp, end = 10.dp) - .size(width = 240.dp, height = 150.dp), - onClick = { onCardClick(context, cardText, snackbarHostState, scope) } - ) { - Text( - text = stringResource(cardText.value), - fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)), - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - textAlign = TextAlign.Center, - ) - } - AnimatedVisibility( - visible = updateAvailable.value, - enter = fadeIn() + expandVertically(), - exit = shrinkVertically() + fadeOut() - ) { - ElevatedCard( - elevation = CardDefaults.cardElevation( - defaultElevation = 6.dp - ), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceVariant, - ), - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, top = 10.dp, end = 10.dp) - .size(width = 140.dp, height = 70.dp), - onClick = { - showUpdateDialog = true - } - ) - { - Text( - text = stringResource(R.string.update_available), - fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)), - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - textAlign = TextAlign.Center, - ) - } - } + Column(modifier = Modifier.padding(paddingValues)) { + ServiceStatusCard(context, cardText, snackbarHostState, scope) + UpdateCard(updateAvailable) { showUpdateDialog = true } if (showUpdateDialog) { - UpdateDialog(context, downloadUrl.value.toString(), changeLog.value.toString(), newVersion, onDismiss = { showUpdateDialog = false }) - } - 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) - ) + UpdateDialog(context, downloadUrl.value.orEmpty(), changeLog.value.orEmpty(), newVersion) { showUpdateDialog = false } } + ServiceControlButtons(context, snackbarHostState, scope) } - }, - snackbarHost = {SnackbarHost(hostState = snackbarHostState)} + } ) } + +@Composable +private fun ServiceStatusCard(context: Context, cardText: MutableState, 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, 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, snackbarHostState: SnackbarHostState, scope: CoroutineScope) { val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) if (sharedPreferences.getBoolean("use_module", false)) { - getStatus { - if (it) { - cardText.value = R.string.status_enabled - } - else { - cardText.value = R.string.status_disabled - } + getStatus { isEnabled -> + cardText.value = if (isEnabled) R.string.status_enabled else R.string.status_disabled } - - } - else { + } else { scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled)) } @@ -250,18 +226,17 @@ fun onCardClick(context: Context, cardText: MutableState, snackbarHostState } fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) { - if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) { - getStatus { - if (it) { - scope.launch { - snackbarHostState.showSnackbar(context.getString(R.string.snack_already_started)) - } - } else { - startService {} - scope.launch { - snackbarHostState.showSnackbar(context.getString(R.string.snack_starting_service)) - } + val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) + if (sharedPreferences.getBoolean("use_module", false)) { + getStatus { isEnabled -> + scope.launch { + snackbarHostState.showSnackbar( + context.getString( + if (isEnabled) R.string.snack_already_started else R.string.snack_starting_service + ) + ) } + if (!isEnabled) startService {} } } else { scope.launch { @@ -271,22 +246,19 @@ fun onBtnStartService(context: Context, snackbarHostState: SnackbarHostState, sc } fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) { - if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) { - getStatus { - if (it) { - stopService{} - scope.launch { - snackbarHostState.showSnackbar(context.getString(R.string.snack_stopping_service)) - } - } - else { - scope.launch { - snackbarHostState.showSnackbar(context.getString(R.string.snack_no_service)) - } + val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) + if (sharedPreferences.getBoolean("use_module", false)) { + getStatus { isEnabled -> + scope.launch { + snackbarHostState.showSnackbar( + context.getString( + if (isEnabled) R.string.snack_stopping_service else R.string.snack_no_service + ) + ) } + if (isEnabled) stopService {} } - } - else { + } else { scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled)) } @@ -294,13 +266,13 @@ fun onBtnStopService(context: Context, snackbarHostState: SnackbarHostState, sco } fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: CoroutineScope) { - if (context.getSharedPreferences("settings", MODE_PRIVATE).getBoolean("use_module", false)) { - restartService{} + val sharedPreferences = context.getSharedPreferences("settings", MODE_PRIVATE) + if (sharedPreferences.getBoolean("use_module", false)) { + restartService {} scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.snack_reload)) } - } - else { + } else { scope.launch { snackbarHostState.showSnackbar(context.getString(R.string.snack_module_disabled)) } @@ -311,7 +283,12 @@ fun onBtnRestart(context: Context, snackbarHostState: SnackbarHostState, scope: fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVersion: MutableState, 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") }, + 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 = { @@ -330,4 +307,4 @@ fun UpdateDialog(context: Context, downloadUrl: String, changeLog: String, newVe } } ) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/cherret/zaprett/ui/screens/HostsRepoScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screens/HostsRepoScreen.kt new file mode 100644 index 0000000..9c5095d --- /dev/null +++ b/app/src/main/java/com/cherret/zaprett/ui/screens/HostsRepoScreen.kt @@ -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?>(null) } + val isUpdate = remember { mutableStateMapOf() } + 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)} + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/cherret/zaprett/ui/screens/HostsScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screens/HostsScreen.kt index 87131fc..5a37468 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screens/HostsScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screens/HostsScreen.kt @@ -7,51 +7,19 @@ import android.provider.OpenableColumns import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.RestartAlt -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -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.SnackbarResult -import androidx.compose.material3.Switch -import androidx.compose.material3.Text +import androidx.compose.material.icons.filled.Download +import androidx.compose.material.icons.filled.UploadFile +import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateMapOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.Snapshot -import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font @@ -60,13 +28,9 @@ 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.R -import com.cherret.zaprett.disableList -import com.cherret.zaprett.enableList -import com.cherret.zaprett.getActiveLists -import com.cherret.zaprett.getAllLists -import com.cherret.zaprett.getZaprettPath -import com.cherret.zaprett.restartService +import com.cherret.zaprett.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import java.io.File @@ -75,7 +39,7 @@ import java.io.IOException @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HostsScreen() { +fun HostsScreen(navController: NavController) { val context = LocalContext.current var allLists by remember { mutableStateOf(getAllLists()) } var activeLists by remember { mutableStateOf(getActiveLists()) } @@ -84,221 +48,199 @@ fun HostsScreen() { var isRefreshing by remember { mutableStateOf(false) } val checked = remember { mutableStateMapOf().apply { - allLists.forEach { list -> - this[list] = activeLists.contains(list) - } + allLists.forEach { list -> this[list] = activeLists.contains(list) } } } val filePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.OpenDocument(), - onResult = { uri: Uri? -> - uri?.let { - copySelectedFile(context, it, snackbarHostState, scope) - } - } - ) + contract = ActivityResultContracts.OpenDocument() + ) { uri -> + uri?.let { copySelectedFile(context, it, snackbarHostState, scope) } + } + Scaffold( topBar = { - val primaryColor = MaterialTheme.colorScheme.surfaceVariant - Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { - Canvas(modifier = Modifier.size(100.dp, 50.dp)) { - rotate(degrees = -30f) { - drawOval( - color = primaryColor, - size = Size(200f, 140f), - topLeft = Offset(-30f, -30f) - ) - } - } - Text( - text = stringResource(R.string.title_hosts), - fontSize = 40.sp, - fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) - ) - } + TopAppBar( + title = { + Text( + text = stringResource(R.string.title_hosts), + fontSize = 40.sp, + fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) + ) + }, + windowInsets = WindowInsets(0) + ) }, - content = { paddingValues -> - Column( - modifier = Modifier - .padding(paddingValues) + content = { paddingValues -> + PullToRefreshBox( + isRefreshing = isRefreshing, + onRefresh = { + isRefreshing = true + allLists = getAllLists() + activeLists = getActiveLists() + checked.clear() + allLists.forEach { list -> + checked[list] = activeLists.contains(list) + } + isRefreshing = false + }, + modifier = Modifier.fillMaxSize() ) { - PullToRefreshBox( - isRefreshing = isRefreshing, - onRefresh = { - isRefreshing = true - allLists = getAllLists() - activeLists = getActiveLists() - checked.clear() - allLists.forEach { list -> - checked[list] = activeLists.contains(list) - } - isRefreshing = false - }, - modifier = Modifier + LazyColumn( + contentPadding = paddingValues, + modifier = Modifier.fillMaxSize() ) { - LazyColumn ( - contentPadding = PaddingValues(bottom = 25.dp) - ){ - items(allLists) { item -> - ElevatedCard( - elevation = CardDefaults.cardElevation( - defaultElevation = 6.dp - ), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainer, - ), - modifier = Modifier - .fillMaxWidth() - .padding(start = 10.dp, top = 25.dp, end = 10.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = item, - modifier = Modifier.weight(1f) - ) - Switch( - checked = checked[item] == true, - onCheckedChange = { isChecked -> - checked[item] = isChecked - if (isChecked) { - enableList(item) - } else { - disableList(item) - } - scope.launch { - val result = snackbarHostState.showSnackbar( - context.getString(R.string.pls_restart_snack), - actionLabel = context.getString(R.string.btn_restart_service) - ) - when (result) { - SnackbarResult.ActionPerformed -> { - restartService {} - scope.launch { - snackbarHostState.showSnackbar( - context.getString( - R.string.snack_reload - ) - ) - } - } - SnackbarResult.Dismissed -> {} - } - } - } - ) - } - HorizontalDivider(thickness = Dp.Hairline) - Row (modifier = Modifier - .fillMaxWidth(), - horizontalArrangement = Arrangement.End) { - FilledTonalButton( - onClick = { - if (deleteHost(item)) { - allLists = getAllLists() - activeLists = getActiveLists() - checked.clear() - allLists.forEach { list -> - checked[list] = activeLists.contains(list) - } - } - scope.launch { - val result = snackbarHostState.showSnackbar( - context.getString(R.string.pls_restart_snack), - actionLabel = context.getString(R.string.btn_restart_service) - ) - when (result) { - SnackbarResult.ActionPerformed -> { - restartService {} - scope.launch { - snackbarHostState.showSnackbar( - context.getString( - R.string.snack_reload - ) - ) - } - } - SnackbarResult.Dismissed -> {} - } - } - }, - modifier = Modifier - .padding(start = 5.dp, end = 5.dp), - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.btn_remove_host), - modifier = Modifier.size(20.dp) - ) - Text( - stringResource(R.string.btn_remove_host) - ) + items(allLists) { item -> + HostItem( + item = item, + isChecked = checked[item] == true, + onCheckedChange = { isChecked -> + checked[item] = isChecked + if (isChecked) enableList(item) else disableList(item) + showRestartSnackbar(context, snackbarHostState, scope) + }, + onDeleteClick = { + if (deleteHost(item)) { + allLists = getAllLists() + activeLists = getActiveLists() + checked.clear() + allLists.forEach { list -> + checked[list] = activeLists.contains(list) } } + showRestartSnackbar(context, snackbarHostState, scope) } - } + ) } } } }, floatingActionButton = { - FloatingActionButton(modifier = Modifier - .size(80.dp, 80.dp), - onClick = {addHost(filePickerLauncher)}) { - Icon(Icons.Default.Add, contentDescription = "Restart") - } + FloatingMenu(navController, filePickerLauncher) }, - snackbarHost = {SnackbarHost(hostState = snackbarHostState)} + snackbarHost = { SnackbarHost(hostState = snackbarHostState) } ) } -fun addHost(launcher: ActivityResultLauncher>) { - launcher.launch(arrayOf("text/plain")) -} - -fun copySelectedFile(context: Context, uri: Uri?, snackbarHostState: SnackbarHostState, scope: CoroutineScope) {// AI Generated - if (Environment.isExternalStorageManager()) { - if (uri == null) return - val contentResolver = context.contentResolver - val fileName = contentResolver.query(uri, null, null, null, null)?.use { cursor -> - val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) - if (cursor.moveToFirst() && nameIndex != -1) cursor.getString(nameIndex) else "copied_file" - } ?: "copied_file" - val outputFile = File(getZaprettPath() + "/lists", fileName) - try { - contentResolver.openInputStream(uri)?.use { inputStream -> - FileOutputStream(outputFile).use { outputStream -> - inputStream.copyTo(outputStream) - } +@Composable +private fun HostItem(item: String, isChecked: Boolean, onCheckedChange: (Boolean) -> Unit, onDeleteClick: () -> Unit) { + ElevatedCard( + elevation = CardDefaults.cardElevation(defaultElevation = 6.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer), + modifier = Modifier + .fillMaxWidth() + .padding(start = 10.dp, top = 25.dp, end = 10.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Text(text = item, modifier = Modifier.weight(1f)) + Switch(checked = isChecked, onCheckedChange = onCheckedChange) + } + HorizontalDivider(thickness = Dp.Hairline) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.End + ) { + FilledTonalButton( + onClick = onDeleteClick, + modifier = Modifier.padding(horizontal = 5.dp) + ) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.btn_remove_host), + modifier = Modifier.size(20.dp) + ) + Text(stringResource(R.string.btn_remove_host)) } - scope.launch { - val result = snackbarHostState.showSnackbar(context.getString(R.string.pls_restart_snack), actionLabel = context.getString(R.string.btn_restart_service)) - when (result) { - SnackbarResult.ActionPerformed -> { - restartService{} - scope.launch { - snackbarHostState.showSnackbar(context.getString(R.string.snack_reload)) - } - } - SnackbarResult.Dismissed -> {} - } - } - } catch (e: IOException) { - e.printStackTrace() } } } -fun deleteHost(item: String): Boolean { - val hostFile = File(item) - if (hostFile.exists()) { - hostFile.delete() - return true +@Composable +private fun FloatingMenu(navController: NavController, launcher: ActivityResultLauncher>) { + 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)) + } + ) } - return false } +private fun addHost(launcher: ActivityResultLauncher>) { + 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)) + } + } +} diff --git a/app/src/main/java/com/cherret/zaprett/ui/screens/SettingsScreen.kt b/app/src/main/java/com/cherret/zaprett/ui/screens/SettingsScreen.kt index db6a9fa..d1ad553 100644 --- a/app/src/main/java/com/cherret/zaprett/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/cherret/zaprett/ui/screens/SettingsScreen.kt @@ -1,31 +1,15 @@ package com.cherret.zaprett.ui.screens import android.content.Context -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.geometry.Offset -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.drawscope.rotate import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily @@ -38,219 +22,206 @@ import com.cherret.zaprett.checkRoot import com.cherret.zaprett.getStartOnBoot import com.cherret.zaprett.setStartOnBoot +@OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen() { val context = LocalContext.current val sharedPreferences = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } val editor = remember { sharedPreferences.edit() } + val useModule = remember { mutableStateOf(sharedPreferences.getBoolean("use_module", false)) } val updateOnBoot = remember { mutableStateOf(sharedPreferences.getBoolean("update_on_boot", false)) } val autoRestart = remember { mutableStateOf(getStartOnBoot()) } val autoUpdate = remember { mutableStateOf(sharedPreferences.getBoolean("auto_update", true)) } val openNoRootDialog = remember { mutableStateOf(false) } val openNoModuleDialog = remember { mutableStateOf(false) } - showNoRootDialog(openNoRootDialog) - showNoModuleDialog(openNoModuleDialog) + var showAboutDialog by remember { mutableStateOf(false) } + + 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( topBar = { - val primaryColor = MaterialTheme.colorScheme.surfaceVariant - Box(modifier = Modifier.padding(start = 20.dp, top = 10.dp)) { - Canvas(modifier = Modifier.size(100.dp, 50.dp)) { - rotate(degrees = -30f) { - drawOval( - color = primaryColor, - size = Size(200f, 140f), - topLeft = Offset(-30f, -30f) - ) + TopAppBar( + title = { + Text( + text = stringResource(R.string.title_settings), + fontSize = 40.sp, + fontFamily = FontFamily(Font(R.font.unbounded, FontWeight.Normal)) + ) + }, + actions = { + var expanded by remember { mutableStateOf(false) } + IconButton(onClick = { expanded = !expanded }) { + Icon(Icons.Default.MoreVert, contentDescription = null) } - } - Text( - text = stringResource(R.string.title_settings), - 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) + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } ) { - Text( - text = stringResource(R.string.btn_use_root), - modifier = Modifier.weight(1f) - ) - Switch( - checked = useModule.value, - onCheckedChange = { isChecked -> - useModule( - context, - isChecked, - updateOnBoot, - openNoRootDialog, - openNoModuleDialog - ) { - if (it) { - useModule.value = isChecked - } - } + DropdownMenuItem( + text = { Text(stringResource(R.string.about_title)) }, + onClick = { + expanded = false + showAboutDialog = true } ) } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = stringResource(R.string.btn_update_on_boot), - modifier = Modifier.weight(1f) - ) - Switch( - checked = updateOnBoot.value, - onCheckedChange = { updateOnBoot.value = it; editor.putBoolean("update_on_boot", it).apply()} - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = stringResource(R.string.btn_autorestart), - modifier = Modifier.weight(1f) - ) - Switch( - checked = autoRestart.value, - onCheckedChange = { if (autoRestart(context, it)) autoRestart.value = it;} - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - ) { - Text( - text = stringResource(R.string.btn_autoupdate), - modifier = Modifier.weight(1f) - ) - Switch( - checked = autoUpdate.value, - onCheckedChange = { autoUpdate.value = it; editor.putBoolean("auto_update", it).apply()} - ) - } + }, + windowInsets = WindowInsets(0) + ) + }, + 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(horizontal = 10.dp, vertical = 25.dp) + ) { + SettingsItem( + title = stringResource(R.string.btn_use_root), + checked = useModule.value, + onCheckedChange = { isChecked -> + useModule( + context = context, + checked = isChecked, + updateOnBoot = updateOnBoot, + openNoRootDialog = openNoRootDialog, + openNoModuleDialog = openNoModuleDialog + ) { success -> + if (success) useModule.value = isChecked + } + } + ) + 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, openNoRootDialog: MutableState, openNoModuleDialog: MutableState, 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, openNoRootDialog: MutableState, openNoModuleDialog: MutableState, callback: (Boolean) -> Unit) { val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val editor = sharedPreferences.edit() if (checked) { - checkRoot { - if (it) { - checkModuleInstallation { - if (it) { - editor.putBoolean("use_module", true).putBoolean("update_on_boot", true).apply() + checkRoot { hasRoot -> + if (hasRoot) { + checkModuleInstallation { hasModule -> + if (hasModule) { + editor.putBoolean("use_module", true) + .putBoolean("update_on_boot", true) + .apply() updateOnBoot.value = true callback(true) - } - else { + } else { openNoModuleDialog.value = true } } - } - else { + } else { openNoRootDialog.value = true } } - } - else { - editor.putBoolean("use_module", false).putBoolean("update_on_boot", false).apply() + } else { + editor.putBoolean("use_module", false) + .putBoolean("update_on_boot", false) + .apply() updateOnBoot.value = false callback(true) } - return false } -fun autoRestart(context: Context, checked: Boolean): Boolean { - if (context.getSharedPreferences("settings", Context.MODE_PRIVATE).getBoolean("use_module", false)) { +private fun handleAutoRestart(context: Context, checked: Boolean): Boolean { + val sharedPreferences = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + return if (sharedPreferences.getBoolean("use_module", false)) { setStartOnBoot(checked) - return true - } - return false -} - -@Composable -fun showNoRootDialog(openDialog: MutableState) { - if (openDialog.value) { - AlertDialog( - title = { - Text(text = stringResource(R.string.error_root_title)) - }, - text = { - Text(text = stringResource(R.string.error_root_message)) - }, - onDismissRequest = { - openDialog.value = false - }, - confirmButton = { - TextButton( - onClick = { - openDialog.value = false - } - ) { - Text(stringResource(R.string.btn_continue)) - } - }, - ) + true + } else { + false } } @Composable -fun showNoModuleDialog(openDialog: MutableState) { - if (openDialog.value) { - AlertDialog( - title = { - Text(text = stringResource(R.string.error_no_module_title)) - }, - text = { - Text(text = stringResource(R.string.error_no_module_message)) - }, - onDismissRequest = { - openDialog.value = false - }, - confirmButton = { - TextButton( - onClick = { - openDialog.value = false - } - ) { - Text(stringResource(R.string.btn_continue)) - } - }, - ) - } +private fun InfoDialog(title: String, message: String, onDismiss: () -> Unit) { + AlertDialog( + title = { Text(text = title) }, + text = { Text(text = message) }, + onDismissRequest = onDismiss, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.btn_continue)) + } + } + ) +} + +@Composable +private fun AboutDialog(context: Context, onDismiss: () -> Unit) { + AlertDialog( + title = { Text(text = stringResource(R.string.about_title)) }, + icon = {Icon(painterResource(R.drawable.ic_launcher_monochrome), contentDescription = stringResource(R.string.app_name), modifier = Modifier + .size(64.dp))}, + text = { Text(text = stringResource(R.string.about_text, context.packageManager.getPackageInfo(context.packageName, 0).versionName.toString())) }, + onDismissRequest = onDismiss, + confirmButton = { } + ) } \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 30f361c..85be721 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -2,6 +2,7 @@ Главная Хосты + Репозиторий хостов Настройки Продолжить Обновить @@ -25,6 +26,14 @@ Остановить сервис Перезапустить сервис Удалить + Скачать + Добавить + Назад + Установить + Обновить + Установка + Обновление + Установленно Переодически перезапускать сервис Авто обновление Версия @@ -44,4 +53,6 @@ Не доступно Работает Не работает + О приложении + Zaprett app От Cherret\nВерсия: %1$s \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ff62d13..e18823b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,6 +2,7 @@ zaprett Home Hosts + Hosts Repo Settings Continue Update @@ -25,6 +26,14 @@ Stop service Restart service Remove + Download + Add + Back + Install + Update + Installing + Updating + Installed Restart service periodicaly Autoupdate Version @@ -44,4 +53,6 @@ Not available Working Not working + About app + Zaprett App By Cherret\nVersion: %1$s \ No newline at end of file diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml index 5787c88..190e96d 100644 --- a/app/src/main/res/xml/file_paths.xml +++ b/app/src/main/res/xml/file_paths.xml @@ -1,4 +1,5 @@ +